Efficient Redirect Management

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:


๐Ÿ” 1. Use 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.


โšก 2. Generate Redirects Programmatically

If you have many redirects from a database or CMS, consider generating them at build time:

js
const 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.


๐ŸŒ 3. Use Edge Middleware for Dynamic Redirects

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.


๐Ÿ—ƒ๏ธ 4. Use a Reverse Proxy for Massive Lists

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.


๐Ÿงน 5. Organize Redirect Data Clearly

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.


โš ๏ธ Cautions

  • 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)?

How to handle thousands of redirects in the middleware?

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:


โœ… Recommended Approach: Use a Lookup Map with Middleware

1. Store Redirects in a Key-Value Map

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... };

2. Use This Map in Middleware

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.


3. Optimize Middleware Location

Use the matcher config to limit which paths trigger middleware:

ts
// middleware.ts export const config = { matcher: ['/old-paths/:path*'], // only match relevant prefixes };

โš ๏ธ Performance Considerations

  • 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.


๐Ÿ’ก Alternatives (Scalable Options)

A. Serve Redirects from Edge Config on Vercel (Edge Config)

Great for frequently updated or massive redirect lists. You can fetch the data once at deployment and cache it.

B. Handle Redirects via CDN (e.g., Vercel, Cloudflare)

Use CDN rules for the heaviest traffic redirect cases. Vercel allows configuration through its dashboard or vercel.json.

C. Pre-generate Static Redirects (if known at build time)

Generate thousands of redirects into next.config.js from a script:

ts
const 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?

I mean strategies like bloom filter

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).


๐Ÿš€ Strategy: Use a Bloom Filter in Middleware

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.

โœ… Why Bloom Filters Work Well for Edge Middleware

  • ๐Ÿง  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


๐Ÿ”ง Implementation Plan

1. Generate a Bloom Filter from Redirect Paths

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()));

2. Load and Use It in Middleware

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); }

โš–๏ธ Tradeoffs

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

๐Ÿง  Bonus: Use Bloom Filter for Redirect Eligibility, Not Resolution

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?

implement the bonus as well

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).


๐Ÿ› ๏ธ Step-by-Step Implementation

1. Directory Structure

/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

2. ๐Ÿ”ง Step 1: Build Script to Create the Bloom Filter

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:

bash
ts-node lib/generate-bloom.ts

3. ๐Ÿ“ฆ Step 2: Middleware with Lazy Map Loading

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 };

4. ๐Ÿงช Example redirects.json

json
[ { "source": "/old-home", "destination": "/new-home", "permanent": true }, { "source": "/legacy-page", "destination": "/modern-page", "permanent": false } ]

โœ… Benefits of This Approach

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?

Isn't converting the list into a map less performant than searching the array itself?

Great question โ€” and itโ€™s a common point of confusion.

๐Ÿ”„ Short Answer:

No, converting the list into a Map once and then doing lookups on that Map is significantly more performant than repeatedly searching an array.


๐Ÿ“Š Performance Comparison

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)

๐Ÿง  Why?

If you use:

ts
array.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:

ts
const map = new Map(redirects.map(r => [r.source, r])); map.get(path);

โ€ฆthe lookup is almost instantaneous, no matter how many items there are.


โš ๏ธ But What About Conversion Cost?

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


๐Ÿงช Benchmark Example

ts
const 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

โœ… Bottom Line

  • 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.