01Config redirects vs middleware โ I benchmarked both
On Vercel you can redirect URLs two ways: static rules in `next.config.js`, or dynamic logic in Edge Middleware (`middleware.js`). It's not just syntax โ they're different layers of the stack.
Config redirects compile at build time and run at Vercel's edge routing layer. Middleware is JavaScript in a V8 sandbox that runs per request. I ran both under load to see what the latency and cost difference actually looks like.
02next.config.js redirects are stupid fast
Redirects in `next.config.js` get baked in at build time. Vercel maps them directly on the CDN โ no serverless function spins up when someone hits an old URL.
Latency stays in the 2โ10ms range and there's no cold start. For anything that doesn't need runtime logic, this is what I use:
module.exports = {
async redirects() {
return [
{
source: '/articles/:slug',
destination: '/blog/:slug',
permanent: true,
},
];
},
};03Middleware when you need runtime context
Edge Middleware is a real code file that runs close to the user. That's where you check cookies, geo headers, A/B buckets โ stuff you can't express as a static map.
The tradeoff is execution time. Warm requests land around 15โ30ms; cold starts can spike to 100ms. Here's the geo redirect pattern I keep around as a reference:
import { NextResponse } from 'next/server';
export function middleware(request) {
const country = request.geo?.country || 'IN';
if (country === 'US') {
return NextResponse.redirect(new URL('/us-version', request.url));
}
return NextResponse.next();
}04Cold starts and the billing surprise
Edge functions sleep when traffic is quiet. The next hit pays an 80โ120ms cold-start tax โ annoying if that request is on your critical path.
Middleware also bills per execution. Run it on every asset request โ images, JS, fonts โ and your invoice gets ugly fast. Tighten the `config.matcher` so middleware only runs on routes that actually need logic.
05What the numbers looked like
I ran 1,000 concurrent requests through both setups. Here's the summary โ your mileage will vary, but the gap was consistent enough that I stopped defaulting to middleware for simple redirects:
| Metric | next.config.js Redirects | Edge Middleware (middleware.js) |
|---|---|---|
| Avg Response Latency | 2 - 10ms | 15 - 30ms |
| Cold Start Delay | 0ms (Static) | 80 - 120ms |
| Execution Billing Cost | Free (Edge Routing) | Serverless Execution Units |
| Dynamic Capabilities | None (Static Mapping) | High (Cookies, Geo, Headers) |
06The matcher that saved my bill
Default middleware runs on almost everything โ including `_next/static`, images, and favicons. I tightened the matcher to only `/blog/:path*` and `/tools/:path*` routes that actually needed cookie checks. Invoice stopped climbing from "mystery edge executions."
Rule of thumb: if the redirect does not need headers, cookies, or geo at request time, it belongs in `next.config.js` or `vercel.json`, not middleware.
07What I still use middleware for on sinhaabhinav.in
Most of my legacy URL cleanup lives in `vercel.json` as static 308s โ old `/articles/*` paths and root-level blog slugs pointing to `/blog/*`. That is exactly the kind of map that should never touch JavaScript.
Middleware stays reserved for experiments: geo-based landing copy, auth gates on preview routes, or A/B headers. Production redirects almost never need it.
08Redirect migration checklist
When I moved from Vite to Next.js I had three URL shapes in the wild: `/articles/slug`, `/slug`, and `/blog/slug`. Step one: pick `/blog/slug` as canonical. Step two: 308 everything else. Step three: update internal links โ crawlers forgive redirects, but internal links should not chain.
After deploy, spot-check five old URLs in curl and confirm one hop, correct destination, and no redirect loop. I lost a day once to a temporary rule that sent `/blog/:slug` to `/` โ permanent redirects plus Google cache are painful to unwind.
09vercel.json vs next.config โ where each redirect lives
On sinhaabhinav.in most legacy blog redirects live in `vercel.json` because they are simple slug maps and deploy independently of the Next build. Framework redirects in `next.config.mjs` are fine too โ I use both, but I avoid duplicating the same source in two files.
When debugging, run `curl -I https://yoursite.com/old-path` and read the status code. You want a single 308/301 to the final URL, not a chain through `/` or `/blog` twice. Chains waste crawl budget and confused AdSense reviewers when old URLs briefly showed the homepage title in Google.
10Performance testing setup I used
I used k6 with 1,000 virtual users hitting the same legacy URL path โ half against `vercel.json` redirects, half against a middleware prototype. Static rules averaged 6ms p95 in Mumbai; middleware averaged 38ms warm and spiked over 100ms on cold starts.
Your numbers will differ by region and plan. The point is to measure before you default to middleware because a tutorial told you it is "modern." Often the boring config redirect is the production answer.
11Cookie-based redirects โ the middleware sweet spot
The one redirect I still consider middleware for: send logged-in users away from `/login` to `/dashboard`, or block unauthenticated access to `/admin/*`. That needs the session cookie at request time โ static maps cannot read it.
Even then, keep the matcher tight:`/admin/:path*` only. Do not run auth middleware on static assets or your favicon will accidentally trigger edge billing.
12next.config vs vercel.json on Vercel
Both work on Vercel. I prefer `vercel.json` for legacy slug migrations because marketing teams can read a flat list without digging into Next config. Framework redirects in `next.config.mjs` are better when the map is generated from code or environment variables.
Never define the same `source` in both places with different destinations โ you will chase ghost redirects in preview deployments for hours.
13When reviewers and bots hit old URLs
AdSense and Google Search Console sometimes crawl legacy paths from old indexes. A clean 308 to the canonical blog URL is fine. A 308 to the homepage with the site title is not โ that is how individual posts end up indexed with your name instead of the article title.
After any redirect change, inspect five old URLs in Search Console and confirm the indexed canonical is `/blog/slug`, not `/slug` and not `/`.


