Optimize CE.SDK integration for faster startup, efficient memory usage, and reliable performance in Node.js server environments.
The @cesdk/node (WASM) and @cesdk/node-native (native) packages provide full CreativeEngine functionality for server-side processing. Optimizing how you load, use, and dispose of the engine improves throughput and resource efficiency in production environments.
This guide covers code splitting for serverless environments, caching and limiting the engine’s network requests, memory monitoring for long-running processes, export timeout configuration, and proper lifecycle management patterns. The example project demonstrates the caching section.
Code Splitting#
Use dynamic imports to load the engine only when needed. This reduces cold start time in serverless environments where the engine may not be used for every request.
async function loadCreativeEngine(): Promise<typeof CreativeEngine> { console.log('Lazy loading CreativeEngine...'); const startTime = Date.now();
// Dynamic import - engine module is loaded only when this function is called const { default: CreativeEngine } = await import('@cesdk/node');
const loadTime = Date.now() - startTime; console.log(`CreativeEngine loaded in ${loadTime}ms`);
return CreativeEngine;}This pattern defers engine loading until loadCreativeEngine() is called. In serverless functions, requests that don’t require image processing skip the engine load entirely.
Caching and Limiting Concurrent Connections#
@cesdk/node downloads scenes, images, and fonts with the fetch function built into Node.js. It also downloads its engine files that way when baseURL points to a URL. Every new engine instance downloads its assets again, and a scene with many images starts its downloads at the same time. A storage backend with a rate limit can reject these bursts.
Node.js sends every fetch request through a global dispatcher. With the undici package, you can replace it with one that caches responses and limits the connections per origin. The engine picks up the new dispatcher without any change to your engine code.
Install undici#
Node.js uses undici internally but doesn’t expose it as a module, so install the package from npm:
npm install undici@8Use undici 8, which requires Node.js 22.19 or later. On older Node.js versions, use undici 7.27 or later. On Node.js 26, the built-in fetch ignores a dispatcher set with undici 7.26 or earlier, without an error.
Configure the Dispatcher#
Call setGlobalDispatcher() once when your server starts, before the engine downloads anything. The dispatcher below allows two connections per origin and keeps the responses of the asset origin in a SQLite file:
import { tmpdir } from 'node:os';import { join } from 'node:path';import { Agent, cacheStores, interceptors, setGlobalDispatcher } from 'undici';
const ASSET_ORIGIN = 'https://img.ly';
setGlobalDispatcher( new Agent({ connections: 2, allowH2: false }).compose( interceptors.cache({ store: new cacheStores.SqliteCacheStore({ location: join(tmpdir(), 'cesdk-fetch-cache.db') }), cacheByDefault: 24 * 60 * 60 * 1000, origins: [ASSET_ORIGIN] }) ));connectionsis the highest number of open connections per origin. Further requests wait in a queue until a connection is free.allowH2: falsekeeps every connection on HTTP/1.1. Over HTTP/2, one connection carries many requests at the same time, soconnectionsno longer limits them. The built-infetchof Node.js 26 uses HTTP/2 when the server supports it.cacheByDefaultis how long, in milliseconds, to keep a response whose headers set no lifetime: nomax-age,s-maxage,Expires, orLast-Modified. A response withmax-age,s-maxage, orExpiresfollows it instead. A response with onlyLast-Modifiedis kept for a tenth of the time since that date.cacheByDefaultalso applies to404responses, so an asset that was missing on the first request stays missing until the entry expires or you delete the cache.originslimits the cache to the hosts that serve your assets. Without it,cacheByDefaultalso applies to your server’s otherfetchcalls, so an API response without cache headers is reused for 24 hours.SqliteCacheStorewrites the cache to disk, so a restarted process reuses it. In production, setlocationto a persistent directory that only your server can access, and cap the number of entries withmaxCount, which has no limit by default. It uses the built-innode:sqlitemodule, which requires Node.js 22.13 or later and which some Node.js versions mark as experimental with a warning.
Other Setups#
The connection limit and the cache work independently. To only limit connections, install a plain Agent:
setGlobalDispatcher(new Agent({ connections: 2, allowH2: false }));To only cache, compose the cache interceptor onto an Agent without a limit:
setGlobalDispatcher( new Agent().compose( interceptors.cache({ store: new cacheStores.SqliteCacheStore({ location: join(tmpdir(), 'cesdk-fetch-cache.db') }), cacheByDefault: 24 * 60 * 60 * 1000, origins: [ASSET_ORIGIN] }) ));If you don’t need the cache to survive a restart, use MemoryCacheStore. The cache then lives only as long as the process. By default, it skips responses larger than 5 MB and holds at most 100 MB, which you can change with its maxEntrySize and maxSize options:
setGlobalDispatcher( new Agent({ connections: 2, allowH2: false }).compose( interceptors.cache({ store: new cacheStores.MemoryCacheStore(), cacheByDefault: 24 * 60 * 60 * 1000, origins: [ASSET_ORIGIN] }) ));Verify the Behavior#
undici reports each request on Node’s diagnostics_channel module. The example counts the requests to the asset origin that go out over the network, and how many of them are open at the same time:
const stats = { sent: 0, active: 0, maxActive: 0 };const onTheWire = new WeakSet<object>();
diagnostics_channel.subscribe('undici:client:sendHeaders', (message) => { const { request } = message as { request: { origin: string } }; if (request.origin !== ASSET_ORIGIN) return; onTheWire.add(request); stats.sent += 1; stats.active += 1; stats.maxActive = Math.max(stats.maxActive, stats.active);});
const onFinished = (message: unknown) => { const { request } = message as { request: object }; if (!onTheWire.delete(request)) return; stats.active -= 1;};diagnostics_channel.subscribe('undici:request:trailers', onFinished);diagnostics_channel.subscribe('undici:request:error', onFinished);undici:client:sendHeaders fires when undici sends a request. A response served from the cache sends no request, so it doesn’t appear in the count. undici:request:trailers and undici:request:error fire when a request ends.
The example then renders the same six-image design twice, each time with a new engine, and logs the counts:
async function renderDesign(label: string): Promise<void> { const engine = await CreativeEngine.init({ baseURL: process.env.IMGLY_LOCAL_ASSETS_URL // license: process.env.CESDK_LICENSE, });
try { const sentBefore = stats.sent; stats.maxActive = 0;
const scene = engine.scene.create(); const page = engine.block.create('page'); engine.block.appendChild(scene, page); engine.block.setWidth(page, 900); engine.block.setHeight(page, 600);
IMAGE_URLS.forEach((url, index) => { const block = engine.block.create('graphic'); engine.block.setShape(block, engine.block.createShape('rect')); const fill = engine.block.createFill('image'); engine.block.setString(fill, 'fill/image/imageFileURI', url); engine.block.setFill(block, fill); engine.block.setPositionX(block, (index % 3) * 300); engine.block.setPositionY(block, Math.floor(index / 3) * 300); engine.block.setWidth(block, 300); engine.block.setHeight(block, 300); engine.block.appendChild(page, block); });
await engine.block.export(page);
console.log( `${label}: ${stats.sent - sentBefore} requests sent for ` + `${IMAGE_URLS.length} images, at most ${stats.maxActive} at a time` ); } finally { engine.dispose(); }}
await renderDesign('First render');await renderDesign('Second render');The first render sends six requests, at most two at a time. The second render sends none, because the cache answers every request. A new run of the example also sends none, because the cache file persists. This holds as long as the cached responses are fresh: the sample images allow up to one hour. After that, undici asks the server whether each image changed, and the count includes these requests even when the server answers that nothing changed.
Why a Connection Limit of 1 Can Show Many Requests in Flight#
If you log requests on undici:request:create, a dispatcher with connections: 1 can still appear to have many requests in flight. Three things cause this:
- The limit is per origin. An origin is the combination of scheme, host, and port. Requests to different origins use separate connections, so with
connections: 1and two origins, up to two requests run at the same time.undicihas no limit across all origins. undici:request:createfires when a request enters the queue.undicisends the request later, when a connection is free. Withconnections: 1, it sends one request at a time over that connection, so the other requests only wait in the queue.- HTTP/2 runs many requests over one connection. Without
allowH2: false, a server that supports HTTP/2 receives all requests at the same time, even withconnections: 1.
Count undici:client:sendHeaders instead, as the example does, to see how many requests undici actually sends.
Memory Management#
Monitor memory usage in long-running server processes using the editor’s memory APIs. This helps detect memory accumulation across requests and triggers cleanup actions.
Monitoring Memory Usage#
Use engine.editor.getUsedMemory() to check current memory consumption and engine.editor.getAvailableMemory() to check remaining capacity.
function logMemoryStats(engine: CreativeEngine, label: string): void { const usedMemory = engine.editor.getUsedMemory(); const availableMemory = engine.editor.getAvailableMemory();
// Convert to Number for arithmetic (memory APIs may return BigInt) const usedNum = Number(usedMemory); const availableNum = Number(availableMemory); const totalMemory = usedNum + availableNum; const usagePercentage = ((usedNum / totalMemory) * 100).toFixed(2);
console.log(`Memory Stats [${label}]:`); console.log(` Used: ${formatBytes(usedMemory)}`); console.log(` Available: ${formatBytes(availableMemory)}`); console.log(` Usage: ${usagePercentage}%`);}
function formatBytes(bytes: number | bigint): string { const numBytes = typeof bytes === 'bigint' ? Number(bytes) : bytes; if (numBytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(numBytes) / Math.log(k)); return parseFloat((numBytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];}Track these values across requests to:
- Detect memory accumulation that indicates leaks
- Trigger engine disposal and recreation when approaching limits
- Log memory metrics for production monitoring
Calculating Usage Percentage#
Calculate memory usage as a percentage to implement automatic cleanup thresholds. Memory APIs may return BigInt values, so convert to Number for arithmetic operations.
const usedMemory = engine.editor.getUsedMemory();const availableMemory = engine.editor.getAvailableMemory();
// Convert to Number for arithmetic (memory APIs may return BigInt)const usedNum = Number(usedMemory);const availableNum = Number(availableMemory);const totalMemory = usedNum + availableNum;const usagePercentage = (usedNum / totalMemory) * 100;
if (usagePercentage > 80) { console.warn('High memory usage detected, consider disposing engine');}Managing Memory in Request Handlers#
For HTTP servers or queue workers, reset state between requests to prevent memory accumulation:
- Clear the scene before processing each request
- Avoid storing references to blocks across requests
- Consider disposing and recreating the engine after a threshold of requests
Export Optimization#
Configure export behavior for reliable processing of large designs or complex scenes.
Export Timeouts#
Configure inactivity timeouts to prevent hanging exports. Use engine.unstable_setExportInactivityTimeout() for image exports and engine.unstable_setVideoExportInactivityTimeout() for video exports.
// Configure export timeouts for reliabilityengine.unstable_setExportInactivityTimeout(30_000);engine.unstable_setVideoExportInactivityTimeout(60_000);console.log('Export timeouts configured (30s image, 60s video)');The default timeout is 10 seconds. Increase this value for:
- High-resolution exports (4K+)
- Complex scenes with many elements
- Video exports with multiple tracks
- Servers with limited CPU resources
Export Size Limits#
Check export capabilities before processing large designs using engine.editor.getMaxExportSize().
const maxExportSize = engine.editor.getMaxExportSize();console.log('Max export size:', maxExportSize, 'pixels');
// Validate design dimensions before exportconst pageWidth = engine.block.getWidth(page);const pageHeight = engine.block.getHeight(page);
if (pageWidth > maxExportSize || pageHeight > maxExportSize) { throw new Error('Design exceeds maximum export size');}Validate design dimensions against this limit before starting export to fail fast on oversized designs.
Engine Lifecycle#
Follow proper patterns for initializing and disposing the engine to prevent memory leaks and ensure consistent behavior across requests.
Initialization#
Initialize the engine with minimal configuration for server environments. The engine doesn’t need UI-related settings.
import CreativeEngine from '@cesdk/node';
const config = { license: process.env.CESDK_LICENSE || '', logger: (level: string, ...args: unknown[]) => { if (level === 'error' || level === 'warn') { console.log(`[${level}]`, ...args); } }};
const engine = await CreativeEngine.init(config);Consider logging levels carefully in production to avoid excessive log output.
Engine Reuse Patterns#
For server environments, choose between per-request engines or a shared engine based on your workload:
Per-Request Pattern (recommended for serverless):
async function handleRequest(designData: Buffer): Promise<Buffer> { const CreativeEngine = await loadCreativeEngine(); const engine = await CreativeEngine.init(config);
try { const result = await processDesign(engine, designData); return result; } finally { engine.dispose(); }}Shared Engine Pattern (for long-running servers):
let engine: CreativeEngine | null = null;
async function getEngine(): Promise<CreativeEngine> { if (!engine) { const CreativeEngine = await loadCreativeEngine(); engine = await CreativeEngine.init(config); } return engine;}
async function handleRequest(designData: Buffer): Promise<Buffer> { const eng = await getEngine();
// Clear previous state const scenes = eng.scene.findAll(); scenes.forEach((scene) => eng.block.destroy(scene));
return processDesign(eng, designData);}Disposal#
Always dispose the engine when processing completes to free native resources. In serverless environments, dispose before the function returns:
async function processDesign(engine: CreativeEngine): Promise<void> { try { // Process the design await doWork(engine); } finally { // Always dispose to free resources engine.dispose(); console.log('Engine disposed'); }}For shared engines in long-running processes, dispose on process shutdown:
process.on('SIGTERM', async () => { if (engine) { engine.dispose(); } process.exit(0);});Serverless Considerations#
Serverless environments have specific constraints that affect CE.SDK performance:
Cold Start Optimization#
- Use code splitting to defer engine loading
- Pre-warm functions that process designs frequently
- Consider provisioned concurrency for latency-sensitive workloads
Memory Limits#
- Monitor memory usage against function limits
- Configure function memory to accommodate engine requirements (minimum 512MB recommended)
- Dispose engine promptly to avoid memory accumulation
Execution Time#
- Set appropriate function timeouts for export operations
- Configure export inactivity timeouts below function timeout
- Stream large exports instead of buffering in memory
Troubleshooting#
High Memory Usage#
Monitor memory with getUsedMemory() and getAvailableMemory(). If memory accumulates across requests:
- Ensure
dispose()is called after each request in per-request patterns - Clear scenes between requests in shared engine patterns
- Consider periodic engine recreation in long-running processes
Slow Cold Starts#
Implement code splitting to defer engine loading. For latency-sensitive workloads:
- Use provisioned concurrency in serverless environments
- Implement keep-alive requests to prevent function cold starts
- Pre-load the engine during process initialization
Export Timeouts#
Increase timeout using unstable_setExportInactivityTimeout() for images or unstable_setVideoExportInactivityTimeout() for videos. For persistent issues:
- Reduce export resolution
- Simplify scene complexity
- Increase server CPU resources
Assets Download Again Despite the Cache#
If the request count from the verification example doesn’t drop to zero on the second render, check the responses and the setup:
- The response has
Cache-Control: no-store,no-cache, ormax-age=0.undicirequests these from the server every time. - The response has
Cache-Control: privateor sets a cookie. The cache stores it only when you passtype: 'private'tointerceptors.cache(). - The cached response is older than the lifetime its headers allow.
undicithen checks with the server before it reuses the response. - The response is a partial response (status 206).
undicinever caches these. - The response is larger than the
maxEntrySizeofMemoryCacheStore, 5 MB by default. - The cache uses
MemoryCacheStoreand the process restarted. - The asset host is missing from the
originsoption. - The dispatcher comes from
undici7.26 or earlier and the server runs Node.js 26.
Memory Leaks#
If memory grows unbounded:
- Verify
dispose()is called in all code paths, including error handlers - Check for stored references to engine blocks
- Implement periodic engine recreation as a safety measure
API Reference#
| Method | Description |
|---|---|
CreativeEngine.init() |
Initialize a new engine instance |
engine.dispose() |
Clean up engine resources |
engine.editor.getUsedMemory() |
Get current memory usage in bytes |
engine.editor.getAvailableMemory() |
Get available memory in bytes |
engine.editor.getMaxExportSize() |
Get maximum export dimension in pixels |
engine.unstable_setExportInactivityTimeout() |
Configure image export timeout |
engine.unstable_setVideoExportInactivityTimeout() |
Configure video export timeout |
Next Steps#
- Architecture - Understand CE.SDK structure and components
- Headless Mode - Run the engine without UI for automation
- Export Overview - Learn about export formats and options