Bhavana's Week 11: Rendering isn't Caching

A Humanitarians AI Fellow discovers her Next.js caching config had never worked, and traces the bug to a single silent cookie read three layers deep.

3:17 video3 min readWatch on YouTube

A config file can say a page revalidates every hour and still never once get cached. That is the gap this week's update closes: a caching setting that looked correct on paper, and the discovery, only after checking the actual HTTP response, that it had never been doing what it claimed.

The trap: rendering correctly is not the same as caching correctly

Back in week four, a caching setting had been chosen for a set of job listing pages, with a flag noting that choosing a setting and verifying it works are two different things. This week's check confirmed the pages had never been cached at all. Every earlier check had confirmed the pages rendered correctly, and they did. But rendering correctly and being cached correctly are separate claims, and only one of them had ever actually been verified. A config line reading "revalidate every hour" is a stated intention. It is not proof that a single response was ever cached.

Reading the real headers

To settle the question, the actual HTTP response headers got pulled and compared directly against the config. On one side sat the config set back in week four: revalidate equals 3600, rebuild the cache every hour. On the other side sat what the wire actually returned: Cache-Control set to no-cache, no-store, and no X-Nextjs-Cache header present at all. The pages were configured to cache and had never been cached, which is exactly the kind of gap that a glance at the config file alone would never reveal.

The culprit: a silent cookie read

Tracing the cause led somewhere genuinely sneaky. The data-fetching client these pages used read cookies internally, and in Next.js, touching cookies anywhere in a route's render path silently forces the entire route to be treated as fully dynamic, overriding the revalidate setting. There is no warning and no error. The caching directive simply gets ignored, because of a cookie read three layers down in a client the page itself never appeared to touch directly.

The fix and what it uncovered

The fix matched the actual problem: these landing pages are read-only and public, so they never needed per-request cookie access in the first place. Switching them to a plain cookie-less client was enough to let Next.js finally honor ISR, confirmed the same way the bug was found, by checking that the real header now read X-Nextjs-Cache: HIT. Turning that same skepticism on the rest of the backend surfaced three more gaps worth closing: no graceful handling if Google's credentials were not yet set, which would crash instead of skipping cleanly; no chunking on database lookups checking submission history, fine at current scale but fragile as it grows; and no row-level security on a new tracking table, out of step with every other table in the schema. All three got fixed, and the architecture doc, which had still called the feature "templates pending" weeks after it went live, was brought back in sync with reality.

Key takeaways

  • A config setting like revalidate is a stated intention, not proof that caching is actually happening; only the response headers prove it.
  • Check Cache-Control and X-Nextjs-Cache directly against the wire response rather than trusting the config file alone.
  • In Next.js, reading cookies anywhere in a route's render path silently forces that route fully dynamic, overriding revalidate with no warning.
  • Read-only, public pages that do not need per-request state should use a cookie-less data client to avoid this silent override.
  • Applying the same skepticism to the rest of a system, not just the bug you were chasing, can surface real gaps: missing credential handling, unchunked queries, missing row-level security.

Try it yourself

Pick a config value you have trusted but never actually verified, a cache setting, a timeout, a feature flag, and ask Claude how to observe its real effect from outside the app: the actual header, the actual timing, the actual behavior. Then ask what invisible thing upstream could be silently overriding it, the way one cookie read overrode this ISR config. This kind of verification work is a regular part of the Humanitarians AI Fellows program.

Chapters

  1. 0:00The Dangling Thread: Verifying the Cache
  2. 0:25Checking Headers: Hit vs. No-Cache
  3. 0:50The Culprit: Silent Cookie Reads
  4. 1:15The Fix: Implementing a Cookieless Client
  5. 1:45Recap: Check the Output, Not the Setting
Full transcript(auto-generated, with timestamps)

The Dangling Thread: Verifying the Cache

[0:00]Hi, I'm Bavana, a humanitarian's AI fellow. Week 11 of my job seeker report, where I'm the SEO engineer making Google find and rank our job listings. Back in week four, I chose a caching setting, but flagged that choosing it and it working were different things. This week I checked, and the pages had never been cached at all. Start with the trap. Every earlier check confirmed the pages rendered correctly, and they did.

Checking Headers: Hit vs. No-Cache

[0:27]But rendering correctly and being cached correctly are two different claims, and only one of them had ever actually been verified. A config line that says revalidate every hour is an intention. It is not proof that a single response was ever cached. Rendering correctly and being cached correctly are two different claims, and only one had ever been checked. So I pulled the real HTTP

The Culprit: Silent Cookie Reads

[0:50]Headers, both sides on screen. On the left, the config set since week four, revalidate equals 3600, rebuild the cache every hour. On the right, what the wire actually returned. Cache control, no cache, no store, and no X-Next-Cache header at all. Configured to cache, never cached. Tracing why led to something genuinely sneaky. The data fetching client these pages used reads cookies internally. And

The Fix: Implementing a Cookieless Client

[1:16]In Next JS, touching cookies anywhere in a route's render path silently forces the whole route to be treated as fully dynamic, overriding revalidate. No warning. No error. Just a caching directive quietly ignored because of a cookie read three layers down. The fix fit the problem. These landing pages are read-only and public. They don't need per request off, so they never needed cookies. Switching them to a plain cookie-less client was enough for Next

Recap: Check the Output, Not the Setting

[1:45]To finally honor ISR. And it was confirmed the same way I found the bug. The real header now reads X-Next-Cache hit. The same skepticism turned on the back end found three more gaps worth closing. No graceful handling if Google's credentials aren't set yet. It would crash instead of skipping cleanly. No chunking on the database lookups that check submission history. Fine now, fragile at scale. And no row-level security on the new tracking table, out of step with every other table in the schema. All three fixed. And the architecture doc brought back in sync. It's still called templates pending that had been live for weeks. The recap. Week four's dangling thread resolved. Revalidate said cached, but the pages never were. Rendering correctly

Isn't caching correctly, and only the header proves it. The cause was a cookie read forcing the route dynamic. The fix, a cookie-less client confirmed by X-Next-Cache-HIT. And a skeptical back end pass closed three more gaps and resynced the doc. The rule this week, a config value is a claim. Go check the output. Your turn. Find a config value you've trusted but never verified. A cache setting, a timeout, a feature flag. Ask Claude how to observe its real effect from outside the app. The actual header, the actual timing, the actual behavior. Then ask what invisible thing upstream could be overriding it silently, the way one cookie read overrode revalidate. Run this and check the output, not the setting. Rendering isn't caching.

More from Humanitarians AI Fellows

Humanitarians AI Lyrical Literacy Project