[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"search-suggestions":44,"article-nuxt-weekly-cross-request-state-leak":92,"related-nuxt-weekly-cross-request-state-leak":482,"code:ts:true:1x5cac1":497,"code:vue:true:1mqig88":498,"code:ts:true:9gqh7o":499,"code:ts:true:1jc50aw":500,"code:ts:true:1p5ptre":501,"code:ts:true:155udf3":502,"code:ts:true:c1k32s":503,"code:ts:true:hiiis8":504,"comments-01a04446-3f08-701c-8cdc-e5ab5e87086e":505},[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,[45,49,53,57,61,65,69,73,77,81,85,89],{"slug":46,"name":47,"articles":48},"webdev","Webdev",73,{"slug":50,"name":51,"articles":52},"frontend","Frontend",64,{"slug":54,"name":55,"articles":56},"javascript","Javascript",61,{"slug":58,"name":59,"articles":60},"css","Css",29,{"slug":62,"name":63,"articles":64},"tutorial","Tutorial",13,{"slug":66,"name":67,"articles":68},"typescript","Typescript",11,{"slug":70,"name":71,"articles":72},"performance","Performance",9,{"slug":74,"name":75,"articles":76},"react","React",7,{"slug":78,"name":79,"articles":80},"grammar","Grammar",6,{"slug":82,"name":83,"articles":84},"node","Node",5,{"slug":86,"name":87,"articles":88},"programming","Programming",4,{"slug":90,"name":91,"articles":88},"html","Html",{"id":93,"slug":94,"title":95,"subtitle":96,"excerpt":97,"coverUrl":98,"locale":13,"readingMinutes":99,"publishedAt":100,"viewCount":101,"likeCount":19,"commentCount":19,"author":102,"vertical":107,"topic":108,"tags":111,"_count":118,"playground":120,"body":122,"bodyMd":466,"seo":467,"translationGroupId":469,"series":96,"podcastUrl":96,"verticalId":5,"thread":470,"assessments":472,"translations":478,"quiz":480},"01a04446-3f08-701c-8cdc-e5ab5e87086e","nuxt-weekly-cross-request-state-leak","Nuxt useState vs ref(): Why Server State Leaks Across Users",null,"A module-scope ref() in Nuxt is shared by every request that hits your server. Learn why useState isolates state per user, and how to fix the leak.","\u002Fmedia\u002Fcovers\u002Fnuxt-weekly-cross-request-state-leak.png",14,"2026-08-30T11:37:11.335Z",46,{"id":103,"name":104,"username":105,"avatarUrl":96,"headline":106},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":109,"name":110},"nuxt","Nuxt",[112,113,116,117],{"slug":109,"name":110,"color":96},{"slug":114,"name":115,"color":96},"ssr","SSR",{"slug":54,"name":55,"color":96},{"slug":62,"name":63,"color":96},{"assessments":119},1,{"slug":94,"title":121},"Nuxt cross-request state leak — interactive simulation",{"blocks":123,"version":119},[124,128,131,136,139,148,151,154,157,160,174,178,181,187,190,195,198,201,204,207,210,213,216,219,223,226,230,233,236,240,244,247,250,253,256,259,263,266,269,273,277,280,283,286,289,292,300,303,311,314,317,320,323,326,329,332,335,339,342,346,349,353,356,359,362,365,368,371,374,377,409,413,416,424,427,430,433,436,439,442,445,448,451,454,457,460],{"id":125,"html":126,"type":127},"b1","\u003Cp>Two people load your Nuxt storefront within the same second. One of them refreshes their cart and sees someone else&#39;s items in it. There&#39;s no stack trace, no 500, no failed request in the network tab — just the wrong data, silently, for one unlucky user. You spend an hour on localhost trying to reproduce it and can&#39;t, because on localhost you only ever have one request in flight at a time.\u003C\u002Fp>","paragraph",{"id":129,"html":130,"type":127},"b2","\u003Cp>The bug isn&#39;t in your cart logic. It&#39;s in \u003Cem>where\u003C\u002Fem> the state that holds the cart was declared.\u003C\u002Fp>",{"id":132,"html":133,"text":134,"type":135,"level":31},"b3","What you&#39;ll learn","What you'll learn","heading",{"id":137,"html":138,"type":127},"b4","\u003Cp>By the end of this article you&#39;ll be able to:\u003C\u002Fp>",{"id":140,"type":141,"items":142,"ordered":18},"b5","list",[143,144,145,146,147],"Explain why a \u003Ccode>ref()\u003C\u002Fcode> or plain object declared at module scope in Nuxt gets shared by every request your server handles, not just the one that created it","Use \u003Ccode>useState()\u003C\u002Fcode> correctly — what its key does, when to omit it, and what it actually isolates","Recognize the same danger inside Nitro server routes, not just components and composables","Tell the difference between module-scope state that&#39;s dangerous (per-user data) and module-scope state that&#39;s fine (shared caches, constants)","Reach for the right tool — \u003Ccode>useState\u003C\u002Fcode>, \u003Ccode>event.context\u003C\u002Fcode>, or a plain \u003Ccode>ref\u003C\u002Fcode> — for a given piece of state",{"id":149,"html":150,"text":150,"type":135,"level":31},"b6","Who this is for",{"id":152,"html":153,"type":127},"b7","\u003Cp>You&#39;ve built at least one Nuxt page and used a composable. You don&#39;t need prior SSR experience — this article builds the mental model for &quot;what runs where&quot; from the ground up.\u003C\u002Fp>",{"id":155,"html":156,"type":127},"b8","\u003Cp>This article is written against \u003Cstrong>Nuxt 4.5.x\u003C\u002Fstrong> (verified against the \u003Ccode>nuxt\u003C\u002Fcode> package&#39;s release history on npm and GitHub in August 2026; latest patch at the time of writing is \u003Cstrong>4.5.2\u003C\u002Fstrong>, with Nuxt 3 having reached end-of-life on July 31, 2026). Code and directory paths use the Nuxt 4 \u003Ccode>app\u002F\u003C\u002Fcode> convention (\u003Ccode>app\u002Fcomposables\u002F\u003C\u002Fcode>, \u003Ccode>app\u002Fplugins\u002F\u003C\u002Fcode>, \u003Ccode>app\u002Fpages\u002F\u003C\u002Fcode> — \u003Ccode>server\u002F\u003C\u002Fcode> stays at the project root, outside \u003Ccode>app\u002F\u003C\u002Fcode>). If your project still uses the flat Nuxt 3 layout (\u003Ccode>composables\u002F\u003C\u002Fcode>, \u003Ccode>plugins\u002F\u003C\u002Fcode>, \u003Ccode>pages\u002F\u003C\u002Fcode> at the root), the same code works unchanged; only the folder location differs, noted once below.\u003C\u002Fp>",{"id":158,"html":159,"text":159,"type":135,"level":31},"b9","Table of contents",{"id":161,"type":141,"items":162,"ordered":18},"b10",[163,164,165,166,167,168,169,170,171,172,173],"\u003Ca href=\"#the-problem-a-cart-that-isnt-yours\">The problem: a cart that isn&#39;t yours\u003C\u002Fa>","\u003Ca href=\"#the-mental-model-one-process-many-requests\">The mental model: one process, many requests\u003C\u002Fa>","\u003Ca href=\"#stage-1-reproducing-the-leak\">Stage 1: reproducing the leak\u003C\u002Fa>","\u003Ca href=\"#stage-2-what-per-request-actually-means\">Stage 2: what &quot;per request&quot; actually means\u003C\u002Fa>","\u003Ca href=\"#stage-3-fixing-it-with-usestate\">Stage 3: fixing it with useState\u003C\u002Fa>","\u003Ca href=\"#stage-4-the-same-bug-in-a-nitro-server-route\">Stage 4: the same bug in a Nitro server route\u003C\u002Fa>","\u003Ca href=\"#stage-5-what-module-scope-is-actually-fine-for\">Stage 5: what module scope is actually fine for\u003C\u002Fa>","\u003Ca href=\"#edge-cases-and-gotchas\">Edge cases and gotchas\u003C\u002Fa>","\u003Ca href=\"#best-practices-which-tool-for-what\">Best practices: which tool, for what\u003C\u002Fa>","\u003Ca href=\"#faq\">FAQ\u003C\u002Fa>","\u003Ca href=\"#cheat-sheet\">Cheat sheet\u003C\u002Fa>",{"id":175,"html":176,"text":177,"type":135,"level":31},"b11","The problem: a cart that isn&#39;t yours","The problem: a cart that isn't yours",{"id":179,"html":180,"type":127},"b12","\u003Cp>Here&#39;s a composable that looks completely ordinary:\u003C\u002Fp>",{"id":182,"code":183,"type":184,"language":185,"highlight":186},"b13","\u002F\u002F app\u002Fcomposables\u002FuseCart.ts\nimport { ref } from 'vue'\n\n\u002F\u002F Looks like a normal shared store — one ref, imported wherever the cart is needed.\nconst cart = ref\u003C{ id: string; qty: number }[]>([])\n\nexport function useCart() {\n  function add(id: string) {\n    cart.value.push({ id, qty: 1 })\n  }\n  return { cart, add }\n}","code","ts",[],{"id":188,"html":189,"type":127},"b14","\u003Cp>Used from a page:\u003C\u002Fp>",{"id":191,"code":192,"type":184,"language":193,"highlight":194},"b15","\u003Cscript setup lang=\"ts\">\nconst { cart, add } = useCart()\nadd('sku-123')\n\u003C\u002Fscript>","vue",[],{"id":196,"html":197,"type":127},"b16","\u003Cp>On your machine, in one browser tab, this works. Add an item, see it in the cart, refresh, it&#39;s still there for the length of the session. Nothing looks wrong.\u003C\u002Fp>",{"id":199,"html":200,"type":127},"b17","\u003Cp>Now imagine two real users, User A and User B, hitting your server within the same few milliseconds — entirely realistic under normal traffic. \u003Ccode>const cart = ref([])\u003C\u002Fcode> at the top of \u003Ccode>useCart.ts\u003C\u002Fcode> runs \u003Cstrong>once\u003C\u002Fstrong>, the first time Node imports that file at server startup. Every request after that reuses the exact same \u003Ccode>ref\u003C\u002Fcode> object. User A adds an item; for a brief window, User B&#39;s server-rendered HTML can include it. Low traffic hides it. A few hundred concurrent requests, and it stops being rare.\u003C\u002Fp>",{"id":202,"html":203,"text":203,"type":135,"level":31},"b18","The mental model: one process, many requests",{"id":205,"html":206,"type":127},"b19","\u003Cp>\u003Cstrong>The mental model:\u003C\u002Fstrong> a Nuxt server (via Nitro) is one long-running process — or, in serverless, one warm function instance — that services many requests by interleaving them, not one process per request. Every \u003Ccode>await\u003C\u002Fcode> in your server-side code is a point where Node can start working on a \u003Cem>different\u003C\u002Fem> request before yours resumes. Anything you declare at \u003Cstrong>module scope\u003C\u002Fstrong> — outside a component&#39;s \u003Ccode>setup()\u003C\u002Fcode>, outside a composable function&#39;s body, outside \u003Ccode>defineEventHandler\u003C\u002Fcode> — is evaluated exactly once, when the module is first imported, and lives for the lifetime of the process. It is scoped to the \u003Cem>server\u003C\u002Fem>, not to the \u003Cem>request\u003C\u002Fem>.\u003C\u002Fp>",{"id":208,"html":209,"type":127},"b20","\u003Cp>A \u003Ccode>ref()\u003C\u002Fcode> created inside \u003Ccode>setup()\u003C\u002Fcode> or inside a composable function&#39;s body is different: it&#39;s created fresh every time that function runs. But \u003Ccode>useCart()\u003C\u002Fcode>&#39;s \u003Ccode>cart\u003C\u002Fcode> isn&#39;t created inside the function — it&#39;s created at the top of the file, outside \u003Ccode>useCart()\u003C\u002Fcode>, so calling \u003Ccode>useCart()\u003C\u002Fcode> a second time just returns a reference to the \u003Cem>same\u003C\u002Fem> object every time, request after request.\u003C\u002Fp>",{"id":211,"html":212,"type":127},"b21","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> &quot;module scope&quot; and &quot;request scope&quot; are not the same lifetime, and the bug is always a state variable that was written as if they were.\u003C\u002Fp>",{"id":214,"html":215,"text":215,"type":135,"level":31},"b22","Stage 1: reproducing the leak",{"id":217,"html":218,"type":127},"b23","\u003Cp>To see this concretely without waiting for real traffic, simulate two overlapping requests directly:\u003C\u002Fp>",{"id":220,"code":221,"type":184,"language":185,"highlight":222},"b24","\u002F\u002F Simulates two \"requests\" racing against the buggy module-scope cart.\nconst cart: string[] = [] \u002F\u002F module scope — created once\n\nasync function handleRequest(user: string, item: string) {\n  cart.push(item)\n  await new Promise((r) => setTimeout(r, 10)) \u002F\u002F stand-in for async work: a DB call, an await inside render\n  console.log(`${user} sees cart:`, cart) \u002F\u002F both users read the SAME array\n}\n\nhandleRequest('User A', 'sku-A')\nhandleRequest('User B', 'sku-B')",[],{"id":224,"html":225,"type":127},"b25","\u003Cp>Run this and both logs print \u003Ccode>[&#39;sku-A&#39;, &#39;sku-B&#39;]\u003C\u002Fcode>. User B, who never added \u003Ccode>sku-A\u003C\u002Fcode>, sees it anyway — because \u003Ccode>cart\u003C\u002Fcode> was never theirs alone. This is the entire bug in twelve lines, stripped of Nuxt: shared mutable state plus concurrent execution.\u003C\u002Fp>",{"id":227,"html":228,"text":229,"type":135,"level":31},"b26","Stage 2: what &quot;per request&quot; actually means","Stage 2: what \"per request\" actually means",{"id":231,"html":232,"type":127},"b27","\u003Cp>Nuxt does create a \u003Cstrong>fresh Vue app instance per request\u003C\u002Fstrong> on the server — that part is correctly isolated. Component instances, their \u003Ccode>setup()\u003C\u002Fcode> locals, and anything created inside a composable function&#39;s own body are all request-scoped, because the function runs again for every render. The trap is specifically \u003Cstrong>values created outside any function\u003C\u002Fstrong> — they exist before any app instance does.\u003C\u002Fp>",{"id":234,"html":235,"type":127},"b28","\u003Cp>So the fix isn&#39;t &quot;avoid \u003Ccode>ref()\u003C\u002Fcode>&quot; — \u003Ccode>ref()\u003C\u002Fcode> inside \u003Ccode>setup()\u003C\u002Fcode> is fine. The fix is a tool that&#39;s request-scoped \u003Cem>and\u003C\u002Fem> still declared once, at the top of a composable, for convenience. That&#39;s \u003Ccode>useState\u003C\u002Fcode>.\u003C\u002Fp>",{"id":237,"html":238,"text":239,"type":135,"level":31},"b29","Stage 3: fixing it with \u003Ccode>useState\u003C\u002Fcode>","Stage 3: fixing it with useState",{"id":241,"code":242,"type":184,"language":185,"highlight":243},"b30","\u002F\u002F app\u002Fcomposables\u002FuseCart.ts\nexport function useCart() {\n  const cart = useState\u003C{ id: string; qty: number }[]>('cart', () => [])\n\n  function add(id: string) {\n    cart.value.push({ id, qty: 1 })\n  }\n  return { cart, add }\n}",[],{"id":245,"html":246,"type":127},"b31","\u003Cp>Two changes matter. First, \u003Ccode>cart\u003C\u002Fcode> is now created \u003Cstrong>inside\u003C\u002Fstrong> \u003Ccode>useCart()\u003C\u002Fcode> — but \u003Ccode>useState\u003C\u002Fcode> doesn&#39;t just make a new plain \u003Ccode>ref\u003C\u002Fcode> each call; it looks up (or creates) a value keyed \u003Ccode>&#39;cart&#39;\u003C\u002Fcode> inside the \u003Cem>current\u003C\u002Fem> Nuxt app instance&#39;s state, and since each request gets its own app instance, each request gets its own \u003Ccode>&#39;cart&#39;\u003C\u002Fcode> entry. Second, the initializer \u003Ccode>() =&gt; []\u003C\u002Fcode> only runs the first time that key is requested per app instance.\u003C\u002Fp>",{"id":248,"html":249,"type":127},"b32","\u003Cp>Under the hood, \u003Ccode>useState(key, init)\u003C\u002Fcode> stores its value in \u003Ccode>nuxtApp.payload.state[key]\u003C\u002Fcode>. The server serializes that payload into the HTML it sends down; the client reads the same payload during hydration and reuses the value instead of re-running the initializer — no duplicate computation, no flash of different content.\u003C\u002Fp>",{"id":251,"html":252,"type":127},"b33","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> \u003Ccode>useState\u003C\u002Fcode>&#39;s key is the isolation boundary. Two calls to \u003Ccode>useState(&#39;cart&#39;, ...)\u003C\u002Fcode> anywhere in your app — same component, different components, a plugin — return the \u003Cem>same\u003C\u002Fem> reactive value within one request\u002Fapp-instance, and a \u003Cem>different\u003C\u002Fem> value than the same key in a different request. Omit the key and Nuxt auto-generates one from the call site, but an explicit string is worth the extra characters: it&#39;s what you&#39;ll grep for, and it avoids two unrelated composables accidentally colliding on an auto-generated key that happens to match.\u003C\u002Fp>",{"id":254,"html":255,"text":255,"type":135,"level":31},"b34","Stage 4: the same bug in a Nitro server route",{"id":257,"html":258,"type":127},"b35","\u003Cp>The identical mistake happens in \u003Ccode>server\u002Fapi\u002F*.ts\u003C\u002Fcode> files, and it&#39;s easy to miss because Nitro handlers look request-scoped even when they aren&#39;t:\u003C\u002Fp>",{"id":260,"code":261,"type":184,"language":185,"highlight":262},"b36","\u002F\u002F server\u002Fapi\u002Frate-limit.ts — BUGGY\nconst requestCounts = new Map\u003Cstring, number>() \u002F\u002F module scope: one Map for the whole process\n\nexport default defineEventHandler((event) => {\n  const ip = getRequestIP(event) ?? 'unknown'\n  const count = (requestCounts.get(ip) ?? 0) + 1\n  requestCounts.set(ip, count)\n  return { requests: count }\n})",[],{"id":264,"html":265,"type":127},"b37","\u003Cp>This particular case is actually \u003Cem>intentional\u003C\u002Fem> module scope — a rate limiter needs one shared counter across all requests, by design. The \u003Ccode>Map\u003C\u002Fcode> itself isn&#39;t the bug; sharing \u003Cstrong>one user&#39;s identifiable data\u003C\u002Fstrong> (a cart, a session, a &quot;current user&quot; object) this way is. If this file instead cached a full user profile fetched during a request and reused it for the next caller regardless of who they were, that&#39;s the same leak, just in \u003Ccode>server\u002F\u003C\u002Fcode> instead of \u003Ccode>app\u002F\u003C\u002Fcode>.\u003C\u002Fp>",{"id":267,"html":268,"type":127},"b38","\u003Cp>For per-request, server-only data — passed between middleware and handler, never meant to reach the client — use \u003Ccode>event.context\u003C\u002Fcode> instead:\u003C\u002Fp>",{"id":270,"code":271,"type":184,"language":185,"highlight":272},"b39","\u002F\u002F server\u002Fmiddleware\u002Fauth.ts\nexport default defineEventHandler((event) => {\n  event.context.user = verifyToken(getHeader(event, 'authorization'))\n})",[],{"id":274,"code":275,"type":184,"language":185,"highlight":276},"b40","\u002F\u002F server\u002Fapi\u002Fprofile.ts\nexport default defineEventHandler((event) => {\n  return { name: event.context.user?.name }\n})",[],{"id":278,"html":279,"type":127},"b41","\u003Cp>\u003Ccode>event\u003C\u002Fcode> is created fresh per request by Nitro, so anything on \u003Ccode>event.context\u003C\u002Fcode> is automatically request-scoped — no key, no leak, and unlike \u003Ccode>useState\u003C\u002Fcode> it never gets serialized to the client.\u003C\u002Fp>",{"id":281,"html":282,"text":282,"type":135,"level":31},"b42","Stage 5: what module scope is actually fine for",{"id":284,"html":285,"type":127},"b43","\u003Cp>Not everything at the top of a file is a bug. Module scope is right for anything that&#39;s the same for every request: \u003Cstrong>constants\u003C\u002Fstrong> (configuration, compiled regexes, a parsed schema), \u003Cstrong>stateless utilities\u003C\u002Fstrong> (pure functions, nothing to leak), \u003Cstrong>process-wide state by design\u003C\u002Fstrong> (a rate limiter&#39;s counters, a cache keyed by \u003Cem>input\u003C\u002Fem> — \u003Ccode>cache.get(productId)\u003C\u002Fcode> is fine, \u003Ccode>cache.get(&#39;currentUser&#39;)\u003C\u002Fcode> is not), and \u003Cstrong>connections\u002Fclients\u003C\u002Fstrong> (a database pool exists precisely to be reused across requests).\u003C\u002Fp>",{"id":287,"html":288,"type":127},"b44","\u003Cp>The dividing line isn&#39;t &quot;was it declared at module scope&quot; — it&#39;s &quot;does the value hold one specific user&#39;s or request&#39;s data.&quot; A rate-limit \u003Ccode>Map\u003C\u002Fcode> keyed by IP is process-wide by design and correct. A \u003Ccode>ref([])\u003C\u002Fcode> meant to be &quot;the current user&#39;s cart&quot; is process-wide by accident and wrong.\u003C\u002Fp>",{"id":290,"html":291,"text":291,"type":135,"level":31},"b45","Edge cases and gotchas",{"id":293,"type":141,"items":294,"ordered":18},"b46",[295,296,297,298,299],"\u003Cstrong>Serverless doesn&#39;t save you.\u003C\u002Fstrong> Most providers reuse (&quot;warm&quot;) a function container across several invocations for performance, so a module-scope leak can still show up there — just more rarely than on a persistent Node server.","\u003Cstrong>\u003Ccode>useState\u003C\u002Fcode>&#39;s value must be serializable.\u003C\u002Fstrong> The payload uses Nuxt&#39;s \u003Ccode>devalue\u003C\u002Fcode>-based serializer, which handles plain objects, arrays, \u003Ccode>Map\u003C\u002Fcode>, \u003Ccode>Set\u003C\u002Fcode>, and \u003Ccode>Date\u003C\u002Fcode> — but not functions or class instances with methods. Store data, not behavior.","\u003Cstrong>\u003Ccode>useState\u003C\u002Fcode> on the client is per browser tab, not per user across tabs.\u003C\u002Fstrong> After hydration, calls to \u003Ccode>useState(&#39;cart&#39;, ...)\u003C\u002Fcode> anywhere in your app return the same client-side ref for that page load — correct, because a tab belongs to exactly one user. This is a different, safe kind of &quot;shared&quot; than the server-side leak above.","\u003Cstrong>Prerendered (SSG) builds can race too.\u003C\u002Fstrong> \u003Ccode>nuxi generate\u003C\u002Fcode> renders multiple routes in parallel; a module-scope value mutated during one route&#39;s render can bleed into another route&#39;s output — a build-time version of the same bug.","\u003Cstrong>The Nuxt 3 flat layout works identically.\u003C\u002Fstrong> On \u003Ccode>composables\u002F\u003C\u002Fcode>, \u003Ccode>plugins\u002F\u003C\u002Fcode>, \u003Ccode>server\u002F\u003C\u002Fcode> at the project root (no top-level \u003Ccode>app\u002F\u003C\u002Fcode>), everything above holds unchanged — only the folder path differs.",{"id":301,"html":302,"text":302,"type":135,"level":31},"b47","Best practices: which tool, for what",{"id":304,"type":141,"items":305,"ordered":18},"b48",[306,307,308,309,310],"\u003Cstrong>\u003Ccode>useState(key, init)\u003C\u002Fcode>\u003C\u002Fstrong> — any reactive value that&#39;s computed during SSR, needs to survive hydration without recomputation, and is specific to the current request\u002Fuser. Covers most &quot;shared state&quot; you&#39;d otherwise put in a module-scope \u003Ccode>ref\u003C\u002Fcode>.","\u003Cstrong>\u003Ccode>event.context\u003C\u002Fcode>\u003C\u002Fstrong> — per-request data only the server needs (an authenticated user object, a parsed token), never serialized to the client.","\u003Cstrong>A plain \u003Ccode>ref()\u003C\u002Fcode> inside \u003Ccode>setup()\u003C\u002Fcode>\u003C\u002Fstrong> — component-local state with no cross-request concern. Most \u003Ccode>ref\u003C\u002Fcode>s in a codebase are exactly this, and never the problem.","\u003Cstrong>Module scope\u003C\u002Fstrong> — constants, pure utilities, state genuinely meant to be shared by the whole process (caches keyed by input, pools, rate limiters). Never a specific user&#39;s data.","\u003Cstrong>Audit rule of thumb:\u003C\u002Fstrong> grep composables and Nitro handlers for \u003Ccode>ref(\u003C\u002Fcode>, \u003Ccode>reactive(\u003C\u002Fcode>, or a bare object\u002Farray literal sitting \u003Cem>outside\u003C\u002Fem> any function. Each hit is either module scope done correctly, or the leak — decide deliberately.",{"id":312,"html":313,"type":127},"b49","\u003C!-- playground:start -->",{"id":315,"html":316,"text":316,"type":135,"level":31},"b50","🎮 Try it yourself",{"id":318,"html":319,"type":127},"b51","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnuxt-weekly-cross-request-state-leak\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":321,"html":322,"type":127},"b52","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":324,"html":325,"type":127},"b53","\u003C!-- playground:end -->",{"id":327,"html":328,"text":328,"type":135,"level":31},"b54","Edge case worth simulating",{"id":330,"html":331,"type":127},"b55","\u003Cp>Because this bug depends on real concurrency, it&#39;s genuinely hard to see by reading code alone — the interactive simulation below runs two &quot;requests&quot; side by side under both storage strategies so you can watch the leak happen and disappear.\u003C\u002Fp>",{"id":333,"html":334,"text":334,"type":135,"level":31},"b56","FAQ",{"id":336,"html":337,"text":338,"type":135,"level":43},"b57","Why doesn&#39;t this bug show up when I test locally?","Why doesn't this bug show up when I test locally?",{"id":340,"html":341,"type":127},"b58","\u003Cp>Because your dev server usually handles one request from one browser tab at a time. The interleaving that causes the leak needs two requests genuinely overlapping — real concurrent traffic, or a deliberately staggered test like Stage 1&#39;s, not a single tab refreshed twice in a row.\u003C\u002Fp>",{"id":343,"html":344,"text":345,"type":135,"level":43},"b59","Is \u003Ccode>useState\u003C\u002Fcode> the same thing as a Pinia store?","Is useState the same thing as a Pinia store?",{"id":347,"html":348,"type":127},"b60","\u003Cp>Related, not identical. \u003Ccode>useState\u003C\u002Fcode> is Nuxt&#39;s built-in, SSR-safe primitive for a single value with automatic payload serialization. Pinia is a full store library (actions, getters, devtools) that&#39;s SSR-safe the same way — each store instance is created per app instance, not at module scope, so it doesn&#39;t have this bug either. Use \u003Ccode>useState\u003C\u002Fcode> for a handful of values; reach for Pinia once there&#39;s real store logic to organize.\u003C\u002Fp>",{"id":350,"html":351,"text":352,"type":135,"level":43},"b61","Does \u003Ccode>useAsyncData\u003C\u002Fcode> or \u003Ccode>useFetch\u003C\u002Fcode> have the same leak risk?","Does useAsyncData or useFetch have the same leak risk?",{"id":354,"html":355,"type":127},"b62","\u003Cp>Not by default — both key their result through the same per-app-instance payload mechanism \u003Ccode>useState\u003C\u002Fcode> uses internally, so a correctly-keyed call is isolated per request. The risk reappears only if you cache their \u003Cem>result\u003C\u002Fem> yourself in a module-scope variable outside the composable.\u003C\u002Fp>",{"id":357,"html":358,"text":358,"type":135,"level":43},"b63","What if I genuinely want one value shared by every user, like a live visitor count?",{"id":360,"html":361,"type":127},"b64","\u003Cp>That&#39;s legitimate module-scope state (Stage 5) — declare and mutate it directly, and skip \u003Ccode>useState\u003C\u002Fcode>, since \u003Ccode>useState\u003C\u002Fcode> exists specifically for \u003Cem>per-request\u003C\u002Fem> isolation, the opposite of what you want here.\u003C\u002Fp>",{"id":363,"html":364,"text":364,"type":135,"level":43},"b65","Does this affect Nuxt 2?",{"id":366,"html":367,"type":127},"b66","\u003Cp>Nuxt 2 had no \u003Ccode>useState\u003C\u002Fcode>; the same trap existed around the Vuex store instead. Nuxt 2 reached end-of-life years ago — treat any lingering \u003Ccode>@nuxtjs\u002Fcomposition-api\u003C\u002Fcode> code as a migration to finish, not a pattern to extend.\u003C\u002Fp>",{"id":369,"html":370,"text":370,"type":135,"level":43},"b67","How do I catch this in code review instead of production?",{"id":372,"html":373,"type":127},"b68","\u003Cp>Grep \u003Ccode>composables\u002F\u003C\u002Fcode>, \u003Ccode>plugins\u002F\u003C\u002Fcode>, and \u003Ccode>server\u002F\u003C\u002Fcode> for module-scope \u003Ccode>ref(\u003C\u002Fcode>, \u003Ccode>reactive(\u003C\u002Fcode>, or object\u002Farray literals, and ask of each: &quot;if two users&#39; requests both touched this at once, would that be correct?&quot; A rate limiter&#39;s \u003Ccode>Map\u003C\u002Fcode>: yes. Anything holding one user&#39;s cart, profile, or session: no.\u003C\u002Fp>",{"id":375,"html":376,"text":376,"type":135,"level":31},"b69","Cheat sheet",{"id":378,"head":379,"rows":383,"type":408},"b70",[380,381,382],"Situation","Use","Why",[384,388,392,396,400,404],[385,386,387],"Reactive value computed during SSR, must survive hydration, per-user","\u003Ccode>useState(&#39;key&#39;, () =&gt; init)\u003C\u002Fcode>","Stored in the per-request payload; auto-serialized to the client",[389,390,391],"Server-only data passed between middleware and handler","\u003Ccode>event.context.foo = …\u003C\u002Fcode>","Per-request by construction; never serialized to the client",[393,394,395],"Component-local state, no SSR\u002Fhydration concern","\u003Ccode>ref()\u003C\u002Fcode> inside \u003Ccode>setup()\u003C\u002Fcode>","Created fresh every time the function runs",[397,398,399],"Cache keyed by input, same for every user (e.g. \u003Ccode>cache.get(id)\u003C\u002Fcode>)","Module-scope \u003Ccode>Map\u003C\u002Fcode>\u002Fobject","Correct use of process lifetime — nothing user-specific in it",[401,402,403],"Rate limiter, connection pool, compiled config","Module-scope value","Meant to persist across every request by design",[405,406,407],"A value one user&#39;s request wrote and a different user&#39;s request would wrongly read","\u003Cstrong>Never\u003C\u002Fstrong> module scope","This is the leak — move it to \u003Ccode>useState\u003C\u002Fcode> or \u003Ccode>event.context\u003C\u002Fcode>","table",{"id":410,"code":411,"type":184,"language":185,"highlight":412},"b71","\u002F\u002F The fix, side by side:\n\u002F\u002F ❌ Module scope — one instance for the whole server process\nconst cart = ref\u003CItem[]>([])\n\n\u002F\u002F ✅ useState — one instance per request\u002Fapp-instance, keyed explicitly\nexport function useCart() {\n  const cart = useState\u003CItem[]>('cart', () => [])\n  return { cart }\n}",[],{"id":414,"html":415,"text":415,"type":135,"level":31},"b72","Key takeaways",{"id":417,"type":141,"items":418,"ordered":18},"b73",[419,420,421,422,423],"Anything declared \u003Cstrong>outside\u003C\u002Fstrong> a component&#39;s \u003Ccode>setup()\u003C\u002Fcode> or a composable&#39;s function body runs once, when the server process starts — not once per request.","Nuxt does create a fresh app instance per request; the bug is state that exists \u003Cem>before\u003C\u002Fem> any app instance does.","\u003Ccode>useState(key, init)\u003C\u002Fcode> is the fix: it&#39;s keyed per app instance, so each request gets its own isolated value, and it survives hydration through the SSR payload.","The same trap exists in Nitro server routes (\u003Ccode>server\u002Fapi\u003C\u002Fcode>, \u003Ccode>server\u002Fmiddleware\u003C\u002Fcode>) — use \u003Ccode>event.context\u003C\u002Fcode> there for per-request, server-only data.","Module scope isn&#39;t wrong by default — it&#39;s wrong specifically when it holds one user&#39;s data. Caches keyed by input, rate limiters, and connection pools are correct uses of it.",{"id":425,"html":426,"type":127},"b74","\u003C!-- quiz:start -->",{"id":428,"html":429,"text":429,"type":135,"level":31},"b75","🧠 Test yourself",{"id":431,"html":432,"type":127},"b76","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnuxt-weekly-cross-request-state-leak\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":434,"html":435,"type":127},"b77","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":437,"html":438,"type":127},"b78","\u003C!-- quiz:end -->",{"id":440,"html":441,"text":441,"type":135,"level":31},"b79","Back to that cart",{"id":443,"html":444,"type":127},"b80","\u003Cp>The cart that showed the wrong items wasn&#39;t broken because of a race condition in the checkout logic, or a database read gone stale — it was broken because \u003Ccode>const cart = ref([])\u003C\u002Fcode> was written one scope too high. Move it inside \u003Ccode>useCart()\u003C\u002Fcode> behind \u003Ccode>useState(&#39;cart&#39;, () =&gt; [])\u003C\u002Fcode>, and the exact same component code stops being a coin flip under real traffic.\u003C\u002Fp>",{"id":446,"html":447,"type":127},"b81","\u003Cp>Next Sunday&#39;s episode picks up where &quot;per-request&quot; leaves off: how \u003Ccode>useAsyncData\u003C\u002Fcode> and \u003Ccode>useFetch\u003C\u002Fcode> decide when two calls are the \u003Cem>same\u003C\u002Fem> request and dedupe them, and when that&#39;s the bug instead of the fix.\u003C\u002Fp>",{"id":449,"html":450,"type":127},"b82","\u003Cp>Has a &quot;how is this even possible&quot; production bug ever turned out to be state declared at the wrong scope? What gave it away?\u003C\u002Fp>",{"id":452,"type":453},"b83","divider",{"id":455,"html":456,"type":127},"b84","\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":458,"html":459,"type":127},"b85","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":461,"type":141,"items":462,"ordered":18},"b86",[463,464,465],"⭐ \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>","Two people load your Nuxt storefront within the same second. One of them refreshes their cart and sees someone else's items in it. There's no stack trace, no 500, no failed request in the network tab — just the wrong data, silently, for one unlucky user. You spend an hour on localhost trying to reproduce it and can't, because on localhost you only ever have one request in flight at a time.\n\nThe bug isn't in your cart logic. It's in *where* the state that holds the cart was declared.\n\n## What you'll learn\n\nBy the end of this article you'll be able to:\n\n- Explain why a `ref()` or plain object declared at module scope in Nuxt gets shared by every request your server handles, not just the one that created it\n- Use `useState()` correctly — what its key does, when to omit it, and what it actually isolates\n- Recognize the same danger inside Nitro server routes, not just components and composables\n- Tell the difference between module-scope state that's dangerous (per-user data) and module-scope state that's fine (shared caches, constants)\n- Reach for the right tool — `useState`, `event.context`, or a plain `ref` — for a given piece of state\n\n## Who this is for\n\nYou've built at least one Nuxt page and used a composable. You don't need prior SSR experience — this article builds the mental model for \"what runs where\" from the ground up.\n\nThis article is written against **Nuxt 4.5.x** (verified against the `nuxt` package's release history on npm and GitHub in August 2026; latest patch at the time of writing is **4.5.2**, with Nuxt 3 having reached end-of-life on July 31, 2026). Code and directory paths use the Nuxt 4 `app\u002F` convention (`app\u002Fcomposables\u002F`, `app\u002Fplugins\u002F`, `app\u002Fpages\u002F` — `server\u002F` stays at the project root, outside `app\u002F`). If your project still uses the flat Nuxt 3 layout (`composables\u002F`, `plugins\u002F`, `pages\u002F` at the root), the same code works unchanged; only the folder location differs, noted once below.\n\n## Table of contents\n\n- [The problem: a cart that isn't yours](#the-problem-a-cart-that-isnt-yours)\n- [The mental model: one process, many requests](#the-mental-model-one-process-many-requests)\n- [Stage 1: reproducing the leak](#stage-1-reproducing-the-leak)\n- [Stage 2: what \"per request\" actually means](#stage-2-what-per-request-actually-means)\n- [Stage 3: fixing it with useState](#stage-3-fixing-it-with-usestate)\n- [Stage 4: the same bug in a Nitro server route](#stage-4-the-same-bug-in-a-nitro-server-route)\n- [Stage 5: what module scope is actually fine for](#stage-5-what-module-scope-is-actually-fine-for)\n- [Edge cases and gotchas](#edge-cases-and-gotchas)\n- [Best practices: which tool, for what](#best-practices-which-tool-for-what)\n- [FAQ](#faq)\n- [Cheat sheet](#cheat-sheet)\n\n## The problem: a cart that isn't yours\n\nHere's a composable that looks completely ordinary:\n\n```ts\n\u002F\u002F app\u002Fcomposables\u002FuseCart.ts\nimport { ref } from 'vue'\n\n\u002F\u002F Looks like a normal shared store — one ref, imported wherever the cart is needed.\nconst cart = ref\u003C{ id: string; qty: number }[]>([])\n\nexport function useCart() {\n  function add(id: string) {\n    cart.value.push({ id, qty: 1 })\n  }\n  return { cart, add }\n}\n```\n\nUsed from a page:\n\n```vue\n\u003Cscript setup lang=\"ts\">\nconst { cart, add } = useCart()\nadd('sku-123')\n\u003C\u002Fscript>\n```\n\nOn your machine, in one browser tab, this works. Add an item, see it in the cart, refresh, it's still there for the length of the session. Nothing looks wrong.\n\nNow imagine two real users, User A and User B, hitting your server within the same few milliseconds — entirely realistic under normal traffic. `const cart = ref([])` at the top of `useCart.ts` runs **once**, the first time Node imports that file at server startup. Every request after that reuses the exact same `ref` object. User A adds an item; for a brief window, User B's server-rendered HTML can include it. Low traffic hides it. A few hundred concurrent requests, and it stops being rare.\n\n## The mental model: one process, many requests\n\n**The mental model:** a Nuxt server (via Nitro) is one long-running process — or, in serverless, one warm function instance — that services many requests by interleaving them, not one process per request. Every `await` in your server-side code is a point where Node can start working on a *different* request before yours resumes. Anything you declare at **module scope** — outside a component's `setup()`, outside a composable function's body, outside `defineEventHandler` — is evaluated exactly once, when the module is first imported, and lives for the lifetime of the process. It is scoped to the *server*, not to the *request*.\n\nA `ref()` created inside `setup()` or inside a composable function's body is different: it's created fresh every time that function runs. But `useCart()`'s `cart` isn't created inside the function — it's created at the top of the file, outside `useCart()`, so calling `useCart()` a second time just returns a reference to the *same* object every time, request after request.\n\n**Key concept:** \"module scope\" and \"request scope\" are not the same lifetime, and the bug is always a state variable that was written as if they were.\n\n## Stage 1: reproducing the leak\n\nTo see this concretely without waiting for real traffic, simulate two overlapping requests directly:\n\n```ts\n\u002F\u002F Simulates two \"requests\" racing against the buggy module-scope cart.\nconst cart: string[] = [] \u002F\u002F module scope — created once\n\nasync function handleRequest(user: string, item: string) {\n  cart.push(item)\n  await new Promise((r) => setTimeout(r, 10)) \u002F\u002F stand-in for async work: a DB call, an await inside render\n  console.log(`${user} sees cart:`, cart) \u002F\u002F both users read the SAME array\n}\n\nhandleRequest('User A', 'sku-A')\nhandleRequest('User B', 'sku-B')\n```\n\nRun this and both logs print `['sku-A', 'sku-B']`. User B, who never added `sku-A`, sees it anyway — because `cart` was never theirs alone. This is the entire bug in twelve lines, stripped of Nuxt: shared mutable state plus concurrent execution.\n\n## Stage 2: what \"per request\" actually means\n\nNuxt does create a **fresh Vue app instance per request** on the server — that part is correctly isolated. Component instances, their `setup()` locals, and anything created inside a composable function's own body are all request-scoped, because the function runs again for every render. The trap is specifically **values created outside any function** — they exist before any app instance does.\n\nSo the fix isn't \"avoid `ref()`\" — `ref()` inside `setup()` is fine. The fix is a tool that's request-scoped *and* still declared once, at the top of a composable, for convenience. That's `useState`.\n\n## Stage 3: fixing it with `useState`\n\n```ts\n\u002F\u002F app\u002Fcomposables\u002FuseCart.ts\nexport function useCart() {\n  const cart = useState\u003C{ id: string; qty: number }[]>('cart', () => [])\n\n  function add(id: string) {\n    cart.value.push({ id, qty: 1 })\n  }\n  return { cart, add }\n}\n```\n\nTwo changes matter. First, `cart` is now created **inside** `useCart()` — but `useState` doesn't just make a new plain `ref` each call; it looks up (or creates) a value keyed `'cart'` inside the *current* Nuxt app instance's state, and since each request gets its own app instance, each request gets its own `'cart'` entry. Second, the initializer `() => []` only runs the first time that key is requested per app instance.\n\nUnder the hood, `useState(key, init)` stores its value in `nuxtApp.payload.state[key]`. The server serializes that payload into the HTML it sends down; the client reads the same payload during hydration and reuses the value instead of re-running the initializer — no duplicate computation, no flash of different content.\n\n**Key concept:** `useState`'s key is the isolation boundary. Two calls to `useState('cart', ...)` anywhere in your app — same component, different components, a plugin — return the *same* reactive value within one request\u002Fapp-instance, and a *different* value than the same key in a different request. Omit the key and Nuxt auto-generates one from the call site, but an explicit string is worth the extra characters: it's what you'll grep for, and it avoids two unrelated composables accidentally colliding on an auto-generated key that happens to match.\n\n## Stage 4: the same bug in a Nitro server route\n\nThe identical mistake happens in `server\u002Fapi\u002F*.ts` files, and it's easy to miss because Nitro handlers look request-scoped even when they aren't:\n\n```ts\n\u002F\u002F server\u002Fapi\u002Frate-limit.ts — BUGGY\nconst requestCounts = new Map\u003Cstring, number>() \u002F\u002F module scope: one Map for the whole process\n\nexport default defineEventHandler((event) => {\n  const ip = getRequestIP(event) ?? 'unknown'\n  const count = (requestCounts.get(ip) ?? 0) + 1\n  requestCounts.set(ip, count)\n  return { requests: count }\n})\n```\n\nThis particular case is actually *intentional* module scope — a rate limiter needs one shared counter across all requests, by design. The `Map` itself isn't the bug; sharing **one user's identifiable data** (a cart, a session, a \"current user\" object) this way is. If this file instead cached a full user profile fetched during a request and reused it for the next caller regardless of who they were, that's the same leak, just in `server\u002F` instead of `app\u002F`.\n\nFor per-request, server-only data — passed between middleware and handler, never meant to reach the client — use `event.context` instead:\n\n```ts\n\u002F\u002F server\u002Fmiddleware\u002Fauth.ts\nexport default defineEventHandler((event) => {\n  event.context.user = verifyToken(getHeader(event, 'authorization'))\n})\n```\n\n```ts\n\u002F\u002F server\u002Fapi\u002Fprofile.ts\nexport default defineEventHandler((event) => {\n  return { name: event.context.user?.name }\n})\n```\n\n`event` is created fresh per request by Nitro, so anything on `event.context` is automatically request-scoped — no key, no leak, and unlike `useState` it never gets serialized to the client.\n\n## Stage 5: what module scope is actually fine for\n\nNot everything at the top of a file is a bug. Module scope is right for anything that's the same for every request: **constants** (configuration, compiled regexes, a parsed schema), **stateless utilities** (pure functions, nothing to leak), **process-wide state by design** (a rate limiter's counters, a cache keyed by *input* — `cache.get(productId)` is fine, `cache.get('currentUser')` is not), and **connections\u002Fclients** (a database pool exists precisely to be reused across requests).\n\nThe dividing line isn't \"was it declared at module scope\" — it's \"does the value hold one specific user's or request's data.\" A rate-limit `Map` keyed by IP is process-wide by design and correct. A `ref([])` meant to be \"the current user's cart\" is process-wide by accident and wrong.\n\n## Edge cases and gotchas\n\n- **Serverless doesn't save you.** Most providers reuse (\"warm\") a function container across several invocations for performance, so a module-scope leak can still show up there — just more rarely than on a persistent Node server.\n- **`useState`'s value must be serializable.** The payload uses Nuxt's `devalue`-based serializer, which handles plain objects, arrays, `Map`, `Set`, and `Date` — but not functions or class instances with methods. Store data, not behavior.\n- **`useState` on the client is per browser tab, not per user across tabs.** After hydration, calls to `useState('cart', ...)` anywhere in your app return the same client-side ref for that page load — correct, because a tab belongs to exactly one user. This is a different, safe kind of \"shared\" than the server-side leak above.\n- **Prerendered (SSG) builds can race too.** `nuxi generate` renders multiple routes in parallel; a module-scope value mutated during one route's render can bleed into another route's output — a build-time version of the same bug.\n- **The Nuxt 3 flat layout works identically.** On `composables\u002F`, `plugins\u002F`, `server\u002F` at the project root (no top-level `app\u002F`), everything above holds unchanged — only the folder path differs.\n\n## Best practices: which tool, for what\n\n- **`useState(key, init)`** — any reactive value that's computed during SSR, needs to survive hydration without recomputation, and is specific to the current request\u002Fuser. Covers most \"shared state\" you'd otherwise put in a module-scope `ref`.\n- **`event.context`** — per-request data only the server needs (an authenticated user object, a parsed token), never serialized to the client.\n- **A plain `ref()` inside `setup()`** — component-local state with no cross-request concern. Most `ref`s in a codebase are exactly this, and never the problem.\n- **Module scope** — constants, pure utilities, state genuinely meant to be shared by the whole process (caches keyed by input, pools, rate limiters). Never a specific user's data.\n- **Audit rule of thumb:** grep composables and Nitro handlers for `ref(`, `reactive(`, or a bare object\u002Farray literal sitting *outside* any function. Each hit is either module scope done correctly, or the leak — decide deliberately.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnuxt-weekly-cross-request-state-leak\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## Edge case worth simulating\n\nBecause this bug depends on real concurrency, it's genuinely hard to see by reading code alone — the interactive simulation below runs two \"requests\" side by side under both storage strategies so you can watch the leak happen and disappear.\n\n## FAQ\n\n### Why doesn't this bug show up when I test locally?\n\nBecause your dev server usually handles one request from one browser tab at a time. The interleaving that causes the leak needs two requests genuinely overlapping — real concurrent traffic, or a deliberately staggered test like Stage 1's, not a single tab refreshed twice in a row.\n\n### Is `useState` the same thing as a Pinia store?\n\nRelated, not identical. `useState` is Nuxt's built-in, SSR-safe primitive for a single value with automatic payload serialization. Pinia is a full store library (actions, getters, devtools) that's SSR-safe the same way — each store instance is created per app instance, not at module scope, so it doesn't have this bug either. Use `useState` for a handful of values; reach for Pinia once there's real store logic to organize.\n\n### Does `useAsyncData` or `useFetch` have the same leak risk?\n\nNot by default — both key their result through the same per-app-instance payload mechanism `useState` uses internally, so a correctly-keyed call is isolated per request. The risk reappears only if you cache their *result* yourself in a module-scope variable outside the composable.\n\n### What if I genuinely want one value shared by every user, like a live visitor count?\n\nThat's legitimate module-scope state (Stage 5) — declare and mutate it directly, and skip `useState`, since `useState` exists specifically for *per-request* isolation, the opposite of what you want here.\n\n### Does this affect Nuxt 2?\n\nNuxt 2 had no `useState`; the same trap existed around the Vuex store instead. Nuxt 2 reached end-of-life years ago — treat any lingering `@nuxtjs\u002Fcomposition-api` code as a migration to finish, not a pattern to extend.\n\n### How do I catch this in code review instead of production?\n\nGrep `composables\u002F`, `plugins\u002F`, and `server\u002F` for module-scope `ref(`, `reactive(`, or object\u002Farray literals, and ask of each: \"if two users' requests both touched this at once, would that be correct?\" A rate limiter's `Map`: yes. Anything holding one user's cart, profile, or session: no.\n\n## Cheat sheet\n\n| Situation | Use | Why |\n| --- | --- | --- |\n| Reactive value computed during SSR, must survive hydration, per-user | `useState('key', () => init)` | Stored in the per-request payload; auto-serialized to the client |\n| Server-only data passed between middleware and handler | `event.context.foo = …` | Per-request by construction; never serialized to the client |\n| Component-local state, no SSR\u002Fhydration concern | `ref()` inside `setup()` | Created fresh every time the function runs |\n| Cache keyed by input, same for every user (e.g. `cache.get(id)`) | Module-scope `Map`\u002Fobject | Correct use of process lifetime — nothing user-specific in it |\n| Rate limiter, connection pool, compiled config | Module-scope value | Meant to persist across every request by design |\n| A value one user's request wrote and a different user's request would wrongly read | **Never** module scope | This is the leak — move it to `useState` or `event.context` |\n\n```ts\n\u002F\u002F The fix, side by side:\n\u002F\u002F ❌ Module scope — one instance for the whole server process\nconst cart = ref\u003CItem[]>([])\n\n\u002F\u002F ✅ useState — one instance per request\u002Fapp-instance, keyed explicitly\nexport function useCart() {\n  const cart = useState\u003CItem[]>('cart', () => [])\n  return { cart }\n}\n```\n\n## Key takeaways\n\n- Anything declared **outside** a component's `setup()` or a composable's function body runs once, when the server process starts — not once per request.\n- Nuxt does create a fresh app instance per request; the bug is state that exists *before* any app instance does.\n- `useState(key, init)` is the fix: it's keyed per app instance, so each request gets its own isolated value, and it survives hydration through the SSR payload.\n- The same trap exists in Nitro server routes (`server\u002Fapi`, `server\u002Fmiddleware`) — use `event.context` there for per-request, server-only data.\n- Module scope isn't wrong by default — it's wrong specifically when it holds one user's data. Caches keyed by input, rate limiters, and connection pools are correct uses of it.\n\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnuxt-weekly-cross-request-state-leak\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\n## Back to that cart\n\nThe cart that showed the wrong items wasn't broken because of a race condition in the checkout logic, or a database read gone stale — it was broken because `const cart = ref([])` was written one scope too high. Move it inside `useCart()` behind `useState('cart', () => [])`, and the exact same component code stops being a coin flip under real traffic.\n\nNext Sunday's episode picks up where \"per-request\" leaves off: how `useAsyncData` and `useFetch` decide when two calls are the *same* request and dedupe them, and when that's the bug instead of the fix.\n\nHas a \"how is this even possible\" production bug ever turned out to be state declared at the wrong scope? What gave it away?\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":95,"canonical":468,"description":97},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnuxt-weekly-cross-request-state-leak","01a04446-3f09-7257-9148-a6b36808c8f7",{"id":471,"locked":18},"01a04446-3f4f-708a-a3cc-1aab8f3161ce",[473],{"id":474,"slug":94,"title":475,"_count":476},"01a04446-3f81-726a-bf3e-02e284c2dc13","Nuxt Cross-Request State Leaks",{"questions":477},8,[479],{"locale":13,"slug":94},{"id":474,"slug":94,"title":475,"_count":481,"questionCount":477},{"questions":477},{"items":483,"meta":496},[484],{"id":93,"slug":94,"title":95,"subtitle":96,"excerpt":97,"coverUrl":98,"locale":13,"readingMinutes":99,"publishedAt":100,"viewCount":485,"likeCount":19,"commentCount":19,"author":486,"vertical":487,"topic":488,"tags":489,"_count":494,"playground":495,"hasQuiz":17,"hasPlayground":17},47,{"id":103,"name":104,"username":105,"avatarUrl":96,"headline":106},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":109,"name":110},[490,491,492,493],{"slug":109,"name":110,"color":96},{"slug":114,"name":115,"color":96},{"slug":54,"name":55,"color":96},{"slug":62,"name":63,"color":96},{"assessments":119},{"slug":94},{"page":119,"perPage":76,"total":119,"totalPages":119},"\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F app\u002Fcomposables\u002FuseCart.ts\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">import\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> ref \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'vue'\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-cmnt\">\u002F\u002F Looks like a normal shared store — one ref, imported wherever the cart is needed.\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> cart \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> ref\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> id\u003Cspan class=\"shj-type\">: string\u003C\u002Fspan>; qty\u003Cspan class=\"shj-type\">: number\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">useCart\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">add\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>id\u003Cspan class=\"shj-type\">: string\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    cart\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>value\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">push\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> id\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> qty\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-num\">1\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> cart\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> add \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-vue shj-multiline\" data-lang=\"vue\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">script\u003C\u002Fspan> \u003Cspan class=\"shj-class\">setup\u003C\u002Fspan> \u003Cspan class=\"shj-class\">lang\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"ts\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> cart\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> add \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-func\">useCart\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\n\u003Cspan class=\"shj-func\">add\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'sku-123'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\n\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">script\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F Simulates two \"requests\" racing against the buggy module-scope cart.\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> cart\u003Cspan class=\"shj-type\">: string\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan> \u003Cspan class=\"shj-cmnt\">\u002F\u002F module scope — created once\n\u003C\u002Fspan>\n\u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">handleRequest\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>user\u003Cspan class=\"shj-type\">: string\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> item\u003Cspan class=\"shj-type\">: string\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  cart\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">push\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>item\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Promise\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>r\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-func\">setTimeout\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>r\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-num\">10\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-cmnt\">\u002F\u002F stand-in for async work: a DB call, an await inside render\n\u003C\u002Fspan>  console\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">log\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">`\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">${\u003C\u002Fspan>user\u003Cspan class=\"shj-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-str\"> sees cart:`\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> cart\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-cmnt\">\u002F\u002F both users read the SAME array\n\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-func\">handleRequest\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'User A'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'sku-A'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\n\u003Cspan class=\"shj-func\">handleRequest\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'User B'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'sku-B'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F app\u002Fcomposables\u002FuseCart.ts\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">useCart\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> cart \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> useState\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> id\u003Cspan class=\"shj-type\">: string\u003C\u002Fspan>; qty\u003Cspan class=\"shj-type\">: number\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'cart'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\n\n  \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">add\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>id\u003Cspan class=\"shj-type\">: string\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    cart\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>value\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">push\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> id\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> qty\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-num\">1\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> cart\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> add \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F server\u002Fapi\u002Frate-limit.ts — BUGGY\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> requestCounts \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Map\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>string\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> number\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-cmnt\">\u002F\u002F module scope: one Map for the whole process\n\u003C\u002Fspan>\n\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">default\u003C\u002Fspan> \u003Cspan class=\"shj-func\">defineEventHandler\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>event\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> ip \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-func\">getRequestIP\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>event\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">??\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'unknown'\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> count \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>requestCounts\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">get\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>ip\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">??\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">+\u003C\u002Fspan> \u003Cspan class=\"shj-num\">1\u003C\u002Fspan>\n  requestCounts\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">set\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>ip\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> count\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> requests\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> count \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F server\u002Fmiddleware\u002Fauth.ts\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">default\u003C\u002Fspan> \u003Cspan class=\"shj-func\">defineEventHandler\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>event\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  event\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>context\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>user \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-func\">verifyToken\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-func\">getHeader\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>event\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'authorization'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F server\u002Fapi\u002Fprofile.ts\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">default\u003C\u002Fspan> \u003Cspan class=\"shj-func\">defineEventHandler\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>event\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> name\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> event\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>context\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>user\u003Cspan class=\"shj-oper\">?.\u003C\u002Fspan>name \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-ts shj-multiline\" data-lang=\"ts\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">\u002F\u002F The fix, side by side:\n\u003C\u002Fspan>\u003Cspan class=\"shj-cmnt\">\u002F\u002F ❌ Module scope — one instance for the whole server process\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> cart \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> ref\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-class\">Item\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-cmnt\">\u002F\u002F ✅ useState — one instance per request\u002Fapp-instance, keyed explicitly\n\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">useCart\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> cart \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> useState\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-class\">Item\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'cart'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> cart \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>",{"locked":18,"total":19,"comments":506},[]]