Template JSON is fetched from the Maildeno server once and cached locally; rendering is a separate step that happens on every call. This page explains the caching model, how it interacts with the render step, and how to configure, inspect, and manage it.
How caching works
$client->renderHtml($templateId)
│
├─ 1. In-process memory cache (MemoryStore) ── HIT → skip to render
│ │ 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 via a fresh `maildeno-engine` subprocess (symfony/process)
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 — same as the other SDKs.
-
Rendering itself does not benefit from the cache the way it does in the JS/Python SDKs. Those load a Wasm module once and reuse it in-process for every render;
maildeno-engineis a separate binary spawned fresh viasymfony/processon everyrender()/renderHtml()/ etc. call, cache hit or not. The cache saves you the network round-trip for template JSON, not the render step. -
This is a fairly cheap process spawn (there’s no compiled-module state to amortize), but if you’re rendering in a tight loop it’s worth knowing there’s a subprocess per call rather than an in-memory render.
Configuration
Pass a cache array to the constructor. Omit it entirely to use memory caching with default settings.
use Maildeno\MaildenoClient;
$client = new MaildenoClient([
'apiKey' => getenv('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. Required 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. |
Memory cache (default)
Zero configuration required.
// Defaults — no cache config needed
$client = new MaildenoClient(['apiKey' => getenv('MAILDENO_API_KEY')]);
// Explicit memory config with a custom TTL
$client = new MaildenoClient([
'apiKey' => getenv('MAILDENO_API_KEY'),
'cache' => ['type' => 'memory', 'ttl' => 60_000, 'maxEntries' => 100],
]);
Trade-off: the cache lives only as long as the MaildenoClient instance does. In a classic php-fpm request/response cycle, that’s the length of a single request — see Environments & Frameworks for when this matters.
Disk cache
DiskStore persists each template as one atomic JSON file per template ID, so entries survive process restarts and are shared by anything pointed at the same directory.
$client = new MaildenoClient([
'apiKey' => getenv('MAILDENO_API_KEY'),
'cache' => [
'type' => 'disk',
'path' => '/var/cache/maildeno',
'ttl' => 600_000, // 10 min
],
]);
When to use: classic php-fpm deployments (each request starts from scratch — a memory cache built inside one request is gone before the next request begins, so disk is what actually gives you cross-request caching), multiple php-fpm workers/servers sharing a filesystem, or anywhere you want the cache to survive a restart.
Stale-on-error fallback
When the TTL has expired and the API is unreachable (network failure or a non-2xx response), the SDK renders from the last known-good cached copy and sets fromStaleCache to true on the returned RenderResult — your render keeps working during a Maildeno outage.
$result = $client->render([
'templateId' => '550e8400-e29b-41d4-a716-446655440000',
'target' => 'html',
]);
if ($result->fromStaleCache) {
// Output is still valid — rendered from the last cached template.
error_log('Rendered from stale cache — Maildeno may be unreachable');
}
If there’s no cached copy at all (e.g. a first-ever render with an empty cache and the API unreachable), the SDK raises the underlying MaildenoError instead. See Error Handling.
Cache inspection & management
// List the template IDs currently held in the cache
$ids = $client->listCached();
// ['550e8400-...', '9ec0c043-...']
// Remove a single template immediately, bypassing TTL
// (e.g. right after updating it on the dashboard)
$client->deleteCached('550e8400-e29b-41d4-a716-446655440000');
// Wipe the entire cache (memory and disk)
$client->clearCache();
A common pattern is to call deleteCached() from a webhook handler so template edits propagate immediately instead of waiting for the TTL to expire.
invalidate($templateId) is a deprecated alias for deleteCached($templateId). It still works but will be removed in a future major version.
|
Choosing a strategy
| Deployment | Strategy | Why |
|---|---|---|
Classic php-fpm (nginx/Apache + php-fpm workers) |
|
Each request tears down its object graph at the end — an in-memory cache built inside the request is gone before the next one starts. Disk is what actually persists across requests. |
Laravel Octane, RoadRunner, Swoole-based runtimes |
|
The application stays booted between requests, so a singleton-held client behaves like the long-running JS/Python server case. |
CLI scripts, queue workers, Artisan/Symfony console commands |
|
The process lives for the duration of the job — no restart to survive. |
Multiple php-fpm workers/servers sharing a filesystem |
|
All workers read the same fetched copy instead of each fetching independently. |
See also
-
Rendering —
render(), convenience methods, andRenderResult. -
Installation & Setup — full constructor configuration and native binary setup.
-
REST API — the
GET /v1/sdk/template/{id}endpoint the cache is built on.