How we cut auth redirect latency to single-digit milliseconds and ran A/B tests without a flash of wrong content, using Vercel Edge Middleware.
Our marketing team wanted a headline test on the homepage. Simple ask. The first version shipped as a client-side flag check, and it flickered the old headline for about 200ms before swapping. On a slow phone in the field it looked broken. That flicker is what pushed us onto Edge Middleware, and once we were there the auth gating followed.
Edge Middleware runs before your page renders, on Vercel's edge network, close to the user. No cold start in the usual sense, and it sees the request before any cache lookup. That timing is the whole point.
The naive approach calls your auth service from middleware. Don't. That turns a 5ms edge decision into a 90ms transatlantic hop. Instead, verify a signed token at the edge and only fall back to the origin when you actually need fresh data.
We use a JWT stored in an httpOnly cookie, signed with a key we can verify at the edge using jose (the Web Crypto based library, since the edge runtime has no Node crypto).
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose';
const secret = new TextEncoder().encode(process.env.JWT_SECRET!);
export const config = {
matcher: ['/app/:path*', '/settings/:path*'],
};
export async function middleware(req: NextRequest) {
const token = req.cookies.get('session')?.value;
if (!token) {
return NextResponse.redirect(new URL('/login', req.url));
}
try {
const { payload } = await jwtVerify(token, secret, {
algorithms: ['HS256'],
});
const res = NextResponse.next();
res.headers.set('x-user-id', payload.sub as string);
return res;
} catch {
const res = NextResponse.redirect(new URL('/login', req.url));
res.cookies.delete('session');
return res;
}
}
Two things people miss here. First, the matcher config keeps middleware off static assets and public pages, so you're not paying the invocation cost on every favicon request. Second, passing x-user-id as a request header means the page handler doesn't re-verify the token, it just trusts the header middleware set. That saved us a second verify on every request.
Token lifetime is the tradeoff. A 15-minute JWT with a refresh token means a revoked session can stay valid for up to 15 minutes at the edge. For most apps that's fine. For anything touching money, keep the sensitive checks at the origin and use the edge only for the coarse "logged in or not" gate.
The flicker problem goes away when the assignment happens before render. Middleware reads a bucket cookie, and if it's missing, assigns one and rewrites to the correct variant path.
export async function middleware(req: NextRequest) {
const bucket = req.cookies.get('exp_homepage')?.value;
if (bucket) {
return NextResponse.rewrite(new URL(`/_variants/${bucket}`, req.url));
}
const assigned = Math.random() < 0.5 ? 'a' : 'b';
const res = NextResponse.rewrite(new URL(`/_variants/${assigned}`, req.url));
res.cookies.set('exp_homepage', assigned, {
maxAge: 60 * 60 * 24 * 30,
sameSite: 'lax',
});
return res;
}
rewrite is the key verb, not redirect. The URL in the browser stays /, but Vercel serves the variant page. The user never sees a bounce. Because assignment is sticky in the cookie, a returning visitor lands in the same bucket, which keeps your conversion metrics honest.
One caveat that bit us: the two variant pages need to be separately cacheable. If you use rewrite to a dynamic route, each variant gets its own cache entry. Check your Cache-Control and make sure the variant path isn't accidentally sharing a cache key with the canonical URL, or bucket A will start serving bucket B's HTML from cache.
Vercel exposes geo data on the request without an external lookup, which is handy for routing EU traffic to a consent flow.
const country = req.geo?.country ?? 'US';
if (country === 'DE' && !req.cookies.get('consent')) {
return NextResponse.rewrite(new URL('/consent', req.url));
}
We measured this end to end. The old client-side flag added roughly 180-220ms of visible flicker plus a hydration cost. The middleware version added about 4ms of p50 execution and 11ms at p99, measured off Vercel's own analytics. That's execution time, not user-visible delay, and it happens before the cache serves the page.
The cost angle is real too. Edge Middleware invocations are billed separately from function invocations. At our volume, roughly 40 million requests a month, tightening the matcher to skip public routes cut middleware invocations by about 60%, which shows up on the bill.
Put the coarse decisions at the edge: is this user logged in, which bucket are they in, which region. Keep anything that needs fresh authority (permissions, billing state, session revocation for sensitive actions) at the origin. If you try to make middleware do everything, you'll either call your API from the edge and lose the latency win, or you'll cache stale auth and ship a security bug. The sweet spot is narrow, and it's worth staying inside it.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
Both put SQLite near your users, but they solve replication and write latency very differently. We ran the same schema on both for a month and picked one.
We had long-lived AWS keys sitting in a datacenter we don't own. IAM Roles Anywhere let us delete every one of them. Here's the real setup.
Explore more articles in this category
We ran secrets three different ways across AWS, GCP, and Vault. External Secrets Operator gave us one Kubernetes-native workflow. Here's the setup and the gotchas.
Moving our fleet from x86 to Graviton promised 20% savings. We got 31%, but only after fixing native dependencies, a broken base image, and one nasty perf regression.
A p99 that jumped to 3.4 seconds during traffic ramps turned out to be cold starts. Here's how we measured them properly and cut the tail, with real init timings.
Evergreen posts worth revisiting.