Middleware runs before cached static content is served, enabling personalization of SSG pages at the edge without converting them to SSR, but with important limitations around ISR and build-time execution
In Next.js, Middleware executes before the cached content is matched and served, acting as a dynamic gatekeeper in front of your static files. This means SSG pages remain fully static and cached at the edge, but Middleware can enhance them with request-time logic like authentication, A/B testing, geolocation-based redirects, or complex routing. The static HTML is generated at build time, and Middleware runs at request time to potentially modify which static file is served, all without sacrificing the performance benefits of SSG.
Personalization at the edge: Middleware reads cookies or headers and rewrites to pre-rendered static variants (e.g., different themes, locales, or A/B test versions) without the user seeing the variant in the URL .
Geolocation-based routing: Using request.geo from Vercel or similar platforms, Middleware can serve country-specific static pages that were pre-rendered at build time for all target regions .
Authentication checks: Middleware can verify authentication tokens and either serve the public static page, redirect to login, or rewrite to an authenticated variant .
Complex i18n routing: Beyond Next.js built-in i18n, Middleware can implement sophisticated locale detection based on accept-language headers, geolocation, or user preferences, then rewrite to the appropriate pre-rendered static locale version .
The critical insight is the execution order: Middleware runs first, then the static cache is checked. This means Middleware does not disable static generation. When Middleware rewrites a request from / to /site/light/US, Next.js still serves the pre-rendered static HTML from /site/light/US that was generated at build time. The user gets a personalized experience with the performance of a static page. This is fundamentally different from SSR, where the page would be generated on each request .
On-Demand ISR bypasses Middleware: When using revalidatePath or revalidateTag for on-demand revalidation, the revalidation request does NOT execute Middleware. This means any rewrite logic in Middleware won't apply during background regeneration, potentially causing cache invalidation of the wrong path .
Build-time vs. Runtime: Middleware cannot influence which pages are generated at build time—that's the job of generateStaticParams. Conversely, generateStaticParams cannot access request data like cookies or headers .
Streaming and Suspense ignored: If you force a route to be static with dynamic = 'force-static' or dynamic = 'error', any Suspense boundaries for streaming are ignored in production. The page becomes a single static file, collapsing fallback UI .
CDN coordination: Middleware runs at the edge, but if you have a CDN in front of your application, you must ensure the CDN doesn't cache responses in a way that bypasses your Middleware logic. Proper Cache-Control headers are essential .
Matcher configuration: Middleware runs on every matched route. For performance, use the matcher config to limit Middleware execution only to paths that actually need it, rather than running on all requests .
Pre-render all variations: Use generateStaticParams to create static pages for every possible combination of personalized content (themes, locales, A/B test variants) at build time .
Use rewrite, not redirect: Prefer NextResponse.rewrite() over redirects when serving personalized static content. Rewrites keep the URL clean while serving the correct static file; redirects add an extra round trip .
Handle ISR carefully: When using on-demand revalidation, bypass middleware by revalidating the exact cached path, not the public-facing path that middleware would rewrite .
Test in production mode: Always test with next build && next start because development mode handles static generation differently and may hide issues like Suspense collapsing .
Set appropriate cache headers: Configure Cache-Control headers in Middleware responses to ensure CDNs don't cache personalized responses too aggressively .
Use matcher efficiently: Limit Middleware execution to only the paths that need it with the matcher config to avoid unnecessary edge function invocations .
Consider a multi-tenant SaaS application where each customer has a subdomain (customer1.app.com, customer2.app.com). You can pre-render all static pages for each customer at build time using generateStaticParams with tenant IDs. Then, Middleware reads the subdomain from the request host, looks up the tenant ID, and rewrites to the appropriate pre-rendered static path. This gives you the performance of static sites with the flexibility of multi-tenant architecture, all without per-request server rendering .