All articles
DevOps & CloudJuly 25, 2026 6 min read

Vercel Edge Middleware Latency: What We Measured When We Moved Auth to the Edge

We moved auth checks from a Node API route to Vercel Edge Middleware expecting free speed. Some routes got faster, some got slower, and the bill moved in ways we didn't predict.

We spent a quarter moving authentication and feature-flag checks from a Node.js API route into Vercel Edge Middleware. The pitch on paper was tidy: run auth closer to the user, cut a network hop, feel fast. The reality was more interesting. Some routes got noticeably faster. Others got slower under specific conditions we didn't anticipate. And the invoice moved in a direction we hadn't modelled.

This is a field report, not a benchmark shootout. Numbers here are from our own traffic patterns — a B2B SaaS with roughly 40% North America, 35% EU, and the rest split across APAC and LATAM. Your mileage will differ. That's kind of the point.

Why we moved auth to the edge in the first place

Our previous setup was standard: a Next.js app on Vercel, with /api/* routes running as regional Serverless Functions in iad1 (US East). Every authenticated request hit that region to validate a JWT, look up a session in Upstash Redis, and then either proxy to our backend or render server components.

The pain points:

  • EU and APAC users ate a full transatlantic or transpacific round trip just to be told they were logged in.
  • Our p75 TTFB for authenticated pages sat in the 380–520ms range depending on region.
  • Cold starts on the auth route showed up as occasional 900ms+ spikes in Sentry.

Edge Middleware runs on Vercel's edge network (built on Cloudflare Workers infrastructure), close to the user, with much smaller cold-start behaviour. The theory: verify the JWT at the edge, attach user context via headers, and only hit the origin when we actually need to.

What actually got faster

For the simple case — validate a signed JWT, check an allowlist, redirect if unauthenticated — the edge was clearly better.

Rough p75 TTFB changes we saw on protected marketing and dashboard shell routes:

  • North America: ~310ms → ~180ms
  • Western Europe: ~470ms → ~150ms
  • Sydney / Singapore: ~620ms → ~210ms

Those are meaningful. The EU number is the one that mattered most to us commercially, because it lined up with the segment we were trying to grow.

The middleware itself was small:

// middleware.ts
import { NextResponse, type NextRequest } from 'next/server';
import { jwtVerify } from 'jose';

const SECRET = new TextEncoder().encode(process.env.JWT_SECRET!);

export const config = {
  matcher: ['/app/:path*', '/api/protected/: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', String(payload.sub));
    res.headers.set('x-org-id', String(payload.org));
    return res;
  } catch {
    return NextResponse.redirect(new URL('/login?expired=1', req.url));
  }
}

No database, no external fetch, no session lookup. Just cryptographic verification of a token we issued. That's the shape of workload the edge is genuinely good at.

Where it got worse — and why

The trouble started when we tried to be thorough about auth. Two patterns bit us.

Pattern 1: Session revocation checks

We wanted logout-everywhere and admin-triggered session revocation to be honoured within seconds, not minutes. That meant middleware had to check a revocation list — a Redis SISMEMBER against a set of revoked JTIs.

We used Upstash Redis with the global replica option so the edge could hit a nearby PoP. In practice:

  • When the user was near a replica: added roughly 15–35ms. Fine.
  • When they weren't (São Paulo hitting a Virginia replica, for example): 180–260ms added. Not fine.
  • On middleware cold-ish invocations in less-trafficked regions, we saw occasional 400ms+ outliers.

Suddenly middleware was doing a network call on every protected request, from a location we didn't fully control, with p99 behaviour that was worse than our old regional setup for the unlucky users.

The fix wasn't clever, it was structural: we moved revocation checks out of the hot path and accepted a bounded staleness window. JWTs got shorter lifetimes (5 minutes), and revocation happened at refresh time in a regional function. Middleware went back to pure crypto.

Pattern 2: Feature flags with user targeting

We also tried to resolve feature flags in middleware so the origin could render the right variant without a flicker. The flag SDK did a fetch to the vendor's edge API. That fetch, cached aggressively, was usually 5–20ms. Usually.

When the cache missed — after a deploy, after a flag update, or when the edge region hadn't warmed up — we saw 150–300ms tacked onto TTFB. Sentry showed a distinctive bimodal distribution: fast, or noticeably slow, rarely in between.

We ended up resolving flags at the origin for server components and only doing edge resolution for a small set of routing decisions (A/B splits on marketing pages) where staleness was tolerable.

The bill moved sideways

Edge Middleware invocations are billed differently from Serverless Function invocations, and the matcher pattern matters more than people realise. Our first cut used matcher: ['/((?!_next/static|favicon.ico).*)'] — essentially "everything." That meant middleware ran on every asset request that wasn't statically excluded, including a lot of prefetch traffic from next/link.

Our middleware invocation count in the first week was roughly 6x our previous API auth invocation count. The unit cost is lower, but 6x lower unit cost isn't lower cost.

Tightening the matcher to only the routes that actually needed auth cut invocations by around 70%. The lesson: treat the middleware matcher as a production configuration, not a convenience. Review it the way you'd review an IAM policy.

What we instrument now

OpenTelemetry support at the edge is workable but not luxurious. We settled on emitting a compact custom span from middleware via a fire-and-forget POST to our collector, with these attributes:

  • edge.region (from request.geo and the runtime hint)
  • middleware.duration_ms
  • auth.result (ok | expired | missing | invalid)
  • matcher.path_group (bucketed, not raw path)

We don't try to propagate full trace context from middleware to origin functions on every request — the overhead wasn't worth it. Instead we sample: about 5% of authenticated requests carry a traceparent header injected by middleware, and those get end-to-end traces in our backend. The other 95% get middleware-local metrics only.

For errors, Sentry's edge runtime SDK works, but we keep it minimal. Verbose breadcrumbs at the edge add real weight.

A short checklist before you move auth to the edge

If you're considering this migration, the questions we wish we'd asked upfront:

  1. Is your auth check pure? JWT verification with a static secret or JWKS: yes. Session lookup, revocation check, permission fetch: probably no, or only with careful design.
  2. What's your matcher going to actually match? Count expected invocations honestly, including prefetches and asset requests.
  3. Where are your users? If you're 90% one region, the edge win is smaller than the operational cost.
  4. What's your staleness tolerance for revocation and flags? If the answer is "zero," the edge is fighting you.
  5. Can you observe it? Middleware failures are harder to debug than function failures. Wire up telemetry before you ship, not after.

Where we'd start next time

If we were doing this again from scratch, we'd move only the JWT verification and the unauthenticated-redirect logic to Edge Middleware on day one. Nothing else. We'd keep session revocation, permission checks, and feature flag resolution at the origin until we had numbers proving each one was worth the edge round trip and the invocation cost.

The edge is a good place for cheap, pure, latency-sensitive work. It is a bad place for anything that needs strong consistency with a database in a specific region. Most auth systems are a mix of both, and the useful engineering is deciding — per check, not per system — which side of that line each piece sits on.

If you're working through a similar migration and want a second pair of eyes on the architecture, our team writes about this kind of thing regularly on the 72Technologies blog, and we help clients ship it as part of our DevOps and cloud work.

#Vercel#Edge#Performance#Next.js#Auth

Want a team like ours?

72Technologies builds production software for the kind of teams who actually read this blog.

Start a project