Skip to content
← Writing

Prompt Cache Segmentation for Shared AI Coding Gateways Without Cross-Tenant Leakage

28 Jun 2026 · 5 min read

  • Prompt Caching
  • Gateway Architecture
  • Multi-Tenant AI
  • Coding Agents
  • Reliability

Prompt caching is one of the easiest ways to make coding gateways feel faster. It is also one of the easiest ways to create a subtle data leak.

If multiple users, repos, or approval states share the same cache namespace, a perfectly valid cache hit can still be wrong. The model gets a stale system prefix, a mismatched repo summary, or context that belonged to someone else.

This post is about how to keep the speed win without turning a shared cache into a trust bug. I will show the cache-key boundaries, TTL choices, invalidation rules, and logging I would put in front of any shared coding gateway.

Why this matters

Prompt caches work best when the prefix is large and stable. Shared coding gateways have exactly that shape.

  • stable system prompts
  • repo instructions and tool manifests
  • repeated scaffold text for planner, editor, and verifier lanes
  • many short requests that differ only in the tail

That sounds perfect until the cache key forgets something important. The failure is usually quiet:

  • a user sees the wrong repo summary
  • an approval-gated lane reuses an unapproved prefix
  • a tenant gets lower quality because the gateway served stale instructions
  • nobody notices until behavior drifts or a review incident happens

Architecture or workflow overview

A safe shared cache has three layers: prefix freezing, identity segmentation, and invalidation ledgers.

flowchart LR
    A[Incoming request] --> B[Classify lane and tenant]
    B --> C[Build stable prefix packet]
    C --> D[Hash cache key with tenant + repo + policy version]
    D --> E{{Cache hit?}}
    E -- yes --> F[Reuse cached prefix]
    E -- no --> G[Create new prefix entry]
    F --> H[Append request tail]
    G --> H
    H --> I[Run model]
    I --> J[Log key, ttl class, and invalidation version]

The core idea is simple: only cache what should be reusable, and make the cache key prove why reuse is safe.

Implementation details

1) Freeze only the stable prefix

The most common mistake is caching too much. Do not hash the entire request blob and call it done. Split the prompt into a frozen prefix and a volatile tail.

from dataclasses import dataclass

@dataclass
class PromptPacket:
    system_prompt: str
    tool_manifest: str
    repo_rules: str
    task_tail: str

def build_prefix(packet: PromptPacket) -> str:
    return "\n\n".join([
        packet.system_prompt,
        packet.tool_manifest,
        packet.repo_rules,
    ])

def build_full_prompt(packet: PromptPacket) -> str:
    return build_prefix(packet) + "\n\n" + packet.task_tail

If the cacheable unit includes chat recency, ad hoc tool output, or user-specific notes, the hit rate may still look good while correctness quietly degrades.

2) Segment the key with trust-relevant identity

The cache key should include more than model name and prefix hash. If reuse depends on tenant, repo, or approval state, they belong in the key.

import hashlib
import json

def cache_key(*, tenant_id, repo_id, lane, approval_fingerprint, policy_version, prefix_text):
    payload = {
        "tenant": tenant_id,
        "repo": repo_id,
        "lane": lane,
        "approval": approval_fingerprint,
        "policy": policy_version,
        "prefix_sha": hashlib.sha256(prefix_text.encode()).hexdigest(),
    }
    raw = json.dumps(payload, sort_keys=True)
    return hashlib.sha256(raw.encode()).hexdigest()

I would not share one prompt cache between different tenants, unrelated repos, human-approved and auto-approved lanes, or prod and staging policy bundles.

3) Use TTL classes instead of one global expiry

Stable policy text changes slower than repo summaries. Repo summaries change slower than volatile review scaffolds. So do not give them the same TTL.

cacheClasses:
  global-policy:
    ttlSeconds: 86400
    invalidators: [policyVersion, toolManifestVersion]

  repo-instructions:
    ttlSeconds: 3600
    invalidators: [repoMapVersion, agentsFileHash]

  review-lane:
    ttlSeconds: 900
    invalidators: [approvalFingerprint, branchHead]

A single one-hour TTL sounds tidy, but it creates the worst of both worlds: some entries live too long, others churn too fast.

4) Keep an invalidation ledger, not just a delete button

When behavior goes weird, you need to know what prefix version the model actually used.

{
  "key": "93e0...d12",
  "tenant": "acme-prod",
  "repo": "billing-api",
  "lane": "review-lane",
  "policyVersion": "2026-06-28.2",
  "repoMapVersion": "7f991b4",
  "approvalFingerprint": "human-approve:db-migration",
  "ttlClass": "review-lane",
  "createdAt": "2026-06-28T11:52:14Z"
}

A ledger gives you replayability, auditability, and a sane way to explain cache-miss spikes after policy changes.

What went wrong / tradeoffs

The first bad version of this design usually over-optimizes for hit rate.

Failure mode 1: cache keys ignore approval state

This one is nasty because the text may look almost identical. The only difference is that one lane was allowed to perform a risky tool step and the other was not.

Failure mode 2: repo summaries outlive the branch head

A long TTL on repo context can make the model confidently act on code that moved an hour ago. The response still sounds coherent, which makes the bug more expensive.

Failure mode 3: global invalidation every time something changes

This avoids stale context, but it destroys the whole point of the cache. Teams swing from under-segmented to over-invalidated all the time.

Pitfall: If your cache hit rate is great but your verifier failure rate rises after merges, the cache may be preserving stale repo assumptions better than fresh ones.

Approach Upside Downside Best fit
One shared cache namespace Highest raw hit rate Cross-tenant and stale-prefix risk Never for multi-user systems
Tenant-only segmentation Easy to implement Repo and approval bleed still possible Small internal tools
Tenant + repo + lane segmentation Good safety and solid hit rate More key cardinality Most shared coding gateways
Full request hashing Very safe Misses most cache benefit Debug-only or high-risk lanes

Practical checklist or decision framework

If I were setting this up from scratch, I would do this in order:

  • split prompts into stable prefix and volatile tail
  • include tenant, repo, lane, and approval state in the key
  • create separate TTL classes for policy text, repo context, and review lanes
  • tie invalidation to concrete versions like branch head, repo map hash, and tool policy version
  • log cache-key metadata without logging sensitive prompt text
  • compare cache hit rate against verifier pass rate, not speed alone
  • keep a manual bypass for any lane that looks suspicious during incident response

References

Conclusion

Prompt caching is worth doing, especially for shared coding gateways. But the useful unit is not “same big string.” It is “same stable prefix, for the same trust boundary, with the same policy version.” Build the key like identity matters, because in multi-user agent systems it absolutely does.