nextjsvercelcachingdeploymentshilpiworks

The Vercel Edge Cache Trap: Why force-dynamic Doesn't Always Work

Arun Batchu, Cascade (AI)·February 23, 2026·5 min read
Share

We have autonomous AI agents publishing new stickers to Shilpiworks around the clock. But customers visiting the /stickers page weren't seeing them. We'd redeploy, wait, refresh — still the same stale catalog from days ago.

A quick curl diagnostic told us everything:

curl -sI https://shilpiworks.com/stickers | grep -E 'x-vercel-cache|age'
# x-vercel-cache: HIT
# age: 732000  # ~8.5 days stale

The page was being served from Vercel's edge cache — 8.5 days old. Our agents were publishing correctly. The database had the new products. But customers were never seeing them.

What We Tried (That Didn't Work)

Our first instinct was to add Next.js runtime directives to the page:

// Attempt 1: runtime directive
export const dynamic = 'force-dynamic';
 
// Attempt 2: unstable API
import { unstable_noStore } from 'next/cache';
unstable_noStore();
 
// Attempt 3: empty commit to trigger rebuild
git commit --allow-empty -m "force redeploy"

None of it worked. After every redeploy, curl still showed x-vercel-cache: HIT and an age in the hundreds of thousands of seconds.

The Root Cause

The issue is a layering problem. When Next.js statically pre-renders a page in a previous deployment, Vercel's edge network caches that HTML. On subsequent requests, the edge serves the cached HTML before Next.js even runs — which means runtime directives like force-dynamic and unstable_noStore() are never reached. They're Next.js instructions, but the edge doesn't speak Next.js.

⚠️ The key insight: force-dynamic tells Next.js not to cache. But if Vercel's edge has already cached the page from a previous deployment, Next.js never gets the request. The edge wins.

A new deployment doesn't automatically purge the edge cache for previously-static pages. The edge keeps serving the old HTML until the cache TTL expires — which, for static pages, can be days.

The Fix

The fix is to set Cache-Control headers in next.config.js. Unlike runtime directives, these headers are applied at the infrastructure level — Vercel's edge reads them and respects them.

// next.config.js
async headers() {
  return [
    {
      source: '/stickers',
      headers: [
        {
          key: 'Cache-Control',
          value: 'private, no-cache, no-store, max-age=0, must-revalidate',
        },
      ],
    },
    // Repeat for other dynamic category pages
    { source: '/bookmarks', headers: [{ key: 'Cache-Control', value: 'private, no-cache, no-store, max-age=0, must-revalidate' }] },
  ];
}

After deploying this change, curl immediately showed x-vercel-cache: MISS and age: 0. New products appeared instantly.

Bonus: The www Redirect Loop

While debugging, we also discovered a related trap. Our Vercel project has www.shilpiworks.com configured as the primary domain, which means Vercel automatically redirects shilpiworks.comwww.shilpiworks.com at the infrastructure level.

We made the mistake of also adding a redirect in next.config.js pointing www → apex. The result: an infinite redirect loop that took down the site. The fix was to remove the code-level redirect entirely and manage the canonical domain exclusively in the Vercel Dashboard under Settings → Domains.

Rule: Never fight your infrastructure in code. If Vercel owns the redirect, let Vercel own it. Adding a code-level redirect in the opposite direction creates a loop.

The Debugging Checklist

  1. Check x-vercel-cache header — should be MISS for dynamic pages.
  2. Check age header — should be 0 or very low.
  3. If HIT with high age: the fix is Cache-Control headers in next.config.js, not runtime directives.
  4. Verify the build succeeded in Vercel Dashboard before assuming it's a cache issue.
  5. Never add host-level redirects in code if your infrastructure already handles them.

The mental model that unlocks this: Next.js and Vercel's edge are two different layers. Next.js runtime directives only work if Next.js actually handles the request. next.config.js headers work because they're compiled into the infrastructure config at build time. Know which layer owns your cache. See the live result at shilpiworks.com/stickers →

Found this useful? Share it.
Share

About the author

If this resonated, reach out. Here's how to continue the conversation.

Arun Batchu

Arun Batchu

Founder & Principal Advisor

I can help you separate AI hype from real operating advantage — and design experiments that build evidence faster than opinions do.