How to handle lots of redirects efficiently in my
next.js app?
Handling lots of redirects efficiently in a Next.js app requires a well-structured approach to maintain performance, scalability, and maintainability. Here are best practices and strategies:
next.config.js
for Static Redirects
If your redirects are static (known at build time),
define them in
next.config.js
:
js// next.config.js module.exports = { async redirects() { return [ { source: '/old-path', destination: '/new-path', permanent: true, // 308 redirect }, { source: '/legacy/:slug*', destination: '/new/:slug*', permanent: false, // 307 redirect }, // Add more here ]; }, };
โ Best for: small to moderate number of static routes.
If you have many redirects from a database or CMS, consider generating them at build time:
jsconst redirectsFromCMS = await getRedirects(); return { async redirects() { return redirectsFromCMS.map((r) => ({ source: r.source, destination: r.destination, permanent: r.permanent, })); }, };
โ Best for: large numbers of redirects pulled from a headless CMS or database.
If redirects depend on runtime conditions (e.g., geo, headers), use Edge Middleware:
js// middleware.ts import { NextResponse } from 'next/server'; export function middleware(request) { const url = request.nextUrl.clone(); if (url.pathname === '/old-dynamic') { url.pathname = '/new-dynamic'; return NextResponse.redirect(url); } return NextResponse.next(); }
โ Best for: real-time redirect logic, personalization, A/B testing.
For very large redirect lists (thousands or more), consider offloading them to a CDN (like Vercel, Cloudflare, or NGINX):
Configure redirects via CDN rules.
Your app stays clean and fast.
CDN handles performance-heavy routing.
โ Best for: extremely large redirect lists or performance-critical apps.
Use JSON or YAML files for better organization:
js// redirect-rules.json [ { "source": "/a", "destination": "/b", "permanent": true }, { "source": "/c", "destination": "/d", "permanent": false } ]
And load them into
next.config.js
.
Avoid excessive redirects in
middleware
โcan add latency.
Monitor for redirect loops.
Set proper HTTP status codes (308
for permanent,
307
for temporary).
Would you like a code example for a specific approach (e.g., CMS-driven or CDN)?
Handling thousands of redirects in Next.js middleware is possible, but not optimal unless done strategicallyโotherwise, it can negatively impact performance, especially since middleware runs on every request at the edge.
Here's how to do it efficiently:
Convert your redirects into an efficient in-memory
lookup object (e.g.,
Map
or
plain object) instead of iterating through a long
array.
ts// redirects.ts export const redirectMap: Record<string, { destination: string; permanent: boolean }> = { "/old-path-1": { destination: "/new-path-1", permanent: true }, "/old-path-2": { destination: "/new-path-2", permanent: false }, // thousands more... };
ts// middleware.ts import { NextRequest, NextResponse } from 'next/server'; import { redirectMap } from './redirects'; export function middleware(request: NextRequest) { const path = request.nextUrl.pathname; const rule = redirectMap[path]; if (rule) { const url = request.nextUrl.clone(); url.pathname = rule.destination; return NextResponse.redirect(url, rule.permanent ? 308 : 307); } return NextResponse.next(); }
This avoids expensive operations like
.find()
over an array.
Use the
matcher
config to limit which paths trigger middleware:
ts// middleware.ts export const config = { matcher: ['/old-paths/:path*'], // only match relevant prefixes };
Middleware runs on the edge, so avoid reading large files or fetching from a database.
Do not fetch dynamic data inside middleware (itโs not allowed anyway).
If your redirect list changes frequently or is huge (10K+), offload to CDN or serverless function.
Great for frequently updated or massive redirect lists. You can fetch the data once at deployment and cache it.
Use CDN rules for the heaviest traffic redirect cases.
Vercel allows configuration through its dashboard or
vercel.json
.
Generate thousands of redirects into
next.config.js
from a script:
tsconst data = require('./redirects.json'); module.exports = { async redirects() { return data.map(({ from, to, permanent }) => ({ source: from, destination: to, permanent, })); }, };
Would you like help generating the redirect map programmatically or integrating with a specific platform like Vercel Edge Config?
Ah, great โ you're thinking like a systems engineer ๐. If you're trying to efficiently handle thousands (or tens/hundreds of thousands) of redirects in Next.js middleware, probabilistic data structures like Bloom filters can absolutely help โ especially when latency is a concern and memory is constrained (as on the edge).
A Bloom filter is a space-efficient, probabilistic data structure that can test whether an element is possibly in a set (with no false negatives, but possible false positives). This makes it ideal as a first-pass filter for redirects.
๐ง Tiny memory footprint
โก Constant-time lookup
โ No false negatives (if the filter says โnot in setโ, itโs definitely not)
๐ก Use to skip logic for 99.9% of users
At build time, generate a Bloom filter containing all
the source paths (e.g.
/old-path-1
,
/foo
,
etc.).
Use a package like
bloom-filters
:
ts// build/generate-bloom.ts import { BloomFilter } from 'bloom-filters'; import redirects from '../redirects.json'; const filter = BloomFilter.create(redirects.length, 0.01); // 1% false positive rate redirects.forEach(({ source }) => { filter.add(source); }); // Serialize and save require('fs').writeFileSync('bloom-filter.json', JSON.stringify(filter.saveAsJSON()));
ts// middleware.ts import { NextResponse } from 'next/server'; import { BloomFilter } from 'bloom-filters'; import redirects from './redirects.json'; // Full list, ideally precompiled map import bloomData from './bloom-filter.json'; const bloom = BloomFilter.fromJSON(bloomData); // Optional: Convert redirects to Map for fast lookup const redirectMap = new Map(redirects.map(r => [r.source, r])); export function middleware(request) { const path = request.nextUrl.pathname; // Use bloom filter to skip the lookup unless maybe present if (!bloom.has(path)) return NextResponse.next(); const rule = redirectMap.get(path); if (!rule) return NextResponse.next(); // false positive from bloom const url = request.nextUrl.clone(); url.pathname = rule.destination; return NextResponse.redirect(url, rule.permanent ? 308 : 307); }
Factor | Bloom Filter | Redirect Map |
---|---|---|
Memory | Very low (~KBs) | Higher (depends on size) |
Speed | Constant-time |
Constant-time if using
Map
|
Accuracy | 100% if not present, ~99% if present | 100% accurate |
Edge-suitability | โ Excellent | โ If optimized |
Use Bloom just to decide whether to even load or parse the redirect map. You can:
Embed Bloom filter directly in middleware
Lazy load
redirectMap
only when
bloom.has(path)
returns
true
Combine with
.matcher
to scope to fewer routes
Would you like a working setup example using this with a large redirect list and build script?
Perfect โ letโs fully implement the optimized redirect system using a Bloom Filter in Next.js middleware, including the bonus: only lazy-loading the redirect map if the Bloom filter suggests a match (i.e., likely redirect).
/lib/ redirects.json // Your full redirect list bloom-filter.json // Precompiled Bloom filter generate-bloom.ts // Build script to generate filter /middleware.ts // Next.js middleware
ts// lib/generate-bloom.ts import { BloomFilter } from 'bloom-filters'; import fs from 'fs'; import path from 'path'; const redirects = require('./redirects.json'); const filter = BloomFilter.create(redirects.length, 0.01); // 1% false positives redirects.forEach(({ source }: { source: string }) => { filter.add(source); }); fs.writeFileSync( path.join(__dirname, 'bloom-filter.json'), JSON.stringify(filter.saveAsJSON(), null, 2) ); console.log('โ Bloom filter generated!');
Run this in a prebuild step or manually:
bashts-node lib/generate-bloom.ts
ts// middleware.ts import { NextRequest, NextResponse } from 'next/server'; import { BloomFilter } from 'bloom-filters'; import bloomData from './lib/bloom-filter.json'; // Load bloom filter once const bloom = BloomFilter.fromJSON(bloomData); // Lazy-initialized redirect map let redirectMap: Map<string, { destination: string; permanent: boolean }> | null = null; // Lazy load on first match function getRedirectMap() { if (!redirectMap) { const redirects = require('./lib/redirects.json'); redirectMap = new Map(redirects.map((r: any) => [r.source, r])); } return redirectMap; } export function middleware(request: NextRequest) { const path = request.nextUrl.pathname; // Check bloom filter if (!bloom.has(path)) return NextResponse.next(); // definitely not in redirect list // Possibly in redirect list โ load map and check const map = getRedirectMap(); const rule = map.get(path); if (!rule) return NextResponse.next(); // false positive const url = request.nextUrl.clone(); url.pathname = rule.destination; return NextResponse.redirect(url, rule.permanent ? 308 : 307); } export const config = { matcher: ['/((?!api|_next|static).*)'], // Adjust as needed };
redirects.json
json[ { "source": "/old-home", "destination": "/new-home", "permanent": true }, { "source": "/legacy-page", "destination": "/modern-page", "permanent": false } ]
Feature | Benefit |
---|---|
๐ Bloom Filter | Filters 99% of non-redirect paths in constant time |
๐ง Lazy Redirect Map | Loads only when needed (on first match) |
๐๏ธ Fast Middleware | Light memory use at the edge |
โ๏ธ Flexible | Scales to tens of thousands of redirects |
Would you like this packaged into a GitHub-ready template or Next.js starter repo?
Great question โ and itโs a common point of confusion.
No,
converting the list into a
Map
once
and then doing lookups on that
Map
is
significantly more performant
than repeatedly searching an array.
Letโs compare lookup time:
Method | Lookup Time Complexity | Lookup Speed |
---|---|---|
array.find()
|
O(n) | Slow with large N |
Map.get(key)
|
O(1) (average) | Fast, constant time |
Object
{ key: value }
|
O(1) (average) | Same as Map (but less safe) |
If you use:
tsarray.find(r => r.source === path);
โฆit loops through the entire array until it finds a match (or not). That means time grows linearly with your number of redirects.
But if you use:
tsconst map = new Map(redirects.map(r => [r.source, r])); map.get(path);
โฆthe lookup is almost instantaneous, no matter how many items there are.
Yes โ converting a list to a
Map
has
a cost, but:
Itโs a one-time O(n) operation
You do it once, not on every request
In your case, you only do it after a bloom filter hit, so itโs rare anyway
tsconst redirects = new Array(100_000).fill(0).map((_, i) => ({ source: `/old-${i}`, destination: `/new-${i}`, })); console.time('find'); redirects.find(r => r.source === '/old-99999'); console.timeEnd('find'); // ~milliseconds const map = new Map(redirects.map(r => [r.source, r])); console.time('map'); map.get('/old-99999'); console.timeEnd('map'); // ~microseconds
For thousands of redirects, always prefer a
Map
or object for fast lookup
Building the map once lazily is a negligible cost
When combined with a Bloom filter, it makes your middleware highly performant
Let me know if you want a benchmark file to test this locally or on Vercel Edge.