Latency spikes under concurrent requests when Nuxt 3 routeRules cache expires
0 reputation · 03 Sept 2021, 16:16 UTC
0 reputation · 03 Sept 2021, 16:16 UTC
Goal: Determine the concurrent request volume at which a Nuxt 3 application experiences measurable latency increase when a routeRules cache entry expires and no request‑coalescing mechanism is in place.
Constraints: Using Nuxt 3 (≥3.8) with a routeRule that defines maxAge and staleWhileRevalidate, running on a Node.js server preset, and without custom middleware or useFetch cache deduplication. The environment should mirror production load‑testing conditions.
Questions: What approximate number of simultaneous requests triggers a noticeable latency spike right after cache expiry? Does enabling staleWhileRevalidate reduce the spike, and at what concurrency level does it become ineffective? Are there specific maxAge or swr values that can limit the stampede effect while keeping content freshness within acceptable bounds?
35275 reputation · 04 Sept 2021, 01:49 UTC
There is no fixed concurrency number. On Nuxt 3 (3.8 or newer, Node server preset, Nitro routeRules cache, no coalescing), every request that arrives while an entry is expired and not yet repopulated runs the route handler again. The threshold is set by two measurable variables: the handler's uncached execution time and the request arrival rate inside the expiry window.
Planning estimates only, not measured facts: handlers that finish in under roughly 50 ms typically need hundreds of simultaneous requests before redundant executions saturate the event loop, while handlers taking hundreds of milliseconds to seconds can spike at tens of concurrent requests, because the vulnerable window is wide enough for misses to pile up. Treat both ranges as hypotheses for your own load test.
With swr enabled, a request that hits an expired-but-stored entry receives the stale payload immediately while a background refresh updates the cache. Clients stop seeing the spike, but the server still pays for at least one full handler execution, and the request that triggers revalidation still incurs the full latency.
Both options take seconds. Size them from measurements, then verify:
| Option | Starting point | Rationale |
|---|---|---|
| maxAge | Longest staleness the content tolerates (60 s for near-real-time, 300–600 s for slow-changing pages) | Fewer expirations mean fewer stampede windows |
| staleWhileRevalidate | Several multiples of the handler's uncached p95 (a p95 of 500 ms suggests 10 s) | The window must cover one full revalidation under load |
// nuxt.config.ts
routeRules: {
'/heavy/**': {
cache: {
maxAge: 60, // seconds
staleWhileRevalidate: 10 // seconds
}
}
}
Run a ramped load test timed to hit just after expiry, and count handler executions per expiry window with a log line or counter inside the handler.
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 50 },
{ duration: '60s', target: 200 }
]
};
export default function () {
http.get('http://localhost:3000/heavy-page');
sleep(0.1);
}
Watch p95 and p99 latency, CPU, and executions per expiry. With swr on, roughly one execution per window means the window is holding; a count that climbs with concurrency means the swr value is too short or revalidation is too slow for the traffic pattern.
What is the handler's uncached p95 latency, and what does it call downstream? That number sets both the concurrency level where the spike appears and the minimum swr worth testing. Cache internals have shifted across Nitro releases, so confirm this behavior on your locked version before committing to values.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.