Build1 distinct publisher3 min readUpdated
A dev.to field report argues the ten-line in-memory limiter multiplies your limit by warm instance count, and that path-based middleware rules cannot see a Server Action at all.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
A set of field notes published on dev.to takes the ten-line rate limiter that most backend engineers have shipped and shows it breaking in three places when moved into a Next.js 16 App Router project on Vercel: where the counter lives, who counts as an identity, and how you attach a limit to a Server Action that has no URL of its own [1]. Both of the first two failures are quiet, which is the consequence worth caring about: the limiter still returns 429s, still looks right in review, and is wrong by a factor nobody in the request can measure.
Start with the arithmetic. A module-scope `Map` is private to one function instance, and a serverless platform runs many instances at once, so each one enforces the full limit on its own [2]. With ten warm instances, a limit of 10 requests per minute permits 100 [3] - a ten-fold error in the only number the policy actually specifies [4]. The instance count is not something you control or can observe from inside the request [5].
Vercel Fluid Compute makes this harder to notice rather than easier, according to the author: it reuses a single instance across concurrent requests instead of spawning one per request, so the `Map` survives far longer than under classic serverless [6]. In local development and in a quiet preview deployment the limiter looks correct, and it only comes apart under traffic spread across enough instances to matter, with nothing in the logs announcing it [7]. The same `Map` never shrinks, so on a long-lived instance every unique key ever seen stays resident until recycling, which the piece describes as a slow memory leak wearing a rate limiter costume [8]. The stated fix is not a cleverer `Map` but a store every instance shares that supports an atomic increment: Redis, or any datastore with a compare-and-set primitive [9].
The second failure is addressing, and it is the one that survives a Redis migration. Every Next.js Server Action POSTs to the URL of the page that called it and carries a build-generated `Next-Action` header, so path-based limiting in middleware cannot tell one action from another [10]. The consequence in the source is blunt: the only reliable place to limit a specific action is inside the action body [11]. A middleware matcher of the shape `['/api/:path*', '/login', '/signup']` [12] therefore never sees a sign-up action invoked from a page outside that list, because the POST is addressed to the calling page [13].
Identity is the third break. `NextRequest.ip` was removed in Next.js 15, and on Vercel the author reads the client address with `ipAddress(request)` from `@vercel/functions` rather than trusting a raw `x-forwarded-for` header [14].
The division of labour that falls out: middleware runs before Next.js resolves the route, so a request rejected there never boots the route's function and never touches your database, which makes it the right place for a coarse, identity-agnostic abuse limit [15]. The route handler knows the authenticated user, the parsed body and the business meaning of the call, which makes it the right place for a per-user quota [16]. Middleware also sees RSC prefetch requests that carry the `RSC: 1` header and that the user never intentionally made [17], so those get counted as traffic unless skipped. Rejections should carry status 429 with a `Retry-After` header [18], and the fail-open versus fail-closed choice should be made per route before Redis has its first outage [19].
Worth checking in your own codebase: whether any limiter state lives in module scope, whether your sensitive Server Actions are limited in the action body or only by a matcher, and whether a hover-triggered prefetch is spending someone's quota.
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
The author writes that moving a simple rate limiter into a Next.js 16 App Router project on Vercel broke it in three unexpected places: where the counter lives, who counts as an identity, and how you attach a limit to a Server Action that has no URL of its own.
A module-scope Map is private to one function instance, and a serverless platform runs many instances at once, so each instance enforces the full limit on its own; the effective limit is the configured limit multiplied by the number of warm instances.
With ten warm function instances, a 10-requests-per-minute limit permits 100 requests per minute.
The number of warm instances is not something you control or can observe from inside the request.
In local development and in a quiet preview deployment the in-memory limiter looks correct; it only comes apart under traffic spread across enough instances to matter, and nothing in the logs announces it.
The Map never shrinks: on a long-lived instance every unique key ever seen stays resident until the instance is recycled, which the author calls a slow memory leak wearing a rate limiter costume.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Coherent mechanism, single unverified account
The core argument is mechanistically self-consistent and shown in code: a per-instance Map cannot enforce a global limit, and a Server Action addressed to the calling page URL cannot be distinguished by a path matcher. But every assertion traces to one dev.to field report with no measurements, reproduction data, vendor documentation or second implementation; the ten-instance / 100-rpm figure is an illustration rather than an observation, and the Fluid Compute concurrency claim is unverified in the cluster.
No adoption signal in cluster
The cluster contains no release notes, deployment reports, benchmarks, pricing or usage disclosures -- only one practitioner's account of their own code. Nothing supports an estimate of how widely either the broken pattern or the recommended shared-store approach is deployed.
Slightly overstated relative to shown evidence
Framing is mostly proportionate -- the failure mode is real and the remedies are conventional -- but the headline generalises an unmeasured multiplier ('a limit of 10 becomes 10 per instance'), the memory-leak metaphor dramatises unbounded key growth without any observed footprint, and platform-specific detail such as Fluid Compute reuse is asserted rather than evidenced. The gap is small because nothing extraordinary is being promised.
Low: practitioner post, no disclosed vendor tie
The piece is a community developer-blog field report with no disclosed sponsorship, employer or vendor relationship, and its recommendations point to generic primitives (any shared store with atomic increment or compare-and-set). Residual incentive is the ordinary one for platform-blog authorship -- authority and audience from a strong headline claim -- plus incidental promotion of Vercel and Redis-shaped tooling as the assumed stack.
Moderate on mechanism, weak on verification
Confidence is moderate: the reasoning is internally consistent, the code artefacts are inspectable, and the layering and 429/Retry-After advice is conventional. It is held down by single-publisher sourcing, absence of any adoption or measurement data, and version- and platform-specific assertions that no second source in the cluster confirms.
build
The fourth Web Push requirement: iOS will not deliver until the user installs your site1 distinct publisher
build
Next.js's architecture is working. Its operating manual isn't keeping up.1 distinct publisher
build
The MCP test that matters: a log tool that fetched the data and then said it failed1 distinct publisher
build
Per-tenant Claude clients belong in the dependency graph, not in middleware1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 18, 2026