Since v2.0 the SDK renders templates in-process. Template JSON is fetched from the Maildeno server once, cached locally, and rendered with the embedded engine on every subsequent call. Your merge tags and visibility context never leave your server.
This page explains the caching model and how to configure, inspect, and manage it.
How caching works
When you call any render method, the SDK walks three tiers before it touches the network:
client.renderHtml(templateId)
│
├─ 1. In-process memory cache ───── HIT → render immediately (0 ms, pure CPU)
│ │ miss
│ ▼
├─ 2. Disk cache (if type="disk") ─ HIT → load into memory, then render
│ │ miss
│ ▼
├─ 3. GET /v1/sdk/template/{id} ─── network call (first render, or after TTL expiry)
│ │ store in memory (and on disk, if configured)
│ ▼
└─ 4. Render in-process with the embedded engine
merge tags & context are applied locally and never sent to Maildeno
-
The network call to Maildeno happens at most once per template per TTL window.
-
After the first fetch, every subsequent render is pure CPU — no I/O, no added latency.
Configuration
All cache settings live under a single cache object on the constructor config. Omit it to use memory caching with default settings.
import { MaildenoClient } from "maildeno"
const client = new MaildenoClient({
apiKey: process.env.MAILDENO_API_KEY!,
cache: {
type: "memory", // "memory" (default) or "disk"
path: "/var/cache/maildeno", // required when type is "disk"
ttl: 300_000, // freshness window in ms (default 5 min)
maxEntries: 50, // evict oldest beyond this (default 50)
},
})
| Key | Type | Default | Description |
|---|---|---|---|
|
|
|
Caching strategy. Memory lives in the process heap; disk persists to the filesystem. |
|
|
|
Directory for disk-cached entries. Only used when |
|
|
|
How long (milliseconds) a cached template is considered fresh. After expiry the SDK attempts a re-fetch. |
|
|
|
Maximum number of templates held before the oldest is evicted. |
The cache is attached to the client instance. Instantiate the client once at module level and reuse it so the cache stays warm across requests.
|
The CacheConfig type is exported from the package root if you want to build the config object separately:
import { MaildenoClient } from "maildeno"
import type { CacheConfig } from "maildeno"
const cache: CacheConfig = { type: "disk", path: "/var/cache/maildeno", ttl: 600_000 }
const client = new MaildenoClient({ apiKey: process.env.MAILDENO_API_KEY!, cache })
Memory cache (default)
Zero configuration required. The cache lives in the process heap.
// Defaults — no cache config needed
const client = new MaildenoClient({ apiKey: process.env.MAILDENO_API_KEY! })
// Explicit memory config with a custom TTL
const tuned = new MaildenoClient({
apiKey: process.env.MAILDENO_API_KEY!,
cache: { type: "memory", ttl: 60_000, maxEntries: 100 },
})
When to use: almost always — unless your process restarts frequently (serverless, edge) and you want the cache to survive cold starts.
Trade-off: the cache is lost on process exit, and each process maintains its own independent copy.
Disk cache
Persists template JSON to the local filesystem. Disk-cached entries survive process restarts and are shared across workers on the same filesystem.
const client = new MaildenoClient({
apiKey: process.env.MAILDENO_API_KEY!,
cache: {
type: "disk",
path: "/var/cache/maildeno", // defaults to ".maildeno-cache"
ttl: 600_000, // 10 min
maxEntries: 200,
},
})
When to use:
-
Serverless or edge deployments where cold-start latency from a network fetch adds unacceptable overhead.
-
Multi-process deployments sharing a filesystem, so workers reuse one fetched copy.
-
Long-lived workers where you want cache persistence across restarts.
The disk strategy requires a writable filesystem. On runtimes with no persistent disk (most edge platforms and some serverless tiers), use the default memory strategy.
|
Stale-on-error fallback
When a cached template’s TTL expires, the SDK attempts a re-fetch. If the Maildeno server is unreachable (network error, timeout, 5xx), the SDK:
-
Renders from the last known-good cached copy (the stale entry).
-
Sets
result.fromStaleCache = trueon the returnedRenderResult. -
Does not throw — your send pipeline continues uninterrupted during Maildeno downtime.
The SDK only throws when the server is unreachable and no prior cached copy exists for that template (a first-ever render with an empty cache).
const result = await client.render({
templateId: "550e8400-e29b-41d4-a716-446655440000",
target: "html",
})
if (result.fromStaleCache) {
// The output is still valid — rendered from the last cached template.
console.warn("Rendered from stale cache — Maildeno may be unreachable", {
templateId: result.templateId,
})
}
fromStaleCache is only present on the RenderResult returned by render(). The convenience methods (renderHtml(), renderReact(), renderMjml()) resolve to the output string directly, so use render() when you need to detect a stale render.
|
Cache inspection & management
All cache-management methods are asynchronous — await them.
// List the template IDs currently held in the cache
const ids = await client.listCached()
// ["550e8400-...", "9ec0c043-..."]
// Remove a single template immediately, bypassing TTL
// (e.g. right after updating it on the dashboard)
await client.deleteCached("550e8400-e29b-41d4-a716-446655440000")
// Wipe the entire cache (memory and disk)
await client.clearCache()
A common pattern is to call deleteCached() from a webhook handler so template edits propagate to your renderers immediately instead of waiting for the TTL to expire.
invalidate(templateId) is deprecated — use deleteCached(templateId) instead. invalidate() is now async and delegates to deleteCached() internally, so existing code keeps working, but new code should call deleteCached(). clearCache() is also async as of v2.1; awaiting it is required in disk mode.
|
Choosing a strategy
| Deployment | Strategy | Why |
|---|---|---|
Long-lived Node server |
|
Simplest, fastest. Cache stays warm for the process lifetime. |
Serverless / edge |
|
Disk survives cold starts; memory is fine if cold starts are rare or no disk is available. |
Multi-process on one host |
|
All processes share one fetched copy instead of each fetching independently. |
See also
-
Rendering —
render(), convenience methods, andRenderResult. -
Installation & Setup — full constructor configuration.
-
REST API — the
GET /v1/sdk/template/{id}endpoint the cache is built on.