n4nAI

Cloudflare Workers KV for caching LLM API responses

Guide to using Cloudflare Workers KV to cache LLM responses at the edge, with runnable worker code, TTL strategies, and invalidation patterns.

n4n Team3 min read704 words

Audio narration

Coming soon — every post will get a voice note here.

Cloudflare Workers KV cache LLM responses is a pragmatic pattern for cutting repeat inference cost and latency when you serve the same prompt repeatedly. This guide walks through a production-grade worker that sits in front of an OpenAI-compatible endpoint and caches completions in Workers KV. You should already have a Cloudflare account, Node 18+, and wrangler installed.

Step 1: Create the KV namespace and scaffold the worker

Workers KV is a global, eventually consistent key-value store. Reads in the edge location are typically single-digit milliseconds after first warm-up, which makes it a good fit for read-heavy LLM traffic where the same prompt appears many times. It is not a substitute for Durable Objects when you need strong consistency or per-key coordination.

Create the namespace and a module-based worker:

npm install -g wrangler
wrangler kv namespace create LLM_CACHE
wrangler init llm-cache-worker --type javascript
cd llm-cache-worker

The kv namespace create command prints an ID. Bind it in wrangler.toml:

name = "llm-cache-worker"
main = "src/index.js"
compatibility_date = "2024-09-01"

[[kv_namespaces]]
binding = "LLM_CACHE"
id = "<namespace-id-from-output>"

Install the workers types if you plan to write TypeScript:

npm install -D @cloudflare/workers-types

Step 2: Derive a stable cache key from the request

The cache key must capture every input that influences the model output. If you omit temperature, top_p, or the system prompt, you will serve semantically wrong completions. Hash the normalized request body with SHA-256.

interface ChatBody {
  model?: string;
  messages: { role: string; content: string }[];
  temperature?: number;
  top_p?: number;
}

async function cacheKey(body: ChatBody): Promise<string> {
  const normalized = {
    model: body.model ?? "gpt-4o-mini",
    messages: body.messages,
    temperature: body.temperature ?? 1,
    top_p: body.top_p ?? 1,
  };
  const encoded = new TextEncoder().encode(JSON.stringify(normalized));
  const digest = await crypto.subtle.digest("SHA-256", encoded);
  const hex = [...new Uint8Array(digest)]
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
  return `llm:${hex}`;
}

Do not embed user identifiers or raw auth tokens in the key. The hash is one-way, but the plaintext body should stay in the value, not the key, to avoid KV key size limits (max 512 bytes).

Step 3: Read from KV before calling the model

Short-circuit the request when the cache has a fresh entry. Return a header so you can observe hit ratio in analytics.

export default {
  async fetch(req: Request, env: { LLM_CACHE: KVNamespace }): Promise<Response> {
    if (req.method !== "POST") return fetch(req);
    const url = new URL(req.url);
    if (!url.pathname.endsWith("/chat/completions")) return fetch(req);

    const body = (await req.json()) as ChatBody;
    const key = await cacheKey(body);

    const cached = await env.LLM_CACHE.get(key, "json");
    if (cached) {
      return new Response(JSON.stringify(cached), {
        headers: { "content-type": "application/json", "x-cache": "HIT" },
      });
    }

    // cache miss: forward below

If the stored JSON is corrupted, get with "json" returns null, so the miss path naturally recovers.

Step 4: Forward to the LLM endpoint and write the response

Call your upstream. This can be OpenAI, an open-weight server, or a gateway. Only cache successful, non-streaming completions.

    const upstream = "https://api.openai.com/v1/chat/completions";
    const apiRes = await fetch(upstream, {
      method: "POST",
      headers: {
        "content-type": "application/json",
        authorization: `Bearer ${env.LLM_API_KEY}`,
      },
      body: JSON.stringify(body),
    });

    const resJson = await apiRes.json();
    if (apiRes.ok && !body.stream) {
      // 1 hour default; tune by prompt volatility
      await env.LLM_CACHE.put(key, JSON.stringify(resJson), {
        expirationTtl: 3600,
      });
    }

    return new Response(JSON.stringify(resJson), {
      headers: { "content-type": "application/json", "x-cache": "MISS" },
    });
  },
};

If you route through a gateway such as n4n.ai, it provides an OpenAI-compatible endpoint covering 240+ models and automatic fallback when a provider is degraded. The worker code stays identical; you just change upstream and the API key.

Streaming caveat

Streaming responses (stream: true) cannot be cached by a simple put after the fact without buffering the whole SSE stream. Either buffer in a TransformStream and then cache, or skip caching for streaming requests entirely. Most high-repeat prompts (classifiers, extractors) do not need streaming.

Step 5: Respect provider cache hints and invalidate

Some providers return cache-control headers indicating freshness. Map that to the KV expirationTtl instead of hard-coding 3600.

    let ttl = 3600;
    const cc = apiRes.headers.get("cache-control");
    if (cc && cc.includes("max-age=")) {
      const m = cc.match(/max-age=(\d+)/);
      if (m) ttl = Math.min(parseInt(m[1], 10), 86400);
    }
    await env.LLM_CACHE.put(key, JSON.stringify(resJson), { expirationTtl: ttl });

A gateway like n4n.ai forwards provider cache-control hints, so the same parsing logic works without vendor-specific code.

Manual invalidation

When you ship a new prompt template or model version, purge affected keys. If you prefix keys with a deploy ID, bulk purge is trivial:

wrangler kv key delete --binding=LLM_CACHE "llm:<hash>"

For bulk ops, list by prefix and delete in a loop, or bump a global prefix (e.g., v2:llm:<hash>) to effectively expire everything.

Step 6: Deploy and verify success

Run locally first to catch type errors:

wrangler dev --local

Then deploy:

wrangler deploy

Verify the cache works with two identical calls:

curl -s -D - -X POST https://llm-cache-worker.<subdomain>.workers.dev/chat/completions \
  -H "content-type: application/json" \
  -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"What is 2+2?"}]}' \
  -o /dev/null | grep x-cache
# first: x-cache: MISS

curl -s -D - -X POST https://llm-cache-worker.<subdomain>.workers.dev/chat/completions \
  -H "content-type: application/json" \
  -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"What is 2+2?"}]}' \
  -o /dev/null | grep x-cache
# second: x-cache: HIT

Confirm the key landed in KV:

wrangler kv key list --binding=LLM_CACHE | head

Cache stampede mitigation

A sudden burst of the same miss can trigger many parallel upstream calls. In Workers, you can use a coordinating Durable Object to serialize the first fetch, but a simpler approach is to accept occasional duplicates for low-QPS workloads and keep TTLs short. For high traffic, cache the empty pending promise in a module-scoped Map with a timeout.

Security notes

Never cache responses that contain user-private data unless the key is scoped per user and TTL is minimal. Store the API key as a secret, not in code:

wrangler secret put LLM_API_KEY

Cloudflare Workers KV cache LLM responses reduces spend on repeated prompts, but treat it as a latency and cost optimization, not a source of truth. Pair it with per-token metering on your upstream to validate the savings.

Step 7: Tune TTL by request class

Not all prompts are equally stable. Route different TTLs by a custom header or by model:

function ttlFor(body: ChatBody): number {
  if (body.model?.includes("vision")) return 300;
  if (body.temperature === 0) return 86400;
  return 1800;
}

Apply it in the put call. This keeps deterministic workloads cached longer while volatile creative generation refreshes often.

The pattern above is end-to-end: scaffold, key hashing, read-through, write-back, hint-aware TTL, and verification. Adjust the upstream and TTL policy to your traffic shape, and you will see fewer repeat charges on your LLM bill.

Tagscloudflare-workerskv-storagecachingllm-api

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All cloudflare workers llm integration posts →