You're launching a global campaign across 50 markets, 5 languages, and 40 product categories. That's 10,000 unique ad variants—and you need them ready by Monday. Your design team can't produce that volume manually, outsourcing to agencies would take weeks and cost a fortune, and copy-pasting templates in Photoshop isn't a realistic option.
This is the reality of performance marketing at scale. The modern approach treats creative generation as infrastructure: designers create one master template with intelligence built in, your systems inject localized data programmatically, and automation renders thousands of perfect variants in hours instead of weeks. You're not replacing creative work—you're separating template design (what humans do best) from variant generation (what automation does best).
In this guide, we'll build a complete pipeline for generating 10,000 localized ad variants using CE.SDK's Engine API. You'll see the full architecture, production-ready code for batch processing, localization handling across languages and markets, performance optimization strategies, and how to deliver finished assets to your ad platforms. This isn't a toy example—it's the real infrastructure teams like Plai use to generate 30,000+ ad creatives monthly.
This guide is written for engineers and technical teams building high-volume creative automation pipelines, and assumes familiarity with Node.js, APIs, and backend infrastructure.
If you're new to CE.SDK, start with our explainer on how the unified engine works to understand the platform. For strategic context on why automation infrastructure matters, see our article on creative automation infrastructure. This guide assumes you're ready to implement and want the technical details.
A quick note on CE.SDK templates: CE.SDK templates are the same artifacts used by both the visual editor and the Engine API, which is what allows designers and automation pipelines to share a single source of truth.
What You'll Build
By the end of this guide, you'll have:
- A production-ready batch rendering pipeline for 10,000+ variants
- A localization-safe template design strategy
- GPU-accelerated rendering with horizontal scaling
- Automated output delivery to ad platforms
System Architecture: From Template to 10,000 Variants
Before writing code, let's map the complete system architecture. You need four layers working together: template design, data management, generation engine, and output delivery.
Template Design Layer is where your designers create the master ad template using CE.SDK's visual editor. They define the layout, set up text variables for headlines and CTAs, configure image placeholders for product photos, lock brand elements that shouldn't change, and test with sample data to ensure the template handles different content lengths. This template becomes the source of truth for all 10,000 variants.
Data Management Layer holds three datasets: your product catalog (40 products with names, prices, images, and features translated into 5 languages), market configurations (50 markets with language, currency, locale, and platform specifications), and creative variants (different headlines and CTAs you want to A/B test). The generation engine combines these datasets to produce unique outputs.
Generation Engine is CE.SDK's Node.js renderer running on your servers. It loads the template, loops through your data combinations, injects each variant's specific data, renders the creative, and exports in your target formats. This runs as batch jobs—either scheduled overnight or triggered by campaign launches.
Output Management takes the rendered assets and delivers them where they need to go: uploads to your CDN with organized paths for easy retrieval, integration with ad platform APIs (Facebook Marketing API, Google Ads API), manifest generation for tracking which variants deployed where, and analytics tagging for performance measurement.
The data flow looks like this: Template (one master design) + Product Data (40 items × 5 languages) + Market Data (50 configurations) + Creative Variants (5 headline/CTA combinations) = 10,000 unique assets ready for deployment.
From an infrastructure perspective, you'll need a Node.js server environment (version 16 or higher recommended), rendering nodes (GPU-enabled for optimal performance), storage for templates, source data, and rendered outputs (S3, Google Cloud Storage, or equivalent), a CDN for asset delivery to ad platforms, and optionally Redis for caching frequently used data and a queue system like RabbitMQ or AWS SQS for managing batch jobs across multiple rendering nodes.
Why This Architecture Scales
Separation of concerns is the key to maintainability. Designers work in the visual editor without touching code. Engineers manage the data pipeline and rendering infrastructure. Marketers configure campaigns and trigger generation. Each team works in their domain, connected by the template format.
Horizontal scaling means adding more rendering nodes increases throughput linearly. One GPU node might handle 100 variants per second. Four nodes handle 400 per second. Ten nodes handle 1,000 per second. Your 10,000-variant job scales from 100 seconds down to 10 seconds just by adding capacity.
Caching strategies make repeated renders faster. Load the template once and duplicate it for each variant instead of reloading from disk every time. Pre-cache fonts used across all variants. Pre-load common assets into memory. These optimizations reduce render time by 40-60%.
Batch processing efficiency comes from parallel execution. Instead of processing one variant at a time, process 100 simultaneously (memory permitting), then the next 100, and so on. This keeps your GPUs saturated and maximizes throughput.
For the deep technical details on GPU-accelerated rendering and server deployment, see our CE.SDK Renderer deep-dive. For this guide, we'll focus on the generation pipeline itself.
Designing Templates That Work Across 50 Markets
Template design is where most multi-market campaigns fail. A template that works perfectly in English might break spectacularly in German (30% longer text), Japanese (different character density), or Arabic (right-to-left layout). Get this foundation right or waste time debugging broken renders at scale.
Text Overflow Handling
Different languages have wildly different text lengths for the same content. "Buy Now" in English becomes "Jetzt Kaufen" in German (33% longer), "今すぐ購入" in Japanese (compact characters), or "اشتر الآن" in Arabic (with different text flow). Your template needs to handle this automatically without manual adjustment per language.
CE.SDK's auto-formatting handles overflow intelligently. You configure text blocks with minimum and maximum font sizes, and the engine scales text to fit. If content is too long, it reduces font size within your defined range. If it's still too long, it wraps to multiple lines (if allowed) or truncates with ellipses (if single-line is required).
Here's how to configure a text block for flexible sizing:
// Configure headline text block with overflow handling
await engine.block.setFloat(headlineBlock, 'text/fontSize', 32);
await engine.block.setFloat(headlineBlock, 'text/minAutomaticFontSize', 24);
await engine.block.setBool(headlineBlock, 'text/automaticFontSizeEnabled', true);
await engine.block.setString(headlineBlock, 'text/horizontalAlignment', 'center');
await engine.block.setString(headlineBlock, 'text/verticalAlignment', 'center');
This configuration says: "Start at 32pt font. If text doesn't fit, scale down to 24pt. If it still doesn't fit, allow the text block to grow vertically (add lines). Center the text horizontally and vertically within its bounds."
Test your template with the longest and shortest language variants before automating. German and Finnish tend to be longest for European languages. Japanese and Korean are typically shortest due to character density. If your template works with German headlines and Japanese CTAs, it'll probably work everywhere.
Layout Flexibility and Safe Zones
Fixed layouts break when content exceeds expected lengths. Flexible layouts adapt. Define safe zones where critical elements (product images, logos, legal disclaimers) must remain visible regardless of text length. Allow text blocks to expand within defined boundaries that don't overlap these zones.
Responsive element positioning means elements move relative to each other instead of having absolute positions. If the headline grows from 2 lines to 3 lines, the CTA button shifts down automatically instead of overlapping the text. CE.SDK's constraint system handles this—you define relationships (button should be 20px below headline) and the engine maintains them.
Right-to-Left (RTL) Language Support
Arabic and Hebrew read right-to-left, which means your entire layout needs to mirror. Text aligns to the right, element ordering reverses, and directional indicators (arrows, chevrons) flip horizontally. You can't just change text alignment—you need full layout transformation.
Detect RTL markets in your market configuration data and apply layout transforms programmatically:
// Check if market requires RTL layout
if (market.textDirection === 'rtl') {
// Mirror the entire scene horizontally
await engine.block.setFloat(sceneBlock, 'transform/rotation', Math.PI);
await engine.block.setFloat(sceneBlock, 'transform/scaleX', -1);
// Right-align text blocks
await engine.block.setString(headlineBlock, 'text/horizontalAlignment', 'right');
await engine.block.setString(bodyBlock, 'text/horizontalAlignment', 'right');
}
Alternatively, design separate RTL and LTR template variants if the layout differences are significant. Load the appropriate template based on market configuration.
Font Management Across Languages
Latin fonts (Roboto, Open Sans, Arial) don't include glyphs for Chinese, Japanese, or Korean characters. Load those fonts and you'll see empty boxes instead of text. You need font families with appropriate character coverage for each language.
Google's Noto font family is your friend here—it has variants covering virtually every writing system. Map languages to appropriate fonts:
const fontMapping = {
'en': 'Roboto',
'es': 'Roboto',
'fr': 'Roboto',
'de': 'Roboto',
'ja': 'Noto Sans JP',
'ko': 'Noto Sans KR',
'zh': 'Noto Sans SC',
'ar': 'Noto Sans Arabic',
'he': 'Noto Sans Hebrew',
'th': 'Noto Sans Thai'
};
async function applyFontForLanguage(engine, textBlock, language) {
const fontFamily = fontMapping[language] || 'Roboto';
await engine.block.setString(textBlock, 'text/fontFamily', fontFamily);
}
Pre-load all required fonts before batch processing starts. Font loading has initialization overhead—doing it once per batch instead of per variant saves significant time.
Cultural and Visual Considerations
Colors carry different meanings across cultures. White signals purity and weddings in Western markets but represents mourning in parts of East Asia. Red is lucky in China but signals danger or alerts elsewhere. Your template should allow market-specific color overrides if needed.
Imagery needs localization too. Stock photos featuring Western models might not resonate in Asian markets. Lifestyle imagery showing specific products (food, clothing) needs cultural adaptation. Build flexibility into your template for swapping background images or graphic elements per market.
Date formats, currency symbols, and number formatting vary by locale. Don't hardcode "12/25/2024" (US format) or "$99.99" (dollar sign). Use market configuration to format correctly: "25.12.2024" and "99,99 €" for European markets, "2024年12月25日" and "¥11,000" for Japan.
Digitas built a centralized campaign portal for 600+ car dealers across 9-10 automotive brands with automated multilingual asset generation in German, French, and Italian. Their 140 active campaigns generate thousands of assets with self-service creation—adapting campaigns from days to seconds while maintaining brand compliance across all stakeholders.
Martin Röhr, Senior Project Manager: "We shipped a much better product than initially planned. Building something like this ourselves would have taken years or ten times the budget."
Template Design Checklist
Before moving to code, validate your template:
- Test with longest language variant (usually German or Finnish)
- Test with shortest variant (often Japanese due to character density)
- Verify RTL layout if supporting Arabic or Hebrew markets
- Confirm font coverage for all target languages (no missing glyphs)
- Leave flexible space for text expansion (30% buffer minimum)
- Define safe zones for critical brand elements
- Test all CTA button texts to ensure they fit
- Validate currency and number formatting across locales
Get the template right and automation becomes straightforward. Get it wrong and you'll debug layout issues across 10,000 variants.
Organizing Data for 10,000 Variants
Data structure determines how efficiently you generate at scale. Three layers of data combine to produce your variants: product information, market configurations, and creative variations. Structure them correctly and your generation code becomes clean. Structure them poorly and you'll fight data transformation bugs.
Product Data Structure
Each product needs information in all target languages plus market-specific details:
{
"productId": "PRD-001",
"category": "electronics",
"name": {
"en": "Wireless Noise-Cancelling Headphones",
"es": "Auriculares Inalámbricos con Cancelación de Ruido",
"fr": "Écouteurs Sans Fil à Réduction de Bruit",
"de": "Kabellose Kopfhörer mit Geräuschunterdrückung",
"ja": "ワイヤレスノイズキャンセリングヘッドホン"
},
"price": {
"USD": 299.99,
"EUR": 279.99,
"GBP": 249.99,
"JPY": 33000
},
"images": {
"product": "https://cdn.example.com/products/prd-001-main.jpg",
"lifestyle": "https://cdn.example.com/products/prd-001-lifestyle.jpg",
"detail": "https://cdn.example.com/products/prd-001-detail.jpg"
},
"features": {
"en": ["Active Noise Cancellation", "40-Hour Battery Life", "Premium Sound Quality"],
"es": ["Cancelación Activa de Ruido", "40 Horas de Batería", "Calidad de Sonido Premium"],
"fr": ["Réduction Active du Bruit", "Autonomie 40 Heures", "Qualité Audio Premium"],
"de": ["Aktive Geräuschunterdrückung", "40 Stunden Akkulaufzeit", "Premium-Klangqualität"],
"ja": ["アクティブノイズキャンセリング", "40時間バッテリー", "プレミアムサウンド"]
}
}
This structure supports language-specific content (names, features) and market-specific pricing. Store products in a database (PostgreSQL, MongoDB) or JSON files depending on your infrastructure and update frequency.
Market Configuration
Markets define language, currency, locale, and platform-specific requirements:
{
"marketId": "US",
"country": "United States",
"language": "en",
"locale": "en-US",
"currency": "USD",
"currencySymbol": "$",
"currencyPosition": "before",
"textDirection": "ltr",
"dateFormat": "MM/DD/YYYY",
"numberFormat": {
"decimal": ".",
"thousands": ","
},
"targetPlatforms": ["facebook", "instagram", "google"],
"adSpecs": {
"facebook": {
"feed": { "width": 1080, "height": 1080, "format": "jpg" },
"story": { "width": 1080, "height": 1920, "format": "jpg" }
},
"instagram": {
"square": { "width": 1080, "height": 1080, "format": "jpg" },
"portrait": { "width": 1080, "height": 1350, "format": "jpg" }
}
}
}
Market configurations centralize localization logic. When rendering for the German market, you load its configuration and apply language, currency, number formatting, and platform specs automatically.
Creative Variant Configuration
Different headlines and CTAs for A/B testing:
{
"variantId": "VAR-001",
"name": "Value-Focused",
"headline": {
"en": "Premium Quality at an Unbeatable Price",
"es": "Calidad Premium a un Precio Inmejorable",
"fr": "Qualité Premium à Prix Imbattable",
"de": "Premium-Qualität zum unschlagbaren Preis",
"ja": "プレミアム品質、比類なき価格"
},
"cta": {
"en": "Shop Now",
"es": "Comprar Ahora",
"fr": "Acheter Maintenant",
"de": "Jetzt Kaufen",
"ja": "今すぐ購入"
},
"background": "gradient-blue",
"layout": "product-focus"
}
Creative variants let you test messaging strategies across markets. Same product, same market, different headline/CTA combinations to find what resonates best.
Combining Data Layers
The Cartesian product of these layers creates your 10,000 variants:
- 40 products
- × 50 markets
- × 5 creative variants
- = 10,000 unique combinations
Each combination needs a unique render with the right product, localized for the right market, using the right creative messaging. Your generation code loops through these combinations systematically. As your variant dimensions grow, treat generation as a queued system rather than a single batch job to avoid runaway combinatorics.
For efficient processing, organize batches strategically. Group by market if you want to process all products for one market sequentially (better for CDN upload batching). Group by product if you want to process all markets for one product in parallel (better for data loading efficiency). Choose based on your downstream workflows.
Building the Generation Engine
Now for the code that actually generates 10,000 variants. This is production-ready implementation with error handling, performance optimization, and progress tracking.
Step 1: Initialize the Rendering Engine
import CreativeEngine from '@cesdk/node';
import fs from 'fs/promises';
import path from 'path';
// Initialize CE.SDK engine
const engine = await CreativeEngine.init({
license: process.env.CESDK_LICENSE,
baseURL: 'https://cdn.img.ly/packages/imgly/cesdk-node/1.31.0/assets'
});
console.log('✓ CreativeEngine initialized');
This initializes CE.SDK's Node.js engine. Store your license key in environment variables for security.
Step 2: Load and Cache the Template
// Load template once, reuse for all variants
const templatePath = './templates/product-ad-template.scene';
const sceneId = await engine.scene.loadFromArchiveURL(templatePath);
console.log(`✓ Template loaded: ${sceneId}`);
// Get block IDs for elements we'll modify
// Strategy 1: Use findByType and filter by kind
const allTextBlocks = engine.block.findByType('text');
const blocks = {
headline: allTextBlocks.find(id => engine.block.getKind(id) === 'headline'),
productName: allTextBlocks.find(id => engine.block.getKind(id) === 'product-name'),
ctaButton: allTextBlocks.find(id => engine.block.getKind(id) === 'cta-button'),
price: allTextBlocks.find(id => engine.block.getKind(id) === 'price'),
feature1: allTextBlocks.find(id => engine.block.getKind(id) === 'feature-1'),
feature2: allTextBlocks.find(id => engine.block.getKind(id) === 'feature-2'),
feature3: allTextBlocks.find(id => engine.block.getKind(id) === 'feature-3')
};
// Get image block
const allImageBlocks = engine.block.findByType('graphic');
blocks.productImage = allImageBlocks.find(id => engine.block.getKind(id) === 'product-image');
console.log('✓ Template blocks mapped');Loading the template once and reusing it for all variants is critical for performance. Reloading from disk for each of 10,000 variants would add hours to your batch job. The specific strategy for managing scenes and blocks depends on your workflow—see the generation function for implementation considerations.
Step 3: Core Generation Function
async function generateAdVariant(engine, templateSceneId, blocks, product, market, variant) {
// Note: For high-volume generation, consider your scene management strategy.
// Option 1: Load template fresh each time (simpler, slower)
// Option 2: Duplicate blocks and modify (more complex, faster)
// Option 3: Modify blocks in place, export, then restore (fastest, requires careful state management)
// This example shows the modify-in-place approach with proper cleanup.
try {
const lang = market.language;
// Update headline with localized creative variant
await engine.block.setString(
blocks.headline,
'text/text',
variant.headline[lang]
);
// Update product name
await engine.block.setString(
blocks.productName,
'text/text',
product.name[lang]
);
// Format and update price with market-specific currency
const formattedPrice = formatCurrency(
product.price[market.currency],
market.currencySymbol,
market.currencyPosition,
market.numberFormat
);
await engine.block.setString(
blocks.price,
'text/text',
formattedPrice
);
// Update CTA button
await engine.block.setString(
blocks.ctaButton,
'text/text',
variant.cta[lang]
);
// Update product features (if product has features)
if (product.features && product.features[lang]) {
const features = product.features[lang];
if (features[0]) await engine.block.setString(blocks.feature1, 'text/text', features[0]);
if (features[1]) await engine.block.setString(blocks.feature2, 'text/text', features[1]);
if (features[2]) await engine.block.setString(blocks.feature3, 'text/text', features[2]);
}
// Replace product image by setting the fill image URI
await engine.block.setString(
blocks.productImage,
'fill/image/imageFileURI',
product.images.product
);
// Apply market-specific font for proper character support
const font = getFontForLanguage(market.language);
await engine.block.setString(blocks.headline, 'text/fontFamily', font);
await engine.block.setString(blocks.productName, 'text/fontFamily', font);
// Handle RTL layouts for Arabic/Hebrew markets
if (market.textDirection === 'rtl') {
await engine.block.setString(blocks.headline, 'text/horizontalAlignment', 'right');
await engine.block.setString(blocks.productName, 'text/horizontalAlignment', 'right');
}
// Generate filename: {marketId}_{productId}_{variantId}.png
const filename = `${market.marketId}_${product.productId}_${variant.variantId}.png`;
const outputDir = `./output/${market.marketId}/${product.category}`;
await fs.mkdir(outputDir, { recursive: true });
const outputPath = path.join(outputDir, filename);
// Export the rendered variant using the scene block
const sceneBlock = engine.block.findByType('scene')[0];
const blob = await engine.block.export(sceneBlock, {
mimeType: 'image/png'
});
const buffer = Buffer.from(await blob.arrayBuffer());
await fs.writeFile(outputPath, buffer);
return { success: true, path: outputPath, filename };
} catch (error) {
return {
success: false,
error: error.message,
product: product.productId,
market: market.marketId,
variant: variant.variantId
};
}
}This function handles a single variant: duplicates the template, injects all localized data, applies market-specific formatting, renders, exports, and cleans up. The try/catch/finally pattern ensures memory cleanup even if rendering fails.
Step 4: Orchestrating the Batch
async function generateAllVariants() {
// Load data
const products = await loadProducts(); // Your function to load 40 products
const markets = await loadMarkets(); // Your function to load 50 markets
const variants = await loadCreativeVariants(); // Your function to load 5 variants
const startTime = Date.now();
let completed = 0;
let failed = 0;
const total = products.length * markets.length * variants.length;
const errors = [];
console.log(`Starting generation of ${total} variants...`);
// Build all combinations
const combinations = [];
for (const product of products) {
for (const market of markets) {
for (const variant of variants) {
combinations.push({ product, market, variant });
}
}
}
// Process in parallel batches to manage memory
const batchSize = 100; // Tune based on your server's memory
for (let i = 0; i < combinations.length; i += batchSize) {
const batch = combinations.slice(i, i + batchSize);
const batchStartTime = Date.now();
// Process batch in parallel
const results = await Promise.all(
batch.map(({ product, market, variant }) =>
generateAdVariant(engine, sceneId, blocks, product, market, variant)
)
);
// Track results
for (const result of results) {
if (result.success) {
completed++;
} else {
failed++;
errors.push(result);
}
}
// Progress reporting
const batchDuration = (Date.now() - batchStartTime) / 1000;
const overallElapsed = (Date.now() - startTime) / 1000;
const rate = completed / overallElapsed;
const remaining = (total - completed - failed) / rate;
console.log(
`Batch ${Math.ceil((i + 1) / batchSize)}/${Math.ceil(total / batchSize)}: ` +
`${completed} completed, ${failed} failed | ` +
`${rate.toFixed(1)} variants/sec | ` +
`${Math.ceil(remaining)}s remaining`
);
}
// Final summary
const totalDuration = (Date.now() - startTime) / 1000;
console.log('\n=== Generation Complete ===');
console.log(`✓ Completed: ${completed}/${total} (${((completed/total)*100).toFixed(1)}%)`);
console.log(`✗ Failed: ${failed}/${total}`);
console.log(`⏱ Duration: ${(totalDuration / 60).toFixed(1)} minutes`);
console.log(`⚡ Average rate: ${(completed / totalDuration).toFixed(1)} variants/sec`);
// Log errors for debugging
if (errors.length > 0) {
console.log('\nFailed variants:');
errors.forEach(err => {
console.log(` ${err.product}/${err.market}/${err.variant}: ${err.error}`);
});
}
return { completed, failed, errors, duration: totalDuration };
}
// Run the batch generation
const result = await generateAllVariants();
This orchestration code processes all 10,000 variants in batches of 100 (tune this based on available memory). It tracks progress, estimates time remaining, handles errors gracefully, and provides a comprehensive summary when complete.
Performance Optimization: From Hours to Minutes
Default configurations won't give you optimal performance. Here's how to tune for speed and scale.
GPU Acceleration: Significant Performance Boost
CE.SDK Renderer leverages GPU acceleration for faster rendering compared to CPU-only processing. The engine automatically uses available GPU resources when properly configured on GPU-enabled infrastructure.
Infrastructure requirements: Use GPU-enabled instances (AWS g4dn.xlarge or larger, Google Cloud n1-standard with GPU, or on-premise servers with NVIDIA GPUs). GPU instances cost more per hour but complete jobs faster, often resulting in lower total cost for batch workloads.
See our CE.SDK Renderer technical guide for detailed GPU deployment instructions.
Template and Asset Caching
Loading templates and assets repeatedly is wasteful. Cache them:
// Bad: Loading template for every variant (slow)
for (const combo of combinations) {
const scene = await engine.scene.loadFromArchiveURL(templatePath); // Repeated loading
// ... render ...
}
// Good: Load once, modify blocks in place for each variant (fast)
const templateScene = await engine.scene.loadFromArchiveURL(templatePath);
// Get references to blocks once
const allTextBlocks = engine.block.findByType('text');
const blocks = {
headline: allTextBlocks.find(id => engine.block.getKind(id) === 'headline'),
productName: allTextBlocks.find(id => engine.block.getKind(id) === 'product-name'),
// ... etc
};
// Then modify these same blocks for each variant, exporting between changes
for (const combo of combinations) {
// Modify block properties
await engine.block.setString(blocks.headline, 'text/text', combo.headline);
// ... other modifications ...
// Export
const blob = await engine.block.export(sceneBlock, { mimeType: 'image/png' });
// Continue to next variant
}Note on font pre-loading: While font caching can improve performance, the specific font loading API varies by implementation. Consult the CE.SDK documentation for current best practices on font management and caching strategies.
Batch Size Tuning
Batch size affects throughput and memory usage. Too small (10-20) means you're not maximizing parallelism. Too large (500+) risks running out of memory and crashing mid-batch.
Find your optimal batch size empirically:
const testCombinations = combinations.slice(0, 1000);
const batchSizes = [50, 100, 150, 200];
for (const size of batchSizes) {
const start = Date.now();
// Process test batch
for (let i = 0; i < testCombinations.length; i += size) {
const batch = testCombinations.slice(i, i + size);
await Promise.all(batch.map(c => generateAdVariant(/* ... */)));
}
const duration = Date.now() - start;
const rate = 1000 / (duration / 1000);
console.log(`Batch size ${size}: ${rate.toFixed(1)} variants/sec`);
}
Typical sweet spot: 100-200 for servers with 16-32GB RAM.
Horizontal Scaling with Multiple Rendering Nodes
One server hitting limits? Add more nodes and distribute work via a queue system:
// Simplified queue-based architecture
import Queue from 'bull'; // Or AWS SQS, RabbitMQ, etc.
const renderQueue = new Queue('ad-generation');
// Producer: Add jobs to queue
for (const combo of combinations) {
await renderQueue.add(combo);
}
// Consumer (runs on each rendering node):
renderQueue.process(async (job) => {
const { product, market, variant } = job.data;
return await generateAdVariant(engine, sceneId, blocks, product, market, variant);
});
With 4 rendering nodes, you get 4x throughput. Your 10,000-variant job that took 25 minutes on one node completes in ~6 minutes with four nodes.
Performance Benchmarks
These are estimated performance ranges. Real-world performance varies significantly based on template complexity, image sizes, effects applied, and infrastructure configuration:
- Single CPU core: ~5-10 variants/second
- Single GPU (NVIDIA T4): ~50-100 variants/second
- 4 GPU nodes: ~200-400 variants/second
- 10,000 variants: Approximately 25 seconds to 30 minutes depending on setup
Example estimate: AWS g4dn.xlarge (1 GPU) @ $0.526/hour might generate ~100 variants/sec = 10,000 in ~100 seconds. Estimated total cost: ~$0.015 per batch. Scaling to 4 nodes could reduce time to ~25 seconds with 4x the hourly cost but similar total spend due to shorter duration.
Always benchmark with your specific templates and infrastructure before committing to production architecture.
Output Management and Delivery to Ad Platforms
Generated assets need organized storage and delivery to ad platforms. Here's the complete workflow.
CDN Upload and Organization
Upload rendered assets to S3 (or equivalent) with organized paths:
import AWS from 'aws-sdk';
const s3 = new AWS.S3();
async function uploadToS3(localPath, product, market, variant) {
// Organized path: ads/{marketId}/{category}/{filename}
const key = `ads/${market.marketId}/${product.category}/${market.marketId}_${product.productId}_${variant.variantId}.png`;
const fileContent = await fs.readFile(localPath);
await s3.putObject({
Bucket: process.env.S3_BUCKET,
Key: key,
Body: fileContent,
ContentType: 'image/png',
CacheControl: 'public, max-age=31536000', // 1 year cache
Metadata: {
productId: product.productId,
marketId: market.marketId,
variantId: variant.variantId,
category: product.category,
generatedAt: new Date().toISOString()
}
}).promise();
const cdnUrl = `https://cdn.example.com/${key}`;
return cdnUrl;
}
Metadata tagging enables analytics tracking and asset management downstream.
Ad Platform Integration
Upload assets to Facebook/Instagram via Marketing API:
import { FacebookAdsApi, AdAccount, AdImage } from 'facebook-nodejs-business-sdk';
async function uploadToFacebookAdLibrary(imagePath, product, market) {
const account = new AdAccount(`act_${process.env.FB_AD_ACCOUNT_ID}`);
const image = new AdImage(null, process.env.FB_AD_ACCOUNT_ID);
image.create({
filename: path.basename(imagePath),
bytes: await fs.readFile(imagePath)
});
// Tag with metadata for campaign organization
const imageHash = image.hash;
return {
hash: imageHash,
url: image.url,
productId: product.productId,
marketId: market.marketId
};
}
Similar integrations exist for Google Ads API, TikTok Ads, LinkedIn Ads, and programmatic platforms.
Manifest Generation
Create a manifest file tracking all generated variants:
const manifest = {
generatedAt: new Date().toISOString(),
totalVariants: 10000,
variants: []
};
// Add each variant to manifest during generation
manifest.variants.push({
filename: `${market.marketId}_${product.productId}_${variant.variantId}.png`,
cdnUrl: 'https://cdn.example.com/ads/US/electronics/US_PRD001_VAR1.png',
productId: product.productId,
productName: product.name.en,
marketId: market.marketId,
language: market.language,
variantId: variant.variantId,
headline: variant.headline[market.language],
platforms: market.targetPlatforms
});
await fs.writeFile('./output/manifest.json', JSON.stringify(manifest, null, 2));
This manifest feeds your campaign management tools, analytics dashboards, and reporting systems.
Troubleshooting Common Issues
Even with solid code, you'll hit edge cases. Here's how to debug them.
Issue: Text Overflow in German Headlines
Problem: German headlines exceed layout bounds, causing truncation or broken layouts.
Solution: Configure text blocks with proper overflow handling and test with German content during template design:
await engine.block.setFloat(headlineBlock, 'text/fontSize', 32);
await engine.block.setFloat(headlineBlock, 'text/minAutomaticFontSize', 20);
await engine.block.setBool(headlineBlock, 'text/automaticFontSizeEnabled', true);
Always test templates with longest-language variants before automating thousands of renders.
Issue: Memory Leaks During Batch Processing
Problem: Memory usage grows continuously, eventually crashing the process.
Solution: Ensure proper resource cleanup and consider your scene management strategy:
try {
// Load or configure scene for variant
// ... rendering ...
// ... export ...
} finally {
// Clean up blocks if needed using engine.block.destroy()
// Specific cleanup depends on your implementation approach
}
Monitor memory with process.memoryUsage() and tune batch sizes accordingly.
Issue: Inconsistent Font Rendering
Problem: Some variants show correct fonts, others show fallbacks or missing glyphs.
Solution: Ensure all required fonts are available and properly configured before batch processing. Verify font files are accessible to the rendering engine (either bundled or loaded from URLs). Consult the CE.SDK documentation for specific font management approaches for your use case.
Issue: Slow First Batch, Then Fast
Problem: First 100 variants take 5 minutes, subsequent batches take 1 minute.
Explanation: Template loading, font initialization, and GPU warmup happen on first batch. This is normal overhead.
Solution: Run a small warmup batch (10-20 variants) before starting production batches, or accept the first-batch penalty as initialization cost.
Issue: Special Characters Rendering as Boxes
Problem: Japanese, Arabic, or Thai characters show as empty boxes (□).
Solution: Verify fonts support required character sets. Use Noto Sans font family for comprehensive Unicode coverage:
// Good: Noto Sans supports extensive character sets
await engine.block.setString(textBlock, 'text/fontFamily', 'Noto Sans JP');
// Bad: Roboto doesn't include CJK characters
await engine.block.setString(textBlock, 'text/fontFamily', 'Roboto'); // Will show boxes for Japanese
Debugging Best Practices
- Start with a single variant to isolate template issues before running full batches
- Log each data injection step to catch malformed data
- Validate data completeness before sending to the engine (check for null/undefined values)
- Keep a test dataset small (10-20 variants) during development
- Export one variant manually through the visual editor to verify template correctness
From Proof of Concept to Production
You now have a complete pipeline for generating 10,000 localized ad variants programmatically. The architecture separates template design (visual editor), data management (structured JSON), generation (batch rendering), and delivery (CDN and ad platforms). This same pattern scales to millions of variants by adding rendering nodes horizontally.
The approach isn't limited to ads. Apply it to email personalization (thousands of unique email headers), social media content (automated post generation for multiple accounts), product catalogs (e-commerce banners for entire inventory), or video campaigns (personalized video at scale). The template-plus-data model works wherever you need high-volume creative generation.
Production considerations you'll want to add: monitoring and alerting for failed generation jobs (integrate with Datadog, CloudWatch, or similar), versioning for templates and data (Git for templates, database migrations for product data), A/B testing integration to track which creative variants perform best, and compliance checks to ensure all outputs meet brand guidelines and legal requirements.
Next steps for implementation: start with a small test batch (100 variants) to validate your complete pipeline end-to-end, optimize batch size and parallelization based on your infrastructure capacity, integrate with your existing marketing automation workflows, add quality gates and approval workflows for high-stakes campaigns, and set up monitoring dashboards to track generation performance and error rates.
For more background, see our CE.SDK explainer for understanding the visual editor and engine API capabilities, our creative automation infrastructure guide for strategic context on why this architecture matters, and our CE.SDK Renderer technical details for GPU rendering optimization and server deployment best practices.
Ready to test this yourself? Try CE.SDK to experiment with template creation and rendering workflows.