Blog · 2026-08-13

TTL cache patterns with denkops.store

Cache expensive external calls with denkops.store: set a ttlSeconds, read through on a miss, namespace keys by prefix.

To cache an expensive external call, write its result to denkops.store with a ttlSeconds, and on the next request read the key back before you call out again. If it is present you skip the call; if it expired you fetch once and rewrite it. The store is a key-value store on a durable disk, so the cache survives redeploys, and TTL entries clean themselves up. Values are strings, so serialize with JSON.stringify.

How do I set a value that expires?

Pass ttlSeconds to set. That is the whole mechanism:

import denkops from "@denkopsai/sdk";

denkops.store.set("weather:London", JSON.stringify(data), { ttlSeconds: 600 });

The entry lives for ten minutes. After that, get returns null and the key drops out of list, without a cleanup job on your side. Leave ttlSeconds off and the value stays until you overwrite or delete it.

const cached = denkops.store.get("weather:London");
if (cached !== null) {
  const data = JSON.parse(cached);
}

What does a read-through cache look like?

The pattern is: check the store, return on a hit, otherwise load, write with a TTL, and return. Wrap it once and reuse it:

import denkops from "@denkopsai/sdk";

async function cached<T>(key: string, ttlSeconds: number, load: () => Promise<T>): Promise<T> {
  const hit = denkops.store.get(key);
  if (hit !== null) return JSON.parse(hit) as T;
  const fresh = await load();
  denkops.store.set(key, JSON.stringify(fresh), { ttlSeconds });
  return fresh;
}

// One slow call, then cheap for an hour.
const rates = await cached("fx:eur", 3600, () =>
  fetch("https://api.example.com/fx?base=eur").then((r) => r.json()),
);

The first request pays for the external call, and every request in the next hour reads a local string. The code versus I/O split in traces makes the win visible: a cache hit shows near-zero I/O time.

How should I name keys?

Namespace them with a prefix, because list filters by prefix and returns only live keys:

denkops.store.set("fx:eur", body, { ttlSeconds: 3600 });
denkops.store.set("fx:usd", body, { ttlSeconds: 3600 });

denkops.store.list("fx:");   // ["fx:eur", "fx:usd"], expired ones excluded

A prefix keeps one feature's keys apart from another's, so you can enumerate or reason about a single cache without walking everything. Include the inputs that change the result in the key. weather:London and weather:Paris are different entries; a shared weather key would serve stale data for one city.

Cache or source of truth?

Decide this per key, because the store holds both and they want opposite settings.

  • A cache is a copy you can lose. Always give it a ttlSeconds, and always be correct when get returns null, that is a miss, not an error. Fall back to the source.
  • A source of truth is the only copy. A job's status, an agent's persisted memory, a user preference. Do not set a TTL, or you will silently delete real data.

The failure mode is mixing them: putting durable state under a TTL, or treating a cache miss as a bug. Keep the two kinds under different prefixes so the intent is obvious at the call site.

Why does this work reliably here?

Because the app runs in an always-on process, the store stays warm between requests and a background scheduled refresh can rewrite hot keys before they expire, turning every user-facing request into a hit. The same durable disk backs it across deploys, so a redeploy does not cold-start your cache. See the docs and features for the full store surface.

FAQ

How do I make a value expire in denkops.store?

Pass `ttlSeconds` to `set`, for example `denkops.store.set(key, value, { ttlSeconds: 600 })`. After that many seconds, `get` returns `null` and the key stops appearing in `list`. Omit `ttlSeconds` for a value that should persist until you change it.

Can I store objects, or only strings?

The store holds strings. Serialize with `JSON.stringify` on write and `JSON.parse` on read. Because a miss returns `null`, always check for `null` before parsing so a cold or expired key does not throw.

Does the cache survive a redeploy?

Yes. The store lives on a durable disk, so entries persist across deploys and restarts. Cache entries with a TTL still expire on their own schedule; only entries whose TTL has passed are removed.

Ship it yourself: bunx denkops deploy or say "deploy on DenkOps" from your coding agent.

Start on DenkOps →

← A CRM connector your sales team can talk to · A finance connector that reads, carefully →