
Next.js refresh
If you've spent any time with Server Actions in the Next.js App Router, you've probably noticed that the UI usually just "knows" to update itself after a mutation. Submit a form that creates a post, and the post list refreshes without you writing a single line of client-side re-fetching logic. That behavior isn't magic, it's Next.js automatically refreshing the client router once a Server Action wrapped in a form submission or transition finishes. Most of the time that automatic behavior is exactly what you want, and you never think about it again.
But "most of the time" isn't "all of the time." There are situations where a Server Action needs to force that refresh itself, explicitly, from inside server-side code, rather than relying on the framework to notice a mutation happened. That's the entire reason the refresh function exists. It's a small, single-purpose API, and the official docs describe it in a few short paragraphs, but understanding why it exists and how it differs from the half-dozen other cache- and router-invalidation APIs Next.js ships is worth spending real time on, because picking the wrong one is a common source of "why isn't my UI updating" bugs.
What refresh Actually Does
refresh is imported from next/cache, and it does exactly one thing: it tells the client router to refresh the current route, re-fetching the React Server Component payload for whatever the user is currently looking at.
import { refresh } from "next/cache";
Its signature is about as minimal as an API can get:
refresh(): void
No arguments in, no return value out. You call it, and the client router re-renders the current route with fresh server data. That's the whole contract.
The critical restriction, and the one that trips people up the most, is where you're allowed to call it: only from inside a Server Action. Not a Route Handler, not a Client Component, not a plain async server-side utility function called outside the Server Action lifecycle. If you try to call refresh from a Route Handler, Next.js throws:
// app/api/posts/route.ts
import { refresh } from "next/cache";
export async function POST() {
// This will throw an error
refresh();
}
The reasoning behind that restriction becomes clearer once you understand what "refreshing the client router" actually means at a mechanical level.
The Client Router Cache, Briefly
When you navigate around a Next.js App Router application, the client keeps an in-memory cache of the React Server Component payloads for the segments you've visited. This is what makes back-and-forth navigation feel instant: instead of re-requesting and re-rendering a route from scratch, the router can often reuse what it already has.
That speed comes with a cost, though: the client can end up looking at server data that's gone stale relative to what's actually in your database. Most of the time, Next.js handles this invisibly. When a Server Action is invoked from a form submission or a useTransition-wrapped call, Next.js automatically triggers a router refresh once the action resolves, which clears the relevant part of that client cache and re-fetches fresh RSC payloads for the affected segments.
refresh exists for the cases where that automatic behavior doesn't kick in, or doesn't kick in at the right moment, and you need to trigger it yourself, explicitly, from within the action.
When You'd Actually Reach for This
Given that Next.js already refreshes automatically after most Server Action invocations, it's worth being precise about when you'd need to call refresh yourself. In practice, this comes up in a few recurring shapes:
Actions invoked outside the standard form/transition flow. If a Server Action is called in a way that doesn't go through the normal <form action={...}> or useTransition path that Next.js instruments for automatic refresh, such as being invoked programmatically from deep inside another async call chain, or triggered indirectly through a library that wraps the call in its own execution context, the automatic refresh signal can get lost. Calling refresh() explicitly at the end of the action guarantees the client router gets the message regardless of how the action was invoked.
Mutations that happen as a side effect, not the primary purpose of the action. Imagine a Server Action whose main job is to log an analytics event, but which also happens to touch data the current view depends on as a secondary effect. Because the "real" purpose of the action isn't a user-facing data mutation, it's easy for the automatic refresh path to not treat it as one. An explicit refresh() call makes the intent unambiguous.
Long-running actions that resolve after other async work. If an action kicks off background work (queuing a job, calling a third-party API, writing to a queue) and only wants to refresh the UI after confirming that work succeeded, an explicit refresh() call placed after the await for that confirmation gives you precise control over the moment the UI updates, rather than relying on the action's overall resolution timing.
"use server";
import { refresh } from "next/cache";
export async function createPost(formData: FormData) {
const title = formData.get("title");
const content = formData.get("content");
const post = await db.post.create({
data: { title, content },
});
refresh();
}
In this example, refresh() runs immediately after the database write succeeds, so the client router re-fetches the current route's RSC payload as soon as the new post genuinely exists. If the db.post.create call had failed and thrown before reaching refresh(), the client router would never be told to refresh, which is exactly the behavior you want: don't update the UI on a failed mutation.
refresh vs. Every Other Invalidation API
This is where a lot of the confusion around refresh comes from. Next.js ships several APIs that all sound like they're about "making stale data go away," and they solve genuinely different problems. Mixing them up is one of the most common mistakes I see.
refresh() re-fetches the RSC payload for the current user's current route, on the client. It does not touch any server-side cache. If ten other users are looking at the same route with stale cached data, calling refresh() in one user's Server Action does nothing for the other nine. It's scoped entirely to the client router session that triggered it.
router.refresh() (from useRouter in a Client Component) does the same thing as refresh(), but it's called from the client rather than the server. If you already have a reference to the router in a Client Component and just want to force a re-fetch after some client-triggered event, router.refresh() is the more natural choice. refresh() exists specifically for the case where you're inside a Server Action and don't have (and shouldn't need) a router reference at all.
revalidatePath() and revalidateTag() invalidate entries in the server-side data cache, based on a specific path or a cache tag, respectively. This is a shared, global invalidation: once you call revalidateTag('posts'), every user's next request for data tagged posts gets fresh data, not just the user whose action triggered the revalidation. This is the tool you reach for when the underlying data has genuinely changed and every client needs to eventually see the new version.
updateTag() is the newer, Cache-Components-era sibling of revalidateTag() that both invalidates the server cache for a tag and refreshes the current client router in one call, specifically designed for the common "I just mutated data tagged X, and I want everyone's cache invalidated and my own screen updated" case.
Here's the practical breakdown:
| Function | Where it's called from | What it invalidates | Who sees the update |
|---|---|---|---|
refresh() | Server Actions only | Nothing server-side; just the client's current route payload | Only the current user, only the current route |
router.refresh() | Client Components | Nothing server-side; just the client's current route payload | Only the current user, only the current route |
revalidatePath() | Server Actions, Route Handlers | Server data cache entries for a path | Every user, next time they request that path |
revalidateTag() | Server Actions, Route Handlers | Server data cache entries for a tag | Every user, next time they request data with that tag |
updateTag() | Server Actions | Server cache for a tag, plus refreshes the current client router | Every user (cache), immediately for the current user (UI) |
If you take away one thing from this comparison, make it this: refresh() is a client UI concern, not a data freshness concern. If your database has genuinely new data that other users need to see, you almost certainly want revalidateTag(), revalidatePath(), or updateTag() instead of, or in addition to, refresh().
A Realistic Example: Refreshing After an Out-of-Band Confirmation
Here's a slightly more involved example that shows why the timing control refresh() gives you actually matters. Imagine a Server Action that submits a support ticket to an external ticketing system, and you only want the ticket list on screen to update once the external system confirms the ticket was actually created, not the moment your own database insert happens:
"use server";
import { refresh } from "next/cache";
import { db } from "@/lib/db";
import { ticketingClient } from "@/lib/ticketing-client";
export async function submitTicket(formData: FormData) {
const subject = formData.get("subject") as string;
const body = formData.get("body") as string;
const localTicket = await db.ticket.create({
data: { subject, body, status: "pending" },
});
// Confirm with the external system before updating the UI
const confirmation = await ticketingClient.create({
subject,
body,
localId: localTicket.id,
});
await db.ticket.update({
where: { id: localTicket.id },
data: { status: "confirmed", externalId: confirmation.id },
});
// Only now do we want the ticket list on screen to reflect the new state
refresh();
}
Without the explicit refresh() call, you'd be relying on Next.js's automatic post-action refresh, which happens as soon as the action resolves, regardless of what state the data ended up in partway through. Placing refresh() at the exact point where you consider the mutation "real" gives you a level of control that the automatic behavior can't offer on its own.
Common Mistakes
Calling it from a Route Handler. This throws immediately. If you need to invalidate something from a Route Handler (for example, a webhook endpoint), you want revalidatePath() or revalidateTag(), not refresh(), since Route Handlers don't have a "current client router" to refresh in the first place.
Expecting it to invalidate the server data cache. refresh() only affects the client router's view of the current route. If the underlying fetch calls or 'use cache'-wrapped functions backing that route are still cached server-side, refreshing the client just gets you a fresh render of the same stale cached data. You need revalidateTag(), revalidatePath(), or updateTag() for that half of the problem.
Assuming it broadcasts to other users. It doesn't, and it's not supposed to. refresh() is scoped to the single client session that invoked the Server Action. If you need every connected user to see new data, you need a server-side revalidation strategy, not a client-side refresh call.
Forgetting it needs no await. Since refresh() returns void, not a Promise, there's nothing to await. Calling await refresh() won't break anything, but it's a signal that you might be assuming it works like the cache-revalidation functions, which is worth double-checking.
Key Takeaways
refresh is a narrow, single-purpose API: it refreshes the current user's client router from inside a Server Action, and it does nothing else. It doesn't touch the server cache, it doesn't notify other users, and it only works inside the Server Action execution context.
- Use
refresh()when a Server Action needs to force the current user's UI to reflect a mutation, especially when the action's control flow doesn't naturally trigger the framework's automatic post-action refresh. - Reach for
revalidatePath(),revalidateTag(), orupdateTag()instead when the underlying data has changed in a way that every user, not just the one who triggered the action, needs to eventually see. - Remember the boundary: Server Actions only. A Route Handler or Client Component calling
refresh()either throws or doesn't compile the way you expect, depending on where the mistake happens.
Used in the right spot, refresh() closes a small but real gap between "the data changed" and "the screen shows it," without reaching for a heavier cache-invalidation tool than the situation calls for.


