DEV Community

Casey Sun
Casey Sun

Posted on

When an Agent Invents a Cache, Refuse the Merge

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.

  1. The key ignores user, tenant, or auth scope.
  2. The store is process memory on a free server.
  3. Invalidation is deferred to a later unnamed ticket.
  4. The agent invented shared Redis without an owner.
  5. No ETag or Cache-Control appears in the plan.
  6. The SPA mixes CDN HTML with private user JSON.
  7. Write-then-read replay tests are missing from CI.
  8. 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");
Enter fullscreen mode Exit fullscreen mode

Run the file with a local Node test runner:

node --test cache_contract.test.mjs
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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:

  1. Fix the query and add a real index.
  2. Send explicit Cache-Control on each class of route.
  3. Use a per-user store with TTL and a bust rule.
  4. Move to a managed cache with IAM and key prefixes.
  5. 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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Feature-flag the cache to off.
  2. Force Cache-Control: private, no-store on user routes.
  3. Delete the agent-added store in the same change.
  4. Re-run the contract test against production-like restarts.
  5. 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)