DEV Community

Anas Sheikh
Anas Sheikh

Posted on

Your Next.js Middleware Might Be Running More Often Than You Think, Thanks to Prefetching

Here's a question worth actually testing on your own app: if a user never clicks a link, just scrolls past it while it sits visible in the viewport, does your middleware run for that link's route anyway?

For a huge number of Next.js apps, the answer is yes, and almost nobody accounts for it.

Why This Happens

The <Link> component prefetches the linked page automatically once it enters the viewport, on a reasonably fast connection. This is genuinely great for perceived performance, the page often feels instant when actually clicked, because most of the work already happened silently in the background while the user was just scrolling.

Middleware runs on every matched request, and a prefetch is a real request. If your middleware assumes "this only fires when a user actually navigates somewhere," that assumption is wrong far more often than most developers realize, since a page can get prefetched, and your middleware can run for it, without the user ever actually going there.

Where This Actually Causes Bugs

Remember the A/B test variant assignment pattern, assigning a random variant and setting a cookie the first time middleware sees a request for a given route?

export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  let variant = request.cookies.get('ab-variant')?.value;

  if (!variant) {
    variant = Math.random() < 0.5 ? 'a' : 'b';
    response.cookies.set('ab-variant', variant, { maxAge: 60 * 60 * 24 * 30 });
  }

  return response;
}
Enter fullscreen mode Exit fullscreen mode

This specific example is actually safe, since it's idempotent, once the cookie is set, prefetches and real visits both see the same value. But a lot of middleware logic isn't written that carefully. Anything with a genuine side effect, incrementing a counter, logging an event, triggering a notification, calling an external API, is not safe to run on a prefetch, because a prefetch means "this page might be visited soon," not "this page was actually visited."

// ❌ Increments a "views" counter on every matched request, including prefetches
export async function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/blog/')) {
    await incrementViewCount(request.nextUrl.pathname); // fires on prefetch too
  }
  return NextResponse.next();
}
Enter fullscreen mode Exit fullscreen mode

A blog post sitting in a list of links, visible in the viewport but never actually clicked, can silently increment a view counter anyway, just because a user scrolled past it. Your analytics are now measuring "how often this link entered someone's viewport," not "how often someone actually read this post," and nothing about the numbers looks obviously wrong, they're just quietly inflated in a way that's hard to notice without specifically checking.

How to Tell If This Is Happening to You

Next.js sends a specific header on prefetch requests that regular navigations don't have. Checking for it in middleware lets you distinguish the two:

export function middleware(request: NextRequest) {
  const isPrefetch = request.headers.get('purpose') === 'prefetch' ||
                      request.headers.get('next-router-prefetch') === '1';

  if (isPrefetch) {
    // Skip anything with a real side effect for prefetch requests
    return NextResponse.next();
  }

  // Safe to run side-effecting logic here, this is a real navigation
  return NextResponse.next();
}
Enter fullscreen mode Exit fullscreen mode

Worth noting the exact header can shift slightly across Next.js versions, so logging request.headers for a route you suspect is affected and checking what's actually present is more reliable than trusting a hardcoded header name blindly.

The Broader Rule This Points To

Anything in middleware that has a genuine side effect, not just reading or redirecting, but writing, logging, counting, notifying, needs to explicitly consider whether it's safe to run on a request that might never correspond to a real user action. Read-only logic, redirects, header forwarding, cookie checks that don't have external side effects, are generally safe regardless. Anything that writes somewhere or triggers an external effect needs the prefetch check, or needs to move out of middleware entirely into something that only runs on an actual page load, a Server Component reading a "real visit" signal instead.

Go Check This

If you have any middleware logic that writes to a database, calls an external API, or logs something meant to represent "a user did X," go verify whether it's running on prefetches too. The easiest test: add a temporary console.log distinguishing prefetch from real navigation, then scroll a page with several internal links without clicking any of them, and watch what fires anyway.


Curious whether this was already on people's radar or is as much of a surprise as it was for me the first time I actually noticed it happening. Drop your experience in the comments, especially if you've caught this causing a real bug somewhere.

Get the templates: https://pixelanas.gumroad.com


Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751

Top comments (0)