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-engine is a separate binary spawned fresh via symfony/process on every render() / 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

type

'memory' | 'disk'

'memory'

Caching strategy. Memory lives in the process heap; disk persists to the filesystem.

path

string

Directory for disk-cached entries. Required when type is 'disk'. Path may be absolute or relative to the current working directory.

ttl

int

300000

How long (milliseconds) a cached template is considered fresh. After expiry the SDK attempts a re-fetch.

maxEntries

int

50

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)

disk

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

memory

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

memory

The process lives for the duration of the job — no restart to survive.

Multiple php-fpm workers/servers sharing a filesystem

disk on a shared path

All workers read the same fetched copy instead of each fetching independently.

See also

  • Renderingrender(), convenience methods, and RenderResult.

  • Installation & Setup — full constructor configuration and native binary setup.

  • REST API — the GET /v1/sdk/template/{id} endpoint the cache is built on.