A brownfield checkout SPA served yesterday's cart to paying users. Support tickets piled up before the noon standup. The spike started after a quiet overnight restart.
A coding agent had inserted an in-memory Map during a refactor. The free process restarted and dropped every cached cart. The cache key also omitted the authenticated user id.
This piece is a do-not-use field guide for that pattern. It lists red flags, a contract test, and exit criteria. Free model access stays on the probe path only.
The failure pattern
Agents reach for caches because latency demos well in diffs. Caches also hide stale reads and missing tenancy. Best-effort compute makes the lie cheaper to ship.
Process memory dies with the process. Sticky sessions are a rumor on free hosts. A restart then looks like a cache hit with empty maps.
The same sequence shows up in brownfield SPAs:
- The agent adds a
Map, LRU, or "tiny Redis." - The key is path-only or body-hash-only.
- Writes never bust the related read keys.
- No TTL, no tenant, and no request id exist.
- Staging never restarts the Node process.
- User JSON still allows public CDN caching.
That mix is how yesterday's cart reaches today's session.
Red flags: do not ship this cache
Stop the merge when any item below is true. Treat the agent's plan as a draft, not a spec.
- The key ignores user, tenant, or auth scope.
- The store is process memory on a free server.
- Invalidation is deferred to a later unnamed ticket.
- The agent invented shared Redis without an owner.
- No
ETagorCache-Controlappears in the plan. - The SPA mixes CDN HTML with private user JSON.
- Write-then-read replay tests are missing from CI.
- A best-effort free model designed the storage choice.
Item 8 needs a hard boundary. Free model access is useful for generating tests. It is a poor architect of production caches.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode's free model access and free server option can host the harness below. They should not host the cart cache itself.
Artifact: an invalidation contract test
The following test is a labeled proposal. Teams must rename routes and claims. It is not production cache code.
The contract is intentionally small:
- Every cache key includes tenant and user.
- A write deletes matching read keys.
- A process restart yields a miss, not stale JSON.
- User JSON sends
Cache-Control: private, no-store.
// proposal: cache_contract.test.mjs — unexecuted until you run it
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
function key({ tenant, user, route, vary }) {
if (!tenant || !user) {
throw new Error("refuse key without tenant and user");
}
const body = JSON.stringify({ tenant, user, route, vary });
return createHash("sha256").update(body).digest("hex");
}
class EphemeralStore {
constructor() {
this.map = new Map();
}
get(k) {
const row = this.map.get(k);
if (!row) return null;
if (Date.now() > row.exp) {
this.map.delete(k);
return null;
}
return row.value;
}
set(k, value, ttlMs) {
this.map.set(k, { value, exp: Date.now() + ttlMs });
}
bust(pred) {
for (const k of [...this.map.keys()]) {
if (pred(k)) this.map.delete(k);
}
}
crash() {
this.map = new Map(); // models a free-server process death
}
}
const store = new EphemeralStore();
const tenant = "acme";
const user = "u-42";
const route = "GET /api/cart";
const k = key({ tenant, user, route, vary: "v1" });
store.set(k, { items: [1] }, 60_000);
assert.deepEqual(store.get(k), { items: [1] });
store.bust(() => true); // write path must bust
assert.equal(store.get(k), null);
store.set(k, { items: [1] }, 60_000);
store.crash();
assert.equal(store.get(k), null);
assert.throws(() => key({ tenant: "", user, route, vary: "v1" }));
console.log("cache contract: green");
Run the file with a local Node test runner:
node --test cache_contract.test.mjs
A green run is not permission to ship. It is the minimum merge gate. Any failure means the agent cache plan is rejected.
Header probe for the SPA mismatch
Cache bugs in brownfield SPAs often live in headers. Agents forget private on JSON. CDNs then freeze a cart for the wrong shopper.
Use this labeled probe against a staging origin. Replace the host before running it.
# proposal: header_probe.sh — point at staging only
set -euo pipefail
URL="${CART_URL:?set CART_URL to a staging cart endpoint}"
HDR=$(curl -sS -D - -o /tmp/cart.json -H "Authorization: Bearer ${TOKEN:?}" "$URL")
echo "$HDR" | grep -i '^cache-control:' || {
echo "fail: missing Cache-Control on user JSON"
exit 1
}
echo "$HDR" | grep -Ei 'cache-control:.*private' || {
echo "fail: user JSON is not private"
exit 1
}
echo "$HDR" | grep -Ei 'cache-control:.*(no-store|no-cache)' || {
echo "fail: user JSON is cacheable by intermediaries"
exit 1
}
echo "header probe: green"
Fail the probe, fail the merge. Do not negotiate with the agent on this point.
Decision table
Use this table in review. Any fail in the first five rows blocks merge.
| Check | Pass means | Fail means |
|---|---|---|
| Key has tenant and user | Scoped reads | Cross-user leaks |
| Store survives process death | Disk or managed cache | Do not use memory |
| Write busts related keys | Fresh cart | Yesterday's cart |
User JSON is private, no-store
|
No CDN mix | SPA header mismatch |
| Named on-call owner | Operable | Agent-owned ghost |
| Free model designed the plan | Treat as draft | Do not treat as spec |
| Free server hosts the store | Probe only | Exit immediately |
Print the table in the pull request. Require a human initials column. Agents should not tick their own boxes.
Better alternatives
Do not invent a cache because the agent suggested one. Prefer work that does not create a second source of truth.
Preferred order:
- Fix the query and add a real index.
- Send explicit
Cache-Controlon each class of route. - Use a per-user store with TTL and a bust rule.
- Move to a managed cache with IAM and key prefixes.
- Last resort: in-process memory, never for auth JSON.
Keep free model access on the sideline for this work. Use it to draft the test file, not the topology. The free server option is a scratch host for the contract test. Checkout sessions do not belong there.
A minimal header policy for user JSON looks like this:
Cache-Control: private, no-store
Vary: Authorization, Cookie
Public marketing pages may still use a long TTL. Mixing those two classes in one agent prompt is a red flag by itself.
Exit criteria
Leave the agent cache design when any signal fires. Do not wait for a second incident.
- Cross-user data appears in a single response.
- A process restart still returns "cached" bodies.
- Support can paste a stale cart screenshot.
- The agent cannot name the bust rule in review.
- Nobody owns the eviction policy on the on-call roster.
- Staging and production disagree on TTL by more than noise.
Exit actions, in this order:
- Feature-flag the cache to off.
- Force
Cache-Control: private, no-storeon user routes. - Delete the agent-added store in the same change.
- Re-run the contract test against production-like restarts.
- Only then consider a managed cache with a human owner.
Who should not use this approach
This field guide is for brownfield HTTP APIs with private JSON. It is not for hashed static assets. It is not for immutable build artifacts on a CDN.
Do not copy the in-memory example into a multi-node production fleet. The crash() helper models one process only. Real clusters lose nodes in parts and keep serving stale peers.
Teams without a review gate should not "just try" agent caches. The cost is silent data mix-ups across shoppers. That cost beats any latency screenshot in a pull request.
Limitations
The sample does not talk to Redis or a CDN. It does not prove edge cache behavior. It does not measure hit rate or tail latency.
Free model answers can still invent keys after the test is green. Reviewers must read the key function line by line. Tests do not replace that reading.
No quota, hardware, or uptime numbers appear here on purpose. Those claims go stale within a product cycle. Verify current terms on primary vendor docs before any commit.
Closing
Agent caches fail in quiet ways. The SPA looks fast and then lies. Treat every agent cache plan as untrusted until the table is green.
Keep best-effort models and free servers in the lab. Promote the contract, not the Map. If the harness needs a disposable host, the free server option is enough for that probe and nothing hotter.
Top comments (0)