Oct 29, 2026
Stop caching your dashboard like a marketing page
A marketing page can be an hour old and nobody notices. A dashboard that is an hour old is wrong. Cache for freshness, not just for speed.
A dashboard bug I keep seeing is not really a bug. A user refunds an order, goes back to the sales dashboard, and the total has not moved. They refresh. Still the old number. They open a new tab, and suddenly it is correct.
Nothing failed. Nothing threw an error. The page did exactly what it was told to do: it served a cached answer.
That is the whole problem. We cache dashboards the way we cache marketing pages, and then we are surprised when the numbers lie.
A marketing page and a dashboard have opposite jobs
A marketing page is the same for everyone, and it changes rarely. Caching it aggressively is free speed.
A dashboard is personal, it changes constantly, and being stale is not a minor cosmetic issue. A stale total, a stale stock count, or a stale alert is a wrong decision waiting to happen. If the data belongs to one signed-in user, or it changes because of an action that user just took, treat freshness as the default and speed as something you earn back carefully.
That one distinction decides most of your caching strategy before you write any code.
Know which cache you are actually fighting
In a Next.js App Router dashboard, "cache" is not one thing. There are at least three layers, and they bite in different ways:
- The data cache. The result of a fetch or a database query, reused across requests.
- The full route cache. The rendered page itself, reused on the server.
- The router cache. The page the browser already visited, reused on the client when you navigate back.
That is why a hard refresh sometimes "fixes" a bug that a normal refresh does not, and why a page can look correct in a new tab but wrong in the tab you have been using. You are not looking at the same cache in each case. When a dashboard shows stale data, do not just clear everything and move on. Find out which layer served the stale answer, because that layer is the one your fix has to touch.
The rule I use now
Do not cache personal dashboard data unless you can answer two questions:
- What event makes this data stale?
- What code runs when that event happens to throw the old copy away?
If you cannot answer both, do not cache it. Fetch it fresh. A slightly slower dashboard that is right beats a fast dashboard that cannot be trusted.
When you can answer both, tag the cached data and invalidate by tag, not by guessing a time. A time-based expiry like "revalidate every hour" is a compromise: it guarantees the data will be wrong for up to an hour. A tag tied to the real event is precise. When an order is refunded, invalidate the sales tag. When stock moves, invalidate the inventory tag. The code that changes the data should be the code that clears the cache.
// After the mutation, invalidate exactly what changed.
// Do not revalidate the whole site for one refund.
revalidateTag('sales');
Path-based revalidation has its place, but only when the stale data really lives on one route. If the same sales number appears on the dashboard, the reports page, and a summary widget, a tag reaches all three. A path does not.
Do not let the slowest widget hold the whole page hostage
The second dashboard mistake is sequential and all-or-nothing loading: the page waits for the slowest chart before it shows anything at all.
Start independent fetches together instead of one after another, and stream the slow parts behind Suspense boundaries. The header, the totals, and the fast widgets should render while the heavy chart is still loading, with a skeleton in its place. The user gets something useful immediately, and the slow widget arrives when it is ready instead of blocking everything else.
import { Suspense } from 'react';
export default function DashboardPage() {
return (
<main>
<SalesTotals />
<Suspense fallback={<ChartSkeleton />}>
<SlowSalesChart />
</Suspense>
</main>
);
}
This is not a trick to make the slow query faster. It is a decision about what the user should see first.
Real-time data is not a caching problem
There is a boundary here that is easy to miss. Server rendering gives you a good first snapshot. It does not give you live data.
If a number is supposed to move while the user is looking at it — live sales, live telemetry, a queue that is draining — that is a client-side job after the first render: polling, server-sent events, a WebSocket, or a client cache like TanStack Query with a short stale time. Trying to solve live updates with a server cache will either show stale data or revalidate so aggressively that the cache is pointless.
So the shape that works for me is: server renders the first, trustworthy snapshot, and the client takes over for anything that has to keep moving. Each layer does the job it is actually good at.
A short checklist before you ship a dashboard page
- Is this data per-user? If yes, it must never be shared through a cache another user could read.
- What event makes it stale, and does that event invalidate it?
- Is the slowest widget behind a Suspense boundary, or is it blocking the whole page?
- Is anything on this page supposed to update live? If so, is that handled on the client, not by hoping the server cache refreshes in time?
Caching is not a speed trick you bolt on at the end. It is a freshness decision you make for every piece of data, and on a dashboard, freshness is the feature. Get that decision right, and the page does not just load faster — it stops lying to the people who rely on it.