7 November 202610 min read • 1921 words

Why Your Next.js Page Still Shows Old Data After a Database Update

Trace a database update through tagged queries, invalidation, regeneration and the browser. Diagnose stale Next.js pages and verify cache priming.

Why Your Next.js Page Still Shows Old Data After a Database Update

You change a headline in the database, confirm the new value, and refresh the page. The old headline is still there.

The write may have worked perfectly. The page can be reading a cached query result, serving previously rendered output, or displaying a payload already held by the browser. When the update comes from a SQL editor or migration, it can also skip the application code responsible for invalidating those caches.

The fastest way to investigate is to follow one field. Find the first place where the value you expect becomes the value you see.

Consider a database-backed marketing site: Supabase RPCs supply page content, unstable_cache stores query results, and admin Route Handlers save edits. An About page headline will be our running example. The architecture follows a Next.js 16.3.5 App Router application without Cache Components enabled.

This scope matters: Next.js 16 recommends use cache with Cache Components for new implementations. The examples here explain an existing unstable_cache setup. Next.js: unstable_cache.

A committed row is only the beginning

Here is the simplified read path. Each arrow represents a dependency, not a guarantee that the preceding step runs on every request:

Committed database row
        │ read by RPC when the cached query recomputes
        ▼
Cached query result
        │ consumed when the route renders
        ▼
Rendered route output, where cached
        │ delivered through a document or navigation response
        ▼
Browser's visible content

After a write:
Application mutation ── invalidation signal ──▶ matching cache entries
Direct SQL          ── requires a separate notification ──▶ application

A cache hit can reuse an earlier result without reading the row again. Likewise, a route can render again without rerunning a cached query, and a fresh server result does not automatically replace the content in an open tab.

That is why repeated browser refreshes are a poor first diagnostic. Start with the data the page actually reads.

Find the cached read before changing the write

In the example, /about gets its content through get_about_content, a database RPC that returns the hero and other page sections. A cached function wraps that call. Its configuration includes these key parts and options:

unstable_cache(async () => get_about_content(), ['about_page_content_data'], { tags: ['about'] });

The page and its metadata both consume the cached function. The key helps identify the stored result; the about tag lets the application invalidate related data.

There is no revalidate interval in this configuration. For unstable_cache, omitting it means the result can remain cached without a time-based refresh until matching invalidation occurs. Changing the row's updated_at value does not change the wrapper's key. Tags label dependencies for invalidation; they are not cache keys. Next.js: cache parameters.

Before investigating invalidation, confirm that the text really comes from this query. A component literal will not change when you update a database row. A fallback value can create a subtler problem: if the RPC fails and the cached function returns fallback content normally, that fallback can become the cached result. A successful page response may therefore contain neither an error message nor the current database value.

Direct SQL skips your application's invalidation code

An admin save can do two jobs: commit the edit and notify Next.js that the cached content changed. In the example, a successful About hero update reaches this helper call:

revalidateAndPrime('about', '/about');

A SQL editor, migration, or external script does not execute that handler. Unless another integration sends an invalidation signal, the application can continue serving its existing cached result after the database commit.

The practical question is: which write paths also notify the cache?

Include admin forms, background jobs, imports, migrations, and external integrations in that inventory. For a writer outside the application, arrange an authenticated notification after a successful commit, covering the affected tags and the correct deployment. Make failures visible and retryable; otherwise a successful write can leave a failed notification unnoticed.

Invalidation, regeneration, and refresh are separate operations

These terms describe different work. Choosing the wrong one can make a fix appear successful while the headline stays unchanged.

Operation

What it does

What it does not establish

Tag invalidation

Marks tagged data for revalidation according to the selected policy

Every consuming page has already rebuilt

Regeneration

Recomputes data and, where applicable, rendered route output

An open browser has received the result

Browser route refresh

Requests another server payload for the current route

Server caches were invalidated

Cache priming

Requests a URL to trigger work before a later visitor arrives

The response contains the expected new content

The helper uses revalidateTag(tag, 'max'). That policy allows stale content while fresh data is fetched in the background. Revalidation is request-driven; calling the function does not eagerly rebuild every page carrying the tag. A short stale interval can be a reasonable latency tradeoff for public content. Next.js: revalidateTag.

router.refresh() requests an updated server payload while preserving unaffected client state, but it does not invalidate server caches. If the server still returns the cached headline, refreshing can bring back the same text. Next.js: useRouter.

Path invalidation is another option: revalidatePath targets a page or layout, while tags follow data dependencies across consumers. In a Route Handler, path revalidation takes effect on the next visit. Choose based on the dependency that changed rather than treating path and tag calls as interchangeable. Next.js: revalidatePath.

Also allow for failure during regeneration. An uncaught revalidation error can leave the last successful result in place. Returning fallback content instead of throwing can instead produce a successful computation containing fallback data. Inspect the query errors and returned content together. Next.js: ISR error handling.

A priming request is not a freshness check

A helper named revalidateAndPrime sounds reassuring. Its control flow still matters.

Here is the fetch statement from the example's priming utility:

fetch(url, { cache: 'no-store' }).catch(e => console.error(`[PRIME_CACHE_ERR] ${url}:`, e));

The enclosing primeCache function is async, but it neither awaits nor returns that fetch. Its caller dispatches requests with forEach without collecting their completion promises. Adding await revalidateAndPrime(...) at the mutation call site would not make this implementation wait for the network response.

Even waiting for the response would answer only part of the question:

  • Request caching: cache: 'no-store' applies to the outbound fetch. It does not disable the destination page's independent unstable_cache query.

  • Revalidation policy: with 'max', a request can receive stale content while triggering background work. An awaited response need not contain the new headline.

  • Response validation: without checking status and content, the helper cannot distinguish the expected page from an error response or old content.

  • Deployment: an origin configured for production can send a preview deployment's priming request to the wrong environment.

  • Request lifetime: detached work needs hosting-runtime support; the helper itself provides no completion guarantee.

Avoid replacing those gaps with a rule such as “the second request will be fresh.” The Next.js 16.3.5 implementation can wait for recomputation during static generation, while ordinary request revalidation can return stale data. Behavior depends on the execution path and whether regeneration succeeds.

To make priming observable, choose the target origin deliberately, await completion, inspect status and redirects, and compare the returned field or revision with the expected value. Coordinate that verification with invalidation processing, and use a bounded retry window when background revalidation is intended.

Trace one headline from the row to the screen

Consider a representative scenario: an editor updates about_hero.headline in staging, but /about still shows the previous headline. The goal is to identify the first boundary where the values diverge.

The diagnostic commands are illustrative; validate them against your staging schema before use.

1. Check the committed row and the RPC result

In an authorized read-only SQL session, compare the source row with the value returned by the page's RPC:

SELECT id, headline, updated_at
FROM public.about_hero
WHERE id = 1;

SELECT public.get_about_content() -> 'hero' ->> 'headline'
  AS rpc_headline;

If the row is old, investigate the write, transaction, or database environment. If the row is new but the RPC returns something else, inspect its source tables and filters. Invalidating a cache will not repair a query that selects the wrong record.

2. Follow the field into its consumer

In this example, the RPC selects the hero with id = 1. The page passes data.hero to AboutHero, which renders data.headline. The wrapper uses key parts about_page_content_data and tag about.

Check the same chain in your application. Confirm the column, RPC field, component prop, and fallback behavior before assuming the stored result is merely old.

3. Check whether invalidation happened after the write

Did the editor use the admin form or direct SQL? For an external write, confirm that application-side invalidation ran after commit, using about, against the deployment serving /about.

Use a dedicated authorized invalidation workflow where available. Do not assume a content-editing endpoint is a cache-only endpoint or resave unrelated fields merely to trigger its helper.

4. Observe regeneration in production-mode staging

Correlate the invalidation with requests to /about and the RPC/render logs. Check the requested host, redirects, query failures, and response content. A cache-status header can help explain a response, but it cannot confirm that the headline matches the database.

Test this behavior with a production build and server in staging. Development behavior is not a reliable substitute for checking ISR under production conditions. Next.js: verifying production behavior.

5. Compare the document, then the visible heading

Once regeneration completes, inspect a subsequent document response for the expected headline. Use a bounded retry window; if it never matches, return to the logs rather than adding an arbitrary delay.

Then load /about through a fresh browser navigation. If the document contains the new headline but an existing tab still shows the old one, investigate retained route payloads and local client state. If different deployments return different values, check cache sharing and invalidation propagation between instances.

Keep a short diagnostic record: expected headline, RPC result, deployment, tag, invalidation time, response heading, and visible heading. Those observations give you a stopping condition more useful than “I refreshed five times.”

Choose a freshness policy that fits the edit

Once the read path is correct, choose how much stale content the mutation can tolerate:

Situation

Policy or next action

Public content can tolerate a brief stale interval

Use revalidateTag(tag, 'max') and observe background revalidation

SQL, migration, or another external writer changes data

Notify an authenticated application invalidation workflow after commit

A webhook or Route Handler needs the next read to wait for fresh data

Consider revalidateTag(tag, { expire: 0 })

A Server Action needs read-your-own-writes behavior

Consider updateTag(tag); it is not available in Route Handlers

Several pages consume the same data

Map the tags and verify each relevant surface

Only an existing browser view remains old

Refresh or navigate after confirming server freshness

Immediate expiry changes how the next cached read behaves. It does not make the database commit, every cache, and every browser update atomic. Next.js: revalidateTag options, Next.js: updateTag.

Shared consumers deserve particular attention. A homepage aggregate can carry the same blogs tag as a listing and detail pages. Invalidating that tag may affect all of them, but priming /blogs verifies none of the other URLs. Likewise, a homepage preview and listing can select different description fields; a mismatch may be field selection rather than caching.

The next time a SQL edit seems invisible, resist starting with another refresh. Pick one field, compare its value at each boundary, and fix the first divergence. Then follow it through to the visible heading. That is the difference between knowing a write succeeded and knowing readers can see it.

Comments (0)

Leave a comment

No comments yet. Be the first to share your thoughts!