[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-nextjs-weekly-updatetag-vs-revalidatetag":44,"search-suggestions":60,"quiz-article-nextjs-weekly-updatetag-vs-revalidatetag":109,"path-membership-nextjs-weekly-updatetag-vs-revalidatetag":510},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"01a1749f-1a03-723f-aaeb-63c4b88aa126","nextjs-weekly-updatetag-vs-revalidatetag","PRACTICE_QUIZ","updateTag vs revalidateTag","Seven questions on what changed in Next.js 16's cache-tag invalidation, and which call to reach for in a Server Action, a Route Handler, or a webhook.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},7,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,69,73,77,81,85,89,93,97,101,105],{"slug":62,"name":63,"articles":64},"webdev","Webdev",156,{"slug":66,"name":67,"articles":68},"javascript","Javascript",128,{"slug":70,"name":71,"articles":72},"frontend","Frontend",89,{"slug":74,"name":75,"articles":76},"tutorial","Tutorial",61,{"slug":78,"name":79,"articles":80},"css","Css",46,{"slug":82,"name":83,"articles":84},"performance","Performance",26,{"slug":86,"name":87,"articles":88},"typescript","Typescript",22,{"slug":90,"name":91,"articles":92},"react","React",19,{"slug":94,"name":95,"articles":96},"browser","Browser",15,{"slug":98,"name":99,"articles":100},"programming","Programming",14,{"slug":102,"name":103,"articles":104},"node","Node",13,{"slug":106,"name":107,"articles":108},"accessibility","Accessibility",10,{"id":110,"slug":46,"title":111,"subtitle":52,"excerpt":112,"coverUrl":113,"locale":13,"readingMinutes":100,"publishedAt":114,"viewCount":115,"likeCount":19,"commentCount":19,"author":116,"vertical":121,"topic":122,"tags":125,"_count":132,"playground":134,"body":136,"bodyMd":459,"seo":460,"translationGroupId":462,"series":463,"podcastUrl":52,"verticalId":5,"thread":501,"assessments":503,"translations":506,"quiz":508},"01a1749f-19a5-768a-82af-f3b8236ad42b","Next.js 16 updateTag vs revalidateTag: What Changed","Next.js 16 deprecated revalidateTag's single-argument call and added updateTag. What the new profile argument changes, plus a copy-paste cheat sheet.","\u002Fmedia\u002Fcovers\u002Fnextjs-weekly-updatetag-vs-revalidatetag.png","2026-10-27T06:11:02.727Z",44,{"id":117,"name":118,"username":119,"avatarUrl":52,"headline":120},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":123,"name":124},"nextjs","Nextjs",[126,127,130,131],{"slug":123,"name":124,"color":52},{"slug":128,"name":129,"color":52},"caching","Caching",{"slug":62,"name":63,"color":52},{"slug":74,"name":75,"color":52},{"assessments":133},1,{"slug":46,"title":135},"updateTag vs revalidateTag — interactive playground",{"blocks":137,"version":133},[138,142,145,150,153,162,165,168,171,174,187,190,193,199,202,205,208,211,216,219,222,225,229,232,236,239,243,246,249,252,255,258,261,264,268,271,275,278,281,285,288,292,295,298,307,310,318,321,325,328,332,335,339,342,345,348,351,354,357,387,391,394,402,405,408,411,414,417,420,423,426,429,432,435,441,444,447,450,453],{"id":139,"html":140,"type":141},"b1","\u003Cp>Your team upgrades to Next.js 16, the build passes, and nothing in the diff touches caching. A week later, publishing a blog post starts taking noticeably longer — not to save, but for the \u003Cem>next reader\u003C\u002Fem> to see it. Somewhere in a Server Action sits a line nobody touched: \u003Ccode>revalidateTag(&#39;posts&#39;)\u003C\u002Fcode>. It still runs. It just does something different now.\u003C\u002Fp>","paragraph",{"id":143,"html":144,"type":141},"b2","\u003Cp>That line used to mark the \u003Ccode>posts\u003C\u002Fcode> tag stale and move on — the next visitor got the old page instantly while Next.js refreshed it in the background. In Next.js 16, the same one-argument call is deprecated, and its \u003Cem>behavior\u003C\u002Fem> changed to match an immediate, blocking cache miss: the next request to anything tagged \u003Ccode>posts\u003C\u002Fcode> now waits for fresh data before it renders. No error, no red build — just a slower app and a cache-invalidation story that quietly inverted.\u003C\u002Fp>",{"id":146,"html":147,"text":148,"type":149,"level":31},"b3","What you&#39;ll learn","What you'll learn","heading",{"id":151,"html":152,"type":141},"b4","\u003Cp>By the end of this article you&#39;ll be able to:\u003C\u002Fp>",{"id":154,"type":155,"items":156,"ordered":18},"b5","list",[157,158,159,160,161],"Explain exactly what changed in \u003Ccode>revalidateTag\u003C\u002Fcode>&#39;s signature in Next.js 16, and why old, un-migrated calls now behave differently than they did in 15","Use the new \u003Ccode>profile\u003C\u002Fcode> argument (\u003Ccode>&quot;max&quot;\u003C\u002Fcode>, a custom \u003Ccode>cacheLife\u003C\u002Fcode> name, or \u003Ccode>{ expire }\u003C\u002Fcode>) to choose stale-while-revalidate on purpose instead of by accident","Reach for \u003Ccode>updateTag\u003C\u002Fcode> correctly — and know the one restriction that makes it unusable outside a Server Action","Pick the right tool for three real situations: a Server Action after a write, a webhook Route Handler, and &quot;the reader must see their own change immediately&quot;","Read the TypeScript error a bare \u003Ccode>revalidateTag(tag)\u003C\u002Fcode> now produces, and migrate it correctly",{"id":163,"html":164,"text":164,"type":149,"level":31},"b6","Who this is for",{"id":166,"html":167,"type":141},"b7","\u003Cp>You&#39;ve used \u003Ccode>revalidateTag\u003C\u002Fcode> or \u003Ccode>cacheTag\u003C\u002Fcode> in an App Router project before — maybe to invalidate a list page after a mutation. You don&#39;t need to have used Cache Components; this article explains the pieces it touches as it goes.\u003C\u002Fp>",{"id":169,"html":170,"type":141},"b8","\u003Cp>This article is written against \u003Cstrong>Next.js 16.4\u003C\u002Fstrong> (verified against the framework&#39;s own \u003Ccode>next\u002Fcache\u003C\u002Fcode> API documentation and the v16.0.0 release notes from October 2025, where both the new \u003Ccode>updateTag\u003C\u002Fcode> function and \u003Ccode>revalidateTag\u003C\u002Fcode>&#39;s new signature shipped). Where the pre-16 behavior differs, it&#39;s stated explicitly rather than assumed.\u003C\u002Fp>",{"id":172,"html":173,"text":173,"type":149,"level":31},"b9","Table of contents",{"id":175,"type":155,"items":176,"ordered":18},"b10",[177,178,179,180,181,182,183,184,185,186],"\u003Ca href=\"#the-problem-a-call-that-still-compiles-now-does-less-for-free\">The problem: a call that still compiles, now does less for free\u003C\u002Fa>","\u003Ca href=\"#the-mental-model-two-knobs-not-one\">The mental model: two knobs, not one\u003C\u002Fa>","\u003Ca href=\"#stage-1-what-the-deprecated-call-actually-does-now\">Stage 1: what the deprecated call actually does now\u003C\u002Fa>","\u003Ca href=\"#stage-2-revalidatetagtag-max--keeping-the-old-ux-on-purpose\">Stage 2: \u003Ccode>revalidateTag(tag, &quot;max&quot;)\u003C\u002Fcode> — keeping the old UX, on purpose\u003C\u002Fa>","\u003Ca href=\"#stage-3-updatetag--read-your-own-write-from-a-server-action-only\">Stage 3: \u003Ccode>updateTag\u003C\u002Fcode> — read-your-own-write, from a Server Action only\u003C\u002Fa>","\u003Ca href=\"#stage-4-the-webhook-case-where-updatetag-isnt-an-option\">Stage 4: the webhook case, where \u003Ccode>updateTag\u003C\u002Fcode> isn&#39;t an option\u003C\u002Fa>","\u003Ca href=\"#edge-cases-and-gotchas\">Edge cases and gotchas\u003C\u002Fa>","\u003Ca href=\"#best-practices\">Best practices\u003C\u002Fa>","\u003Ca href=\"#faq\">FAQ\u003C\u002Fa>","\u003Ca href=\"#cheat-sheet\">Cheat sheet\u003C\u002Fa>",{"id":188,"html":189,"text":189,"type":149,"level":31},"b11","The problem: a call that still compiles, now does less for free",{"id":191,"html":192,"type":141},"b12","\u003Cp>Here&#39;s a Server Action that was correct in Next.js 15 and is still, technically, legal code in 16:\u003C\u002Fp>",{"id":194,"code":195,"type":196,"language":197,"highlight":198},"b13","\u002F\u002F app\u002Factions.ts — written for Next.js 15\n'use server';\n\nimport { revalidateTag } from 'next\u002Fcache';\n\nexport async function publishPost(formData: FormData) {\n  await db.posts.create({ title: formData.get('title') });\n  revalidateTag('posts'); \u002F\u002F one argument — this was the whole API\n}","code","ts",[],{"id":200,"html":201,"type":141},"b14","\u003Cp>In Next.js 15, this marked every cache entry tagged \u003Ccode>posts\u003C\u002Fcode> as stale. It didn&#39;t refresh anything immediately. The \u003Cem>next\u003C\u002Fem> request that touched a \u003Ccode>posts\u003C\u002Fcode>-tagged entry got served the stale copy instantly, while Next.js regenerated it in the background — the standard stale-while-revalidate shape, and the reason revalidation never caused a visible slowdown.\u003C\u002Fp>",{"id":203,"html":204,"type":141},"b15","\u003Cp>In Next.js 16, the same call is deprecated, and its documented behavior now matches passing \u003Ccode>{ expire: 0 }\u003C\u002Fcode> explicitly: the tag&#39;s cached entries lose their stale grace period entirely, so the next request becomes a \u003Cstrong>blocking cache miss\u003C\u002Fstrong> — it waits for the data to be refetched before it can render anything. If you&#39;re on TypeScript with the current \u003Ccode>next\u003C\u002Fcode> types, you&#39;ll actually see this before it ships: the one-argument call no longer type-checks, because \u003Ccode>revalidateTag\u003C\u002Fcode>&#39;s signature now requires a second argument. A plain JavaScript project, or one with type errors suppressed, won&#39;t see anything — just a page that got slower for no reason the diff explains.\u003C\u002Fp>",{"id":206,"html":207,"text":207,"type":149,"level":31},"b16","The mental model: two knobs, not one",{"id":209,"html":210,"type":141},"b17","\u003Cp>\u003Cstrong>The mental model:\u003C\u002Fstrong> before Next.js 16, &quot;revalidate this tag&quot; was one decision — stale now, refreshed lazily, always. Next.js 16 splits that into two independent knobs, and makes you set both:\u003C\u002Fp>",{"id":212,"type":155,"items":213,"ordered":17},"b18",[214,215],"\u003Cstrong>Which tag is now out of date.\u003C\u002Fstrong> Unchanged — you still assign tags with \u003Ccode>cacheTag(&#39;posts&#39;)\u003C\u002Fcode> inside a \u003Ccode>&#39;use cache&#39;\u003C\u002Fcode> function, or the \u003Ccode>next: { tags: [...] }\u003C\u002Fcode> option on \u003Ccode>fetch\u003C\u002Fcode>.","\u003Cstrong>How long is a stale copy still acceptable to serve while fresh data loads.\u003C\u002Fstrong> This is the new \u003Ccode>profile\u003C\u002Fcode> argument. \u003Ccode>&quot;max&quot;\u003C\u002Fcode> means &quot;basically forever&quot; (a one-year window) — in practice, the next visitor always gets the stale copy, and it&#39;s a backstage reader who ever sees a cache miss. \u003Ccode>{ expire: 0 }\u003C\u002Fcode> means &quot;never&quot; — the very next request must wait. Anything in between is a named \u003Ccode>cacheLife\u003C\u002Fcode> profile (\u003Ccode>&quot;hours&quot;\u003C\u002Fcode>, \u003Ccode>&quot;days&quot;\u003C\u002Fcode>, …) or a custom \u003Ccode>{ expire: n }\u003C\u002Fcode> in seconds.",{"id":217,"html":218,"type":141},"b19","\u003Cp>\u003Ccode>updateTag\u003C\u002Fcode> is the same second knob pinned permanently to \u003Ccode>{ expire: 0 }\u003C\u002Fcode>, plus one restriction that \u003Ccode>revalidateTag\u003C\u002Fcode> doesn&#39;t have: it only runs from inside a Server Action. That restriction is the point — it exists for the one case where &quot;the \u003Cem>next\u003C\u002Fem> request should see this&quot; actually means &quot;the request \u003Cem>I&#39;m about to send back to the browser that just wrote this\u003C\u002Fem> should see this,&quot; which is a guarantee only a Server Action&#39;s own round trip can make.\u003C\u002Fp>",{"id":220,"html":221,"text":221,"type":149,"level":31},"b20","Stage 1: what the deprecated call actually does now",{"id":223,"html":224,"type":141},"b21","\u003Cp>Don&#39;t write the bare call going forward — but it&#39;s worth seeing exactly what it degrades to, since it&#39;s the code every pre-16 App Router project already has:\u003C\u002Fp>",{"id":226,"code":227,"type":196,"language":197,"highlight":228},"b22","\u002F\u002F Deprecated as of Next.js 16 — still \"works,\" but not the way it used to\nrevalidateTag('posts');\n\u002F\u002F ...is now read as:\nrevalidateTag('posts', { expire: 0 });",[],{"id":230,"html":231,"type":141},"b23","\u003Cp>Both forms tell Next.js: forget the stale window entirely, and make the next request pay for a fresh fetch. The difference from 15 isn&#39;t the tag, isn&#39;t the function name — it&#39;s that this was never the default behavior before, and now it quietly is.\u003C\u002Fp>",{"id":233,"html":234,"text":235,"type":149,"level":31},"b24","Stage 2: \u003Ccode>revalidateTag(tag, &quot;max&quot;)\u003C\u002Fcode> — keeping the old UX, on purpose","Stage 2: revalidateTag(tag, \"max\") — keeping the old UX, on purpose",{"id":237,"html":238,"type":141},"b25","\u003Cp>Most of the time, the 15-era experience — instant stale response, quiet background refresh — is still what you want. In 16 you ask for it explicitly:\u003C\u002Fp>",{"id":240,"code":241,"type":196,"language":197,"highlight":242},"b26","\u002F\u002F app\u002Factions.ts\n'use server';\n\nimport { revalidateTag } from 'next\u002Fcache';\n\nexport async function publishPost(formData: FormData) {\n  await db.posts.create({ title: formData.get('title') });\n  revalidateTag('posts', 'max'); \u002F\u002F stale-while-revalidate, same shape as pre-16 default\n}",[],{"id":244,"html":245,"type":141},"b27","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> \u003Ccode>&quot;max&quot;\u003C\u002Fcode> isn&#39;t a special case bolted onto \u003Ccode>revalidateTag\u003C\u002Fcode> — it&#39;s the same named profile \u003Ccode>cacheLife(&#39;max&#39;)\u003C\u002Fcode> uses elsewhere (a 30-day background-refresh window, one-year outer expiry). The profile argument accepts any \u003Ccode>cacheLife\u003C\u002Fcode> profile name or a \u003Ccode>{ expire }\u003C\u002Fcode> object; \u003Ccode>&quot;max&quot;\u003C\u002Fcode> is simply the one that reproduces the old always-stale-while-revalidating default most closely.\u003C\u002Fp>",{"id":247,"html":248,"type":141},"b28","\u003Cp>This one-line migration is also the fix for the TypeScript error from Stage 1 — it&#39;s the smallest change that both satisfies the new signature and keeps the behavior your app already shipped with.\u003C\u002Fp>",{"id":250,"html":251,"type":141},"b29","\u003C!-- playground:start -->",{"id":253,"html":254,"text":254,"type":149,"level":31},"b30","🎮 Try it yourself",{"id":256,"html":257,"type":141},"b31","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-updatetag-vs-revalidatetag\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":259,"html":260,"type":141},"b32","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":262,"html":263,"type":141},"b33","\u003C!-- playground:end -->",{"id":265,"html":266,"text":267,"type":149,"level":31},"b34","Stage 3: \u003Ccode>updateTag\u003C\u002Fcode> — read-your-own-write, from a Server Action only","Stage 3: updateTag — read-your-own-write, from a Server Action only",{"id":269,"html":270,"type":141},"b35","\u003Cp>Some flows need the opposite of stale-while-revalidate: the person who just wrote the data must see it on their very next page load, not &quot;eventually.&quot; That&#39;s what \u003Ccode>updateTag\u003C\u002Fcode> is for:\u003C\u002Fp>",{"id":272,"code":273,"type":196,"language":197,"highlight":274},"b36","\u002F\u002F app\u002Factions.ts\n'use server';\n\nimport { updateTag } from 'next\u002Fcache';\n\nexport async function publishPost(formData: FormData) {\n  const post = await db.posts.create({ title: formData.get('title') });\n  updateTag('posts');          \u002F\u002F the list page the author is about to land on\n  updateTag(`post-${post.id}`); \u002F\u002F the detail page too, if it's tagged separately\n  redirect(`\u002Fposts\u002F${post.id}`);\n}",[],{"id":276,"html":277,"type":141},"b37","\u003Cp>\u003Ccode>updateTag(tag)\u003C\u002Fcode> takes no second argument — it&#39;s always an immediate expiry, by design, because &quot;read your own write&quot; has exactly one acceptable staleness window: none. Call it from anywhere else — a Route Handler, a Client Component, \u003Ccode>proxy.ts\u003C\u002Fcode> — and it throws. That&#39;s not a bug to work around; it&#39;s the signal that you&#39;re in the wrong place for a read-your-own-write guarantee and should reach for \u003Ccode>revalidateTag\u003C\u002Fcode> instead.\u003C\u002Fp>",{"id":279,"html":280,"type":141},"b38","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> \u003Ccode>updateTag\u003C\u002Fcode> and \u003Ccode>revalidateTag(tag, { expire: 0 })\u003C\u002Fcode> end up invalidating the same cache entries the same way. The difference is entirely about \u003Cem>where you&#39;re allowed to call them\u003C\u002Fem> and what that implies: \u003Ccode>updateTag\u003C\u002Fcode>&#39;s restriction to Server Actions is what lets Next.js guarantee the very next render — the one already in flight back to that same browser — sees the fresh data.\u003C\u002Fp>",{"id":282,"html":283,"text":284,"type":149,"level":31},"b39","Stage 4: the webhook case, where \u003Ccode>updateTag\u003C\u002Fcode> isn&#39;t an option","Stage 4: the webhook case, where updateTag isn't an option",{"id":286,"html":287,"type":141},"b40","\u003Cp>A CMS webhook that fires when an editor publishes from a completely separate system has no Server Action to call \u003Ccode>updateTag\u003C\u002Fcode> from — it&#39;s a Route Handler, reached by an HTTP request from somewhere else entirely:\u003C\u002Fp>",{"id":289,"code":290,"type":196,"language":197,"highlight":291},"b41","\u002F\u002F app\u002Fapi\u002Frevalidate\u002Froute.ts\nimport type { NextRequest } from 'next\u002Fserver';\nimport { revalidateTag } from 'next\u002Fcache';\n\nexport async function POST(request: NextRequest) {\n  const { tag, secret } = await request.json();\n  if (secret !== process.env.REVALIDATE_SECRET) {\n    return Response.json({ revalidated: false }, { status: 401 });\n  }\n  revalidateTag(tag, 'max');\n  return Response.json({ revalidated: true, now: Date.now() });\n}",[],{"id":293,"html":294,"type":141},"b42","\u003Cp>\u003Ccode>revalidateTag\u003C\u002Fcode> works in both Server Actions and Route Handlers; \u003Ccode>updateTag\u003C\u002Fcode> works in neither except the former. For a webhook, \u003Ccode>&#39;max&#39;\u003C\u002Fcode> is almost always right — the editor&#39;s CMS doesn&#39;t need to see the update instantly, your readers do, and stale-while-revalidate means nobody&#39;s request ever blocks on it. Reach for \u003Ccode>{ expire: 0 }\u003C\u002Fcode> only if the content being invalidated must never be served stale again even once — a takedown, a price correction, something wrong enough that one more stale read is a real cost.\u003C\u002Fp>",{"id":296,"html":297,"text":297,"type":149,"level":31},"b43","Edge cases and gotchas",{"id":299,"type":155,"items":300,"ordered":18},"b44",[301,302,303,304,305,306],"\u003Cstrong>The deprecated one-argument call &quot;works only if TypeScript errors are suppressed.&quot;\u003C\u002Fstrong> That&#39;s the documentation&#39;s own description of its current status — the type signature now requires \u003Ccode>profile\u003C\u002Fcode>, so a strict TypeScript build already fails on it. If your build is green on a bare \u003Ccode>revalidateTag(tag)\u003C\u002Fcode> today, either you&#39;re on plain JavaScript or something in your config is swallowing the type error; either way, the runtime behavior already changed.","\u003Cstrong>Tags are case-sensitive and capped at 256 characters.\u003C\u002Fstrong> A tag longer than that is never actually attached to anything, so revalidating it does nothing — and nothing tells you it silently no-op&#39;d.","\u003Cstrong>A tag has to exist before you can invalidate it.\u003C\u002Fstrong> \u003Ccode>cacheTag(&#39;posts&#39;)\u003C\u002Fcode> inside a \u003Ccode>&#39;use cache&#39;\u003C\u002Fcode> function, or \u003Ccode>fetch(url, { next: { tags: [&#39;posts&#39;] } })\u003C\u002Fcode>, is what attaches the tag in the first place. Calling \u003Ccode>revalidateTag(&#39;posts&#39;)\u003C\u002Fcode> for a tag nothing ever assigned is a legal, silent no-op.","\u003Cstrong>The revalidation runs on the next \u003Cem>request\u003C\u002Fem>, not on the call itself.\u003C\u002Fstrong> \u003Ccode>revalidateTag\u003C\u002Fcode>\u002F\u003Ccode>updateTag\u003C\u002Fcode> mark data stale or expired; they don&#39;t reach out and regenerate it synchronously at call time. A script that calls \u003Ccode>revalidateTag\u003C\u002Fcode> for a thousand tags in a loop doesn&#39;t do a thousand regenerations right then — it marks a thousand tags, and each one regenerates whenever its own next request arrives.","\u003Cstrong>\u003Ccode>revalidateTag\u003C\u002Fcode> invalidates by tag across every page that uses it; \u003Ccode>revalidatePath\u003C\u002Fcode> invalidates one path.\u003C\u002Fstrong> They&#39;re not competitors — a mutation that affects a shared list (tag-based) and a specific detail page (path-based) commonly needs both.","\u003Cstrong>None of these run in \u003Ccode>proxy.ts\u003C\u002Fcode> or a Client Component.\u003C\u002Fstrong> Cache invalidation is a server-side, request-scoped operation; both functions throw outside a Server Action or Route Handler.",{"id":308,"html":309,"text":309,"type":149,"level":31},"b45","Best practices",{"id":311,"type":155,"items":312,"ordered":18},"b46",[313,314,315,316,317],"\u003Cstrong>Default to \u003Ccode>revalidateTag(tag, &#39;max&#39;)\u003C\u002Fcode> for anything a normal visitor reads.\u003C\u002Fstrong> It reproduces the stale-while-revalidate experience every pre-16 app already had, and it&#39;s almost never the wrong call for a list page, a feed, or anything else where &quot;a few seconds old&quot; is indistinguishable from fresh.","\u003Cstrong>Reserve \u003Ccode>updateTag\u003C\u002Fcode> for the exact read-your-own-write moment\u003C\u002Fstrong> — right after a Server Action&#39;s own mutation, on the page that same action is about to redirect to. Don&#39;t reach for it as a general-purpose &quot;make this fresher&quot; tool; that&#39;s what the \u003Ccode>profile\u003C\u002Fcode> argument on \u003Ccode>revalidateTag\u003C\u002Fcode> is for.","\u003Cstrong>Migrate the bare \u003Ccode>revalidateTag(tag)\u003C\u002Fcode> calls first, deliberately, not by auto-fixing them to \u003Ccode>{ expire: 0 }\u003C\u002Fcode>.\u003C\u002Fstrong> The codemod or a quick find-and-replace will make the type error go away either way, but silently keeping the new blocking behavior everywhere is exactly the slowdown this article opened with. Decide \u003Ccode>&#39;max&#39;\u003C\u002Fcode> vs \u003Ccode>{ expire: 0 }\u003C\u002Fcode> per call site.","\u003Cstrong>Keep webhook-triggered invalidation on \u003Ccode>revalidateTag\u003C\u002Fcode>, always.\u003C\u002Fstrong> \u003Ccode>updateTag\u003C\u002Fcode>&#39;s Server-Action-only restriction isn&#39;t a gap to route around with a trick — treat it as confirmation you&#39;re using the right tool for an external trigger.","\u003Cstrong>Pair tag-based and path-based invalidation when a mutation really touches both.\u003C\u002Fstrong> A single \u003Ccode>revalidateTag\u003C\u002Fcode> call doesn&#39;t help a page that was only ever cached by path.",{"id":319,"html":320,"text":320,"type":149,"level":31},"b47","FAQ",{"id":322,"html":323,"text":324,"type":149,"level":43},"b48","Why did \u003Ccode>revalidateTag(&#39;posts&#39;)\u003C\u002Fcode> get slower after I upgraded to Next.js 16?","Why did revalidateTag('posts') get slower after I upgraded to Next.js 16?",{"id":326,"html":327,"type":141},"b49","\u003Cp>It didn&#39;t get slower by accident — its documented behavior changed. The one-argument form is deprecated and now matches \u003Ccode>{ expire: 0 }\u003C\u002Fcode>: an immediate expiry instead of the old lazy, stale-while-revalidate refresh. Add \u003Ccode>&#39;max&#39;\u003C\u002Fcode> as a second argument to restore the pre-16 behavior on purpose.\u003C\u002Fp>",{"id":329,"html":330,"text":331,"type":149,"level":43},"b50","Is \u003Ccode>revalidateTag\u003C\u002Fcode> being removed?","Is revalidateTag being removed?",{"id":333,"html":334,"type":141},"b51","\u003Cp>Not entirely. The one-argument call is deprecated and documented as something that &quot;may be removed in a future version,&quot; but \u003Ccode>revalidateTag(tag, profile)\u003C\u002Fcode> with an explicit profile is the current, supported API — it isn&#39;t going anywhere.\u003C\u002Fp>",{"id":336,"html":337,"text":338,"type":149,"level":43},"b52","Can I call \u003Ccode>updateTag\u003C\u002Fcode> from a Route Handler?","Can I call updateTag from a Route Handler?",{"id":340,"html":341,"type":141},"b53","\u003Cp>No. It only runs inside a Server Action, and calling it anywhere else — a Route Handler, a Client Component, \u003Ccode>proxy.ts\u003C\u002Fcode> — throws. Use \u003Ccode>revalidateTag(tag, { expire: 0 })\u003C\u002Fcode> there instead if you need the same immediate-expiry behavior outside a Server Action.\u003C\u002Fp>",{"id":343,"html":344,"text":344,"type":149,"level":43},"b54","What profile should I pass for a webhook that revalidates on publish?",{"id":346,"html":347,"type":141},"b55","\u003Cp>\u003Ccode>&#39;max&#39;\u003C\u002Fcode> almost always. The system calling the webhook (a CMS, a headless commerce backend) doesn&#39;t need the new data to appear instantly from its own point of view — your readers do, and \u003Ccode>&#39;max&#39;\u003C\u002Fcode> means they always get an instant, cached response while Next.js refreshes it behind the scenes.\u003C\u002Fp>",{"id":349,"html":350,"text":350,"type":149,"level":43},"b56","Does this change anything about how tags are assigned?",{"id":352,"html":353,"type":141},"b57","\u003Cp>No. \u003Ccode>cacheTag()\u003C\u002Fcode> inside a \u003Ccode>&#39;use cache&#39;\u003C\u002Fcode> function and the \u003Ccode>next: { tags: [...] }\u003C\u002Fcode> option on \u003Ccode>fetch\u003C\u002Fcode> work exactly as before. What changed is only the second half of the pair — how you tell Next.js to treat a tag&#39;s existing cached data as stale.\u003C\u002Fp>",{"id":355,"html":356,"text":356,"type":149,"level":31},"b58","Cheat sheet",{"id":358,"head":359,"rows":363,"type":386},"b59",[360,361,362],"Situation","Call","Where it runs",[364,368,371,375,379,383],[365,366,367],"Keep pre-16 behavior (stale-while-revalidate)","\u003Ccode>revalidateTag(tag, &#39;max&#39;)\u003C\u002Fcode>","Server Action or Route Handler",[369,370,367],"Custom staleness window","\u003Ccode>revalidateTag(tag, &#39;hours&#39;)\u003C\u002Fcode> or \u003Ccode>revalidateTag(tag, { expire: 3600 })\u003C\u002Fcode>",[372,373,374],"Reader must see their own write immediately","\u003Ccode>updateTag(tag)\u003C\u002Fcode>","\u003Cstrong>Server Action only\u003C\u002Fstrong> — throws elsewhere",[376,377,378],"Immediate expiry, outside a Server Action (e.g. a webhook)","\u003Ccode>revalidateTag(tag, { expire: 0 })\u003C\u002Fcode>","Route Handler",[380,381,382],"⚠️ Old one-argument call","\u003Ccode>revalidateTag(tag)\u003C\u002Fcode>","Deprecated — now behaves like \u003Ccode>{ expire: 0 }\u003C\u002Fcode>, and fails to type-check under current \u003Ccode>next\u003C\u002Fcode> types",[384,385,367],"Invalidate a specific route instead of a tag","\u003Ccode>revalidatePath(path)\u003C\u002Fcode>","table",{"id":388,"code":389,"type":196,"language":197,"highlight":390},"b60","\u002F\u002F The decision, as code\n'use server';\nimport { revalidateTag, updateTag } from 'next\u002Fcache';\n\nexport async function publishPost(formData: FormData) {\n  const post = await db.posts.create({ title: formData.get('title') });\n\n  \u002F\u002F Everyone else: stale is fine, never block a request for it.\n  revalidateTag('posts', 'max');\n\n  \u002F\u002F The author, about to be redirected to the post they just wrote:\n  \u002F\u002F they must see it now, not the pre-write cached version.\n  updateTag(`post-${post.id}`);\n\n  redirect(`\u002Fposts\u002F${post.id}`);\n}",[],{"id":392,"html":393,"text":393,"type":149,"level":31},"b61","Key takeaways",{"id":395,"type":155,"items":396,"ordered":18},"b62",[397,398,399,400,401],"Next.js 16 split &quot;revalidate this tag&quot; into two decisions: which tag is stale, and how long a stale copy may still be served. The second one is a required argument now, not an implicit default.","The bare \u003Ccode>revalidateTag(tag)\u003C\u002Fcode> call is deprecated and now behaves like \u003Ccode>{ expire: 0 }\u003C\u002Fcode> — an immediate, blocking cache miss — which is the \u003Cem>opposite\u003C\u002Fem> of the lazy stale-while-revalidate behavior it had in 15.","\u003Ccode>revalidateTag(tag, &#39;max&#39;)\u003C\u002Fcode> is the one-line migration that restores the old, reader-friendly default on purpose.","\u003Ccode>updateTag\u003C\u002Fcode> exists for exactly one job — the writer seeing their own change on the very next render — and that job is why it&#39;s restricted to Server Actions only.","Webhooks and anything outside a Server Action stay on \u003Ccode>revalidateTag\u003C\u002Fcode>; there&#39;s no version of \u003Ccode>updateTag\u003C\u002Fcode> for that context, by design.",{"id":403,"html":404,"type":141},"b63","\u003C!-- quiz:start -->",{"id":406,"html":407,"text":407,"type":149,"level":31},"b64","🧠 Test yourself",{"id":409,"html":410,"type":141},"b65","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-updatetag-vs-revalidatetag\u002Fquiz\">Take the 7-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":412,"html":413,"type":141},"b66","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":415,"html":416,"type":141},"b67","\u003C!-- quiz:end -->",{"id":418,"html":419,"text":419,"type":149,"level":31},"b68","Back to that slow publish button",{"id":421,"html":422,"type":141},"b69","\u003Cp>The team in the opening didn&#39;t ship a bug — they shipped code that was correct for the Next.js version it was written against, and the ground moved under one function call during an upgrade that otherwise looked clean. The fix was one argument: \u003Ccode>revalidateTag(&#39;posts&#39;, &#39;max&#39;)\u003C\u002Fcode> to get the old behavior back, and \u003Ccode>updateTag\u003C\u002Fcode> reserved for the one page where &quot;eventually&quot; was never good enough.\u003C\u002Fp>",{"id":424,"html":425,"type":141},"b70","\u003Cp>If \u003Ccode>cacheTag\u003C\u002Fcode>, \u003Ccode>cacheLife\u003C\u002Fcode>, and the static-shell-vs-streaming split underneath all of this are new to you, \u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-cache-components-explained\">Next.js Cache Components Explained\u003C\u002Fa> builds that model from the ground up. For the mutation side of this same flow — validating the \u003Ccode>FormData\u003C\u002Fcode>, the CSRF guarantees a Server Action gives you for free, and the \u003Ccode>redirect()\u003C\u002Fcode> gotcha — see \u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-server-actions-mutations-security\">Next.js Server Actions: Mutations &amp; Security\u003C\u002Fa>. And if your cache entries are built from a Route Handler&#39;s own \u003Ccode>GET\u003C\u002Fcode> rather than a Server Component, \u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-route-handlers-caching-streaming\">Next.js Route Handlers: GET Stopped Caching in 15\u003C\u002Fa> covers the other half of the 15-to-16 caching story.\u003C\u002Fp>",{"id":427,"html":428,"type":141},"b71","\u003Cp>Did your own \u003Ccode>revalidateTag\u003C\u002Fcode> calls survive the upgrade with the behavior you expected — or did you find this one the hard way?\u003C\u002Fp>",{"id":430,"html":431,"type":141},"b72","\u003C!-- related:start -->",{"id":433,"html":434,"text":434,"type":149,"level":31},"b73","📚 Read next",{"id":436,"type":155,"items":437,"ordered":18},"b74",[438,439,440],"\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-middleware-to-proxy-network-boundary\">Next.js proxy.ts Explained (with Cheat Sheet)\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-route-handlers-caching-streaming\">Next.js Route Handlers: GET Stopped Caching in 15 — How to Cache in 16\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-cache-components-explained\">Next.js Cache Components Explained (with Cheat Sheet)\u003C\u002Fa>",{"id":442,"html":443,"type":141},"b75","\u003C!-- related:end -->",{"id":445,"type":446},"b76","divider",{"id":448,"html":449,"type":141},"b77","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":451,"html":452,"type":141},"b78","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":454,"type":155,"items":455,"ordered":18},"b79",[456,457,458],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","Your team upgrades to Next.js 16, the build passes, and nothing in the diff touches caching. A week later, publishing a blog post starts taking noticeably longer — not to save, but for the *next reader* to see it. Somewhere in a Server Action sits a line nobody touched: `revalidateTag('posts')`. It still runs. It just does something different now.\n\nThat line used to mark the `posts` tag stale and move on — the next visitor got the old page instantly while Next.js refreshed it in the background. In Next.js 16, the same one-argument call is deprecated, and its *behavior* changed to match an immediate, blocking cache miss: the next request to anything tagged `posts` now waits for fresh data before it renders. No error, no red build — just a slower app and a cache-invalidation story that quietly inverted.\n\n## What you'll learn\n\nBy the end of this article you'll be able to:\n\n- Explain exactly what changed in `revalidateTag`'s signature in Next.js 16, and why old, un-migrated calls now behave differently than they did in 15\n- Use the new `profile` argument (`\"max\"`, a custom `cacheLife` name, or `{ expire }`) to choose stale-while-revalidate on purpose instead of by accident\n- Reach for `updateTag` correctly — and know the one restriction that makes it unusable outside a Server Action\n- Pick the right tool for three real situations: a Server Action after a write, a webhook Route Handler, and \"the reader must see their own change immediately\"\n- Read the TypeScript error a bare `revalidateTag(tag)` now produces, and migrate it correctly\n\n## Who this is for\n\nYou've used `revalidateTag` or `cacheTag` in an App Router project before — maybe to invalidate a list page after a mutation. You don't need to have used Cache Components; this article explains the pieces it touches as it goes.\n\nThis article is written against **Next.js 16.4** (verified against the framework's own `next\u002Fcache` API documentation and the v16.0.0 release notes from October 2025, where both the new `updateTag` function and `revalidateTag`'s new signature shipped). Where the pre-16 behavior differs, it's stated explicitly rather than assumed.\n\n## Table of contents\n\n- [The problem: a call that still compiles, now does less for free](#the-problem-a-call-that-still-compiles-now-does-less-for-free)\n- [The mental model: two knobs, not one](#the-mental-model-two-knobs-not-one)\n- [Stage 1: what the deprecated call actually does now](#stage-1-what-the-deprecated-call-actually-does-now)\n- [Stage 2: `revalidateTag(tag, \"max\")` — keeping the old UX, on purpose](#stage-2-revalidatetagtag-max--keeping-the-old-ux-on-purpose)\n- [Stage 3: `updateTag` — read-your-own-write, from a Server Action only](#stage-3-updatetag--read-your-own-write-from-a-server-action-only)\n- [Stage 4: the webhook case, where `updateTag` isn't an option](#stage-4-the-webhook-case-where-updatetag-isnt-an-option)\n- [Edge cases and gotchas](#edge-cases-and-gotchas)\n- [Best practices](#best-practices)\n- [FAQ](#faq)\n- [Cheat sheet](#cheat-sheet)\n\n## The problem: a call that still compiles, now does less for free\n\nHere's a Server Action that was correct in Next.js 15 and is still, technically, legal code in 16:\n\n```ts\n\u002F\u002F app\u002Factions.ts — written for Next.js 15\n'use server';\n\nimport { revalidateTag } from 'next\u002Fcache';\n\nexport async function publishPost(formData: FormData) {\n  await db.posts.create({ title: formData.get('title') });\n  revalidateTag('posts'); \u002F\u002F one argument — this was the whole API\n}\n```\n\nIn Next.js 15, this marked every cache entry tagged `posts` as stale. It didn't refresh anything immediately. The *next* request that touched a `posts`-tagged entry got served the stale copy instantly, while Next.js regenerated it in the background — the standard stale-while-revalidate shape, and the reason revalidation never caused a visible slowdown.\n\nIn Next.js 16, the same call is deprecated, and its documented behavior now matches passing `{ expire: 0 }` explicitly: the tag's cached entries lose their stale grace period entirely, so the next request becomes a **blocking cache miss** — it waits for the data to be refetched before it can render anything. If you're on TypeScript with the current `next` types, you'll actually see this before it ships: the one-argument call no longer type-checks, because `revalidateTag`'s signature now requires a second argument. A plain JavaScript project, or one with type errors suppressed, won't see anything — just a page that got slower for no reason the diff explains.\n\n## The mental model: two knobs, not one\n\n**The mental model:** before Next.js 16, \"revalidate this tag\" was one decision — stale now, refreshed lazily, always. Next.js 16 splits that into two independent knobs, and makes you set both:\n\n1. **Which tag is now out of date.** Unchanged — you still assign tags with `cacheTag('posts')` inside a `'use cache'` function, or the `next: { tags: [...] }` option on `fetch`.\n2. **How long is a stale copy still acceptable to serve while fresh data loads.** This is the new `profile` argument. `\"max\"` means \"basically forever\" (a one-year window) — in practice, the next visitor always gets the stale copy, and it's a backstage reader who ever sees a cache miss. `{ expire: 0 }` means \"never\" — the very next request must wait. Anything in between is a named `cacheLife` profile (`\"hours\"`, `\"days\"`, …) or a custom `{ expire: n }` in seconds.\n\n`updateTag` is the same second knob pinned permanently to `{ expire: 0 }`, plus one restriction that `revalidateTag` doesn't have: it only runs from inside a Server Action. That restriction is the point — it exists for the one case where \"the *next* request should see this\" actually means \"the request *I'm about to send back to the browser that just wrote this* should see this,\" which is a guarantee only a Server Action's own round trip can make.\n\n## Stage 1: what the deprecated call actually does now\n\nDon't write the bare call going forward — but it's worth seeing exactly what it degrades to, since it's the code every pre-16 App Router project already has:\n\n```ts\n\u002F\u002F Deprecated as of Next.js 16 — still \"works,\" but not the way it used to\nrevalidateTag('posts');\n\u002F\u002F ...is now read as:\nrevalidateTag('posts', { expire: 0 });\n```\n\nBoth forms tell Next.js: forget the stale window entirely, and make the next request pay for a fresh fetch. The difference from 15 isn't the tag, isn't the function name — it's that this was never the default behavior before, and now it quietly is.\n\n## Stage 2: `revalidateTag(tag, \"max\")` — keeping the old UX, on purpose\n\nMost of the time, the 15-era experience — instant stale response, quiet background refresh — is still what you want. In 16 you ask for it explicitly:\n\n```ts\n\u002F\u002F app\u002Factions.ts\n'use server';\n\nimport { revalidateTag } from 'next\u002Fcache';\n\nexport async function publishPost(formData: FormData) {\n  await db.posts.create({ title: formData.get('title') });\n  revalidateTag('posts', 'max'); \u002F\u002F stale-while-revalidate, same shape as pre-16 default\n}\n```\n\n**Key concept:** `\"max\"` isn't a special case bolted onto `revalidateTag` — it's the same named profile `cacheLife('max')` uses elsewhere (a 30-day background-refresh window, one-year outer expiry). The profile argument accepts any `cacheLife` profile name or a `{ expire }` object; `\"max\"` is simply the one that reproduces the old always-stale-while-revalidating default most closely.\n\nThis one-line migration is also the fix for the TypeScript error from Stage 1 — it's the smallest change that both satisfies the new signature and keeps the behavior your app already shipped with.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-updatetag-vs-revalidatetag\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## Stage 3: `updateTag` — read-your-own-write, from a Server Action only\n\nSome flows need the opposite of stale-while-revalidate: the person who just wrote the data must see it on their very next page load, not \"eventually.\" That's what `updateTag` is for:\n\n```ts\n\u002F\u002F app\u002Factions.ts\n'use server';\n\nimport { updateTag } from 'next\u002Fcache';\n\nexport async function publishPost(formData: FormData) {\n  const post = await db.posts.create({ title: formData.get('title') });\n  updateTag('posts');          \u002F\u002F the list page the author is about to land on\n  updateTag(`post-${post.id}`); \u002F\u002F the detail page too, if it's tagged separately\n  redirect(`\u002Fposts\u002F${post.id}`);\n}\n```\n\n`updateTag(tag)` takes no second argument — it's always an immediate expiry, by design, because \"read your own write\" has exactly one acceptable staleness window: none. Call it from anywhere else — a Route Handler, a Client Component, `proxy.ts` — and it throws. That's not a bug to work around; it's the signal that you're in the wrong place for a read-your-own-write guarantee and should reach for `revalidateTag` instead.\n\n**Key concept:** `updateTag` and `revalidateTag(tag, { expire: 0 })` end up invalidating the same cache entries the same way. The difference is entirely about *where you're allowed to call them* and what that implies: `updateTag`'s restriction to Server Actions is what lets Next.js guarantee the very next render — the one already in flight back to that same browser — sees the fresh data.\n\n## Stage 4: the webhook case, where `updateTag` isn't an option\n\nA CMS webhook that fires when an editor publishes from a completely separate system has no Server Action to call `updateTag` from — it's a Route Handler, reached by an HTTP request from somewhere else entirely:\n\n```ts\n\u002F\u002F app\u002Fapi\u002Frevalidate\u002Froute.ts\nimport type { NextRequest } from 'next\u002Fserver';\nimport { revalidateTag } from 'next\u002Fcache';\n\nexport async function POST(request: NextRequest) {\n  const { tag, secret } = await request.json();\n  if (secret !== process.env.REVALIDATE_SECRET) {\n    return Response.json({ revalidated: false }, { status: 401 });\n  }\n  revalidateTag(tag, 'max');\n  return Response.json({ revalidated: true, now: Date.now() });\n}\n```\n\n`revalidateTag` works in both Server Actions and Route Handlers; `updateTag` works in neither except the former. For a webhook, `'max'` is almost always right — the editor's CMS doesn't need to see the update instantly, your readers do, and stale-while-revalidate means nobody's request ever blocks on it. Reach for `{ expire: 0 }` only if the content being invalidated must never be served stale again even once — a takedown, a price correction, something wrong enough that one more stale read is a real cost.\n\n## Edge cases and gotchas\n\n- **The deprecated one-argument call \"works only if TypeScript errors are suppressed.\"** That's the documentation's own description of its current status — the type signature now requires `profile`, so a strict TypeScript build already fails on it. If your build is green on a bare `revalidateTag(tag)` today, either you're on plain JavaScript or something in your config is swallowing the type error; either way, the runtime behavior already changed.\n- **Tags are case-sensitive and capped at 256 characters.** A tag longer than that is never actually attached to anything, so revalidating it does nothing — and nothing tells you it silently no-op'd.\n- **A tag has to exist before you can invalidate it.** `cacheTag('posts')` inside a `'use cache'` function, or `fetch(url, { next: { tags: ['posts'] } })`, is what attaches the tag in the first place. Calling `revalidateTag('posts')` for a tag nothing ever assigned is a legal, silent no-op.\n- **The revalidation runs on the next *request*, not on the call itself.** `revalidateTag`\u002F`updateTag` mark data stale or expired; they don't reach out and regenerate it synchronously at call time. A script that calls `revalidateTag` for a thousand tags in a loop doesn't do a thousand regenerations right then — it marks a thousand tags, and each one regenerates whenever its own next request arrives.\n- **`revalidateTag` invalidates by tag across every page that uses it; `revalidatePath` invalidates one path.** They're not competitors — a mutation that affects a shared list (tag-based) and a specific detail page (path-based) commonly needs both.\n- **None of these run in `proxy.ts` or a Client Component.** Cache invalidation is a server-side, request-scoped operation; both functions throw outside a Server Action or Route Handler.\n\n## Best practices\n\n- **Default to `revalidateTag(tag, 'max')` for anything a normal visitor reads.** It reproduces the stale-while-revalidate experience every pre-16 app already had, and it's almost never the wrong call for a list page, a feed, or anything else where \"a few seconds old\" is indistinguishable from fresh.\n- **Reserve `updateTag` for the exact read-your-own-write moment** — right after a Server Action's own mutation, on the page that same action is about to redirect to. Don't reach for it as a general-purpose \"make this fresher\" tool; that's what the `profile` argument on `revalidateTag` is for.\n- **Migrate the bare `revalidateTag(tag)` calls first, deliberately, not by auto-fixing them to `{ expire: 0 }`.** The codemod or a quick find-and-replace will make the type error go away either way, but silently keeping the new blocking behavior everywhere is exactly the slowdown this article opened with. Decide `'max'` vs `{ expire: 0 }` per call site.\n- **Keep webhook-triggered invalidation on `revalidateTag`, always.** `updateTag`'s Server-Action-only restriction isn't a gap to route around with a trick — treat it as confirmation you're using the right tool for an external trigger.\n- **Pair tag-based and path-based invalidation when a mutation really touches both.** A single `revalidateTag` call doesn't help a page that was only ever cached by path.\n\n## FAQ\n\n### Why did `revalidateTag('posts')` get slower after I upgraded to Next.js 16?\n\nIt didn't get slower by accident — its documented behavior changed. The one-argument form is deprecated and now matches `{ expire: 0 }`: an immediate expiry instead of the old lazy, stale-while-revalidate refresh. Add `'max'` as a second argument to restore the pre-16 behavior on purpose.\n\n### Is `revalidateTag` being removed?\n\nNot entirely. The one-argument call is deprecated and documented as something that \"may be removed in a future version,\" but `revalidateTag(tag, profile)` with an explicit profile is the current, supported API — it isn't going anywhere.\n\n### Can I call `updateTag` from a Route Handler?\n\nNo. It only runs inside a Server Action, and calling it anywhere else — a Route Handler, a Client Component, `proxy.ts` — throws. Use `revalidateTag(tag, { expire: 0 })` there instead if you need the same immediate-expiry behavior outside a Server Action.\n\n### What profile should I pass for a webhook that revalidates on publish?\n\n`'max'` almost always. The system calling the webhook (a CMS, a headless commerce backend) doesn't need the new data to appear instantly from its own point of view — your readers do, and `'max'` means they always get an instant, cached response while Next.js refreshes it behind the scenes.\n\n### Does this change anything about how tags are assigned?\n\nNo. `cacheTag()` inside a `'use cache'` function and the `next: { tags: [...] }` option on `fetch` work exactly as before. What changed is only the second half of the pair — how you tell Next.js to treat a tag's existing cached data as stale.\n\n## Cheat sheet\n\n| Situation | Call | Where it runs |\n| --- | --- | --- |\n| Keep pre-16 behavior (stale-while-revalidate) | `revalidateTag(tag, 'max')` | Server Action or Route Handler |\n| Custom staleness window | `revalidateTag(tag, 'hours')` or `revalidateTag(tag, { expire: 3600 })` | Server Action or Route Handler |\n| Reader must see their own write immediately | `updateTag(tag)` | **Server Action only** — throws elsewhere |\n| Immediate expiry, outside a Server Action (e.g. a webhook) | `revalidateTag(tag, { expire: 0 })` | Route Handler |\n| ⚠️ Old one-argument call | `revalidateTag(tag)` | Deprecated — now behaves like `{ expire: 0 }`, and fails to type-check under current `next` types |\n| Invalidate a specific route instead of a tag | `revalidatePath(path)` | Server Action or Route Handler |\n\n```ts\n\u002F\u002F The decision, as code\n'use server';\nimport { revalidateTag, updateTag } from 'next\u002Fcache';\n\nexport async function publishPost(formData: FormData) {\n  const post = await db.posts.create({ title: formData.get('title') });\n\n  \u002F\u002F Everyone else: stale is fine, never block a request for it.\n  revalidateTag('posts', 'max');\n\n  \u002F\u002F The author, about to be redirected to the post they just wrote:\n  \u002F\u002F they must see it now, not the pre-write cached version.\n  updateTag(`post-${post.id}`);\n\n  redirect(`\u002Fposts\u002F${post.id}`);\n}\n```\n\n## Key takeaways\n\n- Next.js 16 split \"revalidate this tag\" into two decisions: which tag is stale, and how long a stale copy may still be served. The second one is a required argument now, not an implicit default.\n- The bare `revalidateTag(tag)` call is deprecated and now behaves like `{ expire: 0 }` — an immediate, blocking cache miss — which is the *opposite* of the lazy stale-while-revalidate behavior it had in 15.\n- `revalidateTag(tag, 'max')` is the one-line migration that restores the old, reader-friendly default on purpose.\n- `updateTag` exists for exactly one job — the writer seeing their own change on the very next render — and that job is why it's restricted to Server Actions only.\n- Webhooks and anything outside a Server Action stay on `revalidateTag`; there's no version of `updateTag` for that context, by design.\n\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 7-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-updatetag-vs-revalidatetag\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n## Back to that slow publish button\n\nThe team in the opening didn't ship a bug — they shipped code that was correct for the Next.js version it was written against, and the ground moved under one function call during an upgrade that otherwise looked clean. The fix was one argument: `revalidateTag('posts', 'max')` to get the old behavior back, and `updateTag` reserved for the one page where \"eventually\" was never good enough.\n\nIf `cacheTag`, `cacheLife`, and the static-shell-vs-streaming split underneath all of this are new to you, [Next.js Cache Components Explained](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-cache-components-explained) builds that model from the ground up. For the mutation side of this same flow — validating the `FormData`, the CSRF guarantees a Server Action gives you for free, and the `redirect()` gotcha — see [Next.js Server Actions: Mutations & Security](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-server-actions-mutations-security). And if your cache entries are built from a Route Handler's own `GET` rather than a Server Component, [Next.js Route Handlers: GET Stopped Caching in 15](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-route-handlers-caching-streaming) covers the other half of the 15-to-16 caching story.\n\nDid your own `revalidateTag` calls survive the upgrade with the behavior you expected — or did you find this one the hard way?\n\n\u003C!-- related:start -->\n\n## 📚 Read next\n\n- [Next.js proxy.ts Explained (with Cheat Sheet)](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-middleware-to-proxy-network-boundary)\n- [Next.js Route Handlers: GET Stopped Caching in 15 — How to Cache in 16](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-route-handlers-caching-streaming)\n- [Next.js Cache Components Explained (with Cheat Sheet)](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-cache-components-explained)\n\n\u003C!-- related:end -->\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":111,"canonical":461,"description":112},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-updatetag-vs-revalidatetag","01a1749f-19a5-768a-82af-f5f46f9621ae",{"name":464,"part":465,"total":465,"items":466},"Next.js Deep Dive",9,[467,471,475,479,484,488,492,496,500],{"slug":468,"title":469,"publishedAt":470,"readingMinutes":100},"nextjs-weekly-cache-components-explained","Next.js Cache Components Explained (with Cheat Sheet)","2026-09-01T11:24:15.087Z",{"slug":472,"title":473,"publishedAt":474,"readingMinutes":96},"nextjs-weekly-server-actions-mutations-security","Next.js Server Actions: Mutations & Security (Cheat Sheet)","2026-09-08T10:57:00.975Z",{"slug":476,"title":477,"publishedAt":478,"readingMinutes":100},"nextjs-weekly-parallel-intercepting-routes-modals","Next.js Parallel & Intercepting Routes: Modals Done Right","2026-09-15T17:50:47.218Z",{"slug":480,"title":481,"publishedAt":482,"readingMinutes":483},"nextjs-weekly-middleware-to-proxy-network-boundary","Next.js proxy.ts Explained (with Cheat Sheet)","2026-09-22T19:36:08.416Z",12,{"slug":485,"title":486,"publishedAt":487,"readingMinutes":96},"nextjs-weekly-route-handlers-caching-streaming","Next.js Route Handlers: GET Stopped Caching in 15 — How to Cache in 16","2026-09-29T09:16:08.176Z",{"slug":489,"title":490,"publishedAt":491,"readingMinutes":100},"nextjs-weekly-streaming-metadata-head","Next.js Streaming Metadata: Why Your `\u003Chead>` Looks Incomplete","2026-10-06T06:10:31.468Z",{"slug":493,"title":494,"publishedAt":495,"readingMinutes":96},"nextjs-weekly-image-priority-quality-changes","Next.js 16 Image: priority Deprecated, quality Now Allowlisted","2026-10-13T06:10:29.815Z",{"slug":497,"title":498,"publishedAt":499,"readingMinutes":100},"nextjs-weekly-instant-navigations-partial-prefetching","Next.js 16.3 Instant Navigations: How Partial Prefetching Works","2026-10-20T06:10:59.936Z",{"slug":46,"title":111,"publishedAt":114,"readingMinutes":100},{"id":502,"locked":18},"01a1749f-19e0-7370-a75c-b62121083c47",[504],{"id":45,"slug":46,"title":48,"_count":505},{"questions":51},[507],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":509,"questionCount":51},{"questions":51},[]]