Next.js 13 App Router Link Prefetching Flooded Our Client API cover image
Back to Blog
TechnologyPublished 8 July 2026· Updated 22 August 2026· 5 min read

Next.js 13 App Router Link Prefetching Flooded Our Client API

A production incident where Next.js 13 App Router prefetching flooded our client API with thousands of empty JSON requests per session, and the targeted fix we shipped.

When Next.js 13 App Router Prefetching Flooded Our Client API

The Incident: A Client Site Drowning in Prefetch Requests

The Setup: E-commerce Product Listing Page

Last month we were migrating a client's e-commerce product listing page from the Pages Router to the Next.js 13 App Router. The page renders a grid of 48 products, each wrapped in a <Link> to its detail page. We upgraded to Next.js 13.4.5, switched to the App Router, and deployed to staging.

The page looked fine. Clicks were fast. But our API gateway started throwing 429s within minutes of the first internal tester opening the page.

What We Observed in Production Logs

Our API logs showed a spike in GET /_next/data/.../product/[id].json requests. Each product link was firing a prefetch request the moment it entered the viewport. With 48 links per page and testers scrolling quickly, we saw 200+ prefetch requests from a single session in under 10 seconds.

The payloads were all empty objects: {}. No useful data. Just noise.

The Smoking Gun: Thousands of Requests per User Session

A single tester session generated over 1,200 prefetch requests. Our API rate limiter started rejecting them. The client's backend engineer called it a self-inflicted DDoS.

We confirmed the behavior in Chrome DevTools Network tab. Every <Link> with prefetch enabled (the default) was hitting the data route on viewport entry. The Next.js prefetching guide confirms this: "By default, Next.js prefetches routes based on the links in your application code."

What We Tried First (And What Failed)

Disabling All Prefetching with prefetch={false}

Our first instinct was to kill prefetching entirely. We added prefetch={false} to every <Link> in the product grid.

<Link href={`/product/${product.id}`} prefetch={false}>
  {product.name}
</Link>

This reduced the request count, but GitHub Discussion #44596 users reported the same issue: "next still issues all the empty data requests on every hover." We confirmed it. Hovering over a link still triggered a prefetch request even with prefetch={false}.

Rolling Our Own Link Wrapper

We built a custom NoPrefetchLink component that wrapped <Link> and suppressed all prefetch behavior using router.prefetch() overrides.

import Link from 'next/link';
import { useRouter } from 'next/navigation';

export default function NoPrefetchLink({ href, children, ...props }) {
  const router = useRouter();
  const handleClick = (e) => {
    e.preventDefault();
    router.push(href);
  };
  return (
    <a href={href} onClick={handleClick} {...props}>
      {children}
    </a>
  );
}

This worked, but we lost client-side transition benefits. Navigation became full page reloads. Not acceptable for an e-commerce experience.

The Middleware Misdirection

We suspected our middleware was the culprit. We had a middleware that rewrites URLs and injects headers. Discussion #44596 mentioned: "set config.experimental.middlewarePrefetch = 'strict' to avoid this behavior."

We added it to next.config.js:

module.exports = {
  experimental: {
    middlewarePrefetch: 'strict',
  },
};

No change. The empty JSON requests kept flowing.

The Working Fix: Targeted Prefetching with Real Commands

Configuring next.config.js for Controlled Prefetching

We discovered that Next.js 13.4+ supports partial prefetching via the partialPrefetching config. This switches from all-or-nothing prefetching to per-route App Shell prefetching.

// next.config.js
module.exports = {
  experimental: {
    partialPrefetching: true,
  },
};

With partial prefetching enabled, Next.js prefetches the route's App Shell once per route, not per link. A page with 48 links to the same product detail route makes one prefetch request instead of 48.

Using router.prefetch() for Hover-Based Loading

We replaced automatic viewport prefetching with hover-based prefetching using router.prefetch().

import Link from 'next/link';
import { useRouter } from 'next/navigation';

export default function ProductLink({ product }) {
  const router = useRouter();

  const handleMouseEnter = () => {
    router.prefetch(`/product/${product.id}`);
  };

  return (
    <Link
      href={`/product/${product.id}`}
      onMouseEnter={handleMouseEnter}
      prefetch={false}
    >
      {product.name}
    </Link>
  );
}

This defers prefetching until the user shows intent by hovering. Combined with partial prefetching, we cut prefetch requests by 90%.

Implementing loading.tsx Boundaries for Dynamic Routes

For dynamic routes like /product/[id], we added a loading.tsx file to enable partial prefetching.

// app/product/[id]/loading.tsx
export default function Loading() {
  return (
    <div className="p-6">
      <div className="animate-pulse">Loading product...</div>
    </div>
  );
}

The Next.js linking guide recommends this: "We recommend adding loading.tsx to dynamic routes to enable partial prefetching, trigger immediate navigation, and display a loading UI while the route renders."

Pitfalls We Would Warn an Intern About

Prefetching Dynamic Segments Without generateStaticParams()

Issue #47981 users found that dynamic segments without generateStaticParams() get prefetched but refetched on navigation. The cache is invalidated during hard navigation.

Always add generateStaticParams() to dynamic routes you want to prefetch effectively.

Cache Invalidation During Hard Navigations

Dynamic routes that don't match the current route's dynamic segments trigger hard navigations. During a hard navigation, "the cache is invalidated and the server refetches data and re-renders the changed segments."

This means prefetching a dynamic route is wasted if the user navigates there directly.

Middleware Prefetch Headers Stripping Cache-Control

Discussion #44596 users reported that prefetch responses for page.json files are stripped of cache-control headers, even when getServerSideProps is used. This can cause caching services to cache empty prefetch responses.

Check your CDN and middleware for header stripping on _next/data routes.

What We Would Do Differently Next Time

Audit Link Density Before Upgrading to App Router

Before migrating, we should have audited how many <Link> components existed per page. A product grid with 48 links is a prefetch storm waiting to happen.

Use partialPrefetching Config from the Start

We should have enabled partialPrefetching during the initial migration instead of discovering it after the incident.

Monitor Network Tab for Empty JSON Payloads Early

The empty {} payloads were visible in DevTools from the first test. We should have caught this before staging deployment.

The Fix in Production

After shipping the targeted prefetching fix, our API gateway stopped throwing 429s. Prefetch requests dropped from 1,200 per session to fewer than 10. The product grid still feels instant on hover, and client-side transitions work as expected.

We documented the fix in our internal runbook and added a lint rule to flag <Link> components without explicit prefetch props in high-density areas.

The lesson: Next.js prefetching is powerful, but it assumes you want to prefetch everything. In high-link-density scenarios, you need to take control.


Sources:

Enjoyed this article?

Back to Blog