We ran into an issue where clicking a menu item sometimes did not navigate to the expected page.
The URL in the address bar would change correctly, but the content on the screen would not update. If we kept clicking the menu, navigation would occasionally work.The issue did not appear in local development, but only in the production environment.
It took some time to figure out the cause and solution, I would like to share what we learned.
Reason
Next.js Sends an RSC Request When a <Link> Is Clicked
In the App Router, clicking a <Link> component triggers a client-side navigation instead of a full page reload. To render the next page, Next.js sends an internal request related to React Server Components (RSC) to fetch the server-rendered data required for the transition.
It allows the page to update without reloading the entire document.
What We Found in the Network Tab
While inspecting the Network tab during navigation, we noticed the following response headers:
x-middleware-rewrite: /rate-limit-error?retryAfter=1&_rsc=xxxx
x-nextjs-rewritten-path: /rate-limit-error
x-middleware-rewrite:
The middleware rewrote the internal RSC request to the rate-limit error route.
x-nextjs-rewritten-path:
Next.js ultimately processed the request as /rate-limit-error.
This means that when an internal RSC request was triggered, the middleware rewrote it to the rate-limit error route, and Next.js ultimately processed the request as /rate-limit-error.
Flow of the Issue
- The user clicks a
<Link>. - Next.js sends an internal RSC request to fetch data for the next page.
- The rate-limiting middleware incorrectly applies rate limiting to this internal request.
- The middleware rewrites the request to /rate-limit-error.
- The browser URL remains correct, but the fetched content comes from a different route.
- This mismatch causes the page to appear broken or not update as expected.
This mismatch causes the page to appear broken or not update as expected.
Let's fix
Before
Our rate-limiting middleware was too broad and could apply to internal Next.js requests. As a result, RSC requests triggered by navigation were sometimes rate-limited and rewritten to /rate-limit-error, causing the URL and rendered content to mismatch.
Before (problem)
// Rate limit applied too broadly
const shouldRateLimit = true; ←too broad
// Incomplete RSC detection
const hasRscParam = requestUrl.includes('_rsc=');
const hasRscHeader = request.headers.get('rsc') === '1';
const isRSCRequest = hasRscParam || hasRscHeader;
// Internal requests were not excluded
if (shouldRateLimit && isRateLimited) {
return NextResponse.rewrite(
new URL('/rate-limit-error', request.url)
);
}
Problem:
Internal RSC requests triggered by navigation could be rate-limited and rewritten.
After
We restricted rate limiting to API routes and non-GET requests only, and explicitly excluded internal Next.js requests (RSC/router requests). This prevented the middleware from rewriting navigation-related requests and restored stable client-side routing.
// Rate limit only APIs and non-GET requests
const isApiRoute = pathname.startsWith('/api/');
const isNonGetRequest = request.method !== 'GET';
const shouldRateLimit = isApiRoute || isNonGetRequest;
// Accurate RSC detection
const hasRscParam = requestUrl.includes('_rsc=');
const hasRscHeader = request.headers.get('rsc') === '1';
const hasRscAccept =
request.headers.get('accept')?.includes('text/x-component');
const isRSCRequest = hasRscParam || hasRscHeader || hasRscAccept;
// Exclude internal navigation requests
if (shouldRateLimit && !isRSCRequest && isRateLimited) {
return NextResponse.rewrite(
new URL('/rate-limit-error', request.url)
);
}
Exclude internal RSC requests from rate limiting and apply rate limits only to API and non-GET requests.
To avoid similar issues in the future
- Apply rate limiting only to API routes and non-GET requests.
- Explicitly exclude internal Next.js requests such as RSC and router-related requests from middleware logic.
- Be cautious when using rewrites in middleware, especially with client-side navigation.
- When navigation behaves inconsistently, inspect the Network tab and response headers to check for unexpected rewrites.
Keeping these points in mind helps ensure stable navigation and avoids hard-to-debug issues in production.

Top comments (0)