RA
RenovateAPIEngineering Hub
Frontend & Web Architecture

Your Next.js Proxy Is Probably Adding 300ms to Every Request

How to build a fast, edge-compatible proxy.ts in Next.js App Router — stateless JWT auth with jose, HTTP-based Redis rate limiting, and a matcher config that stops running on every image request.

AAbhishek6 min read
Frontend & Web Architecture6 min read

Your Next.js Proxy Is Probably Adding 300ms to Every Request

RENOVATEAPI ARCHITECTURAL SPEC
CANONICAL GUIDE

If your proxy.ts file makes a database call, you've already lost 150-350ms before your page even starts rendering. That's not an edge case — it's the single most common reason Next.js apps ship slow TTFB despite doing everything else right.

The proxy file (what most of us still call middleware out of habit) runs before every request completes. That makes it a great place for auth checks, geolocation redirects, and rate limiting. It also makes it a terrible place for anything that touches the network synchronously, because whatever latency you add there gets charged to every single request that matches your config.

Here's how to build one that costs less than 3ms.

The three ways proxy files go wrong

Three patterns show up over and over in production Next.js apps, and they compound:

  1. Network latency waterfalls. Calling an external API or database directly inside the proxy on every page load, adding 150-400ms to TTFB before anything else happens.
  2. No matcher filtering. The proxy runs on static assets, _next/static bundles, and favicon requests it was never meant to touch.
  3. Edge runtime incompatibility. Importing Node built-ins like fs, crypto, or a native database driver — none of which exist in the V8-based edge runtime, so they fail at request time, not build time.

Fix these three and you've solved most of what makes edge proxies slow.

Verify sessions with zero network hops

The fix for the first problem is to stop checking sessions against a database at all. Instead, sign a JWT when the user logs in, and verify it cryptographically, in-memory, on every request after that. No round trip, no waterfall.

jose is the library for this — it's built for edge runtimes and doesn't drag in Node-specific crypto internals.

// proxy.ts

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { jwtVerify } from 'jose';

const JWT_SECRET = new TextEncoder().encode(
  process.env.JWT_SECRET_KEY || 'default-secret-key-32-chars-long'
);

const PUBLIC_PATHS = ['/login', '/signup', '/api/auth', '/pricing'];

export async function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // Skip public unauthenticated routes
  if (PUBLIC_PATHS.some((path) => pathname.startsWith(path))) {
    return NextResponse.next();
  }

  // Extract session token from cookies
  const sessionToken = request.cookies.get('session_token')?.value;

  if (!sessionToken) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('redirect', pathname);
    return NextResponse.redirect(loginUrl);
  }

  try {
    // Verify the JWT in-memory — zero network latency
    const { payload } = await jwtVerify(sessionToken, JWT_SECRET, {
      algorithms: ['HS256'],
    });

    // Forward verified user claims downstream via request headers
    const requestHeaders = new Headers(request.headers);
    requestHeaders.set('x-user-id', payload.sub as string);
    requestHeaders.set('x-user-role', (payload.role as string) || 'user');

    return NextResponse.next({
      request: { headers: requestHeaders },
    });
  } catch (error) {
    // Expired or tampered token: bounce to login
    const response = NextResponse.redirect(new URL('/login', request.url));
    response.cookies.delete('session_token');
    return response;
  }
}

// Strict matcher: exclude static files, images, and internal Next.js assets
export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
};

Notice the matcher does double duty here. It's not just an optimization bolted on afterward — it's what stops your auth check from running on every single image and font request on the page. Skip it and you're paying the (small, but nonzero) cost of a proxy invocation on assets that have nothing to do with auth.

Rate limit at the edge without a TCP client

Session checks aren't the only place teams reach for a database inside the proxy. Rate limiting is the other big one, and the same rule applies: a traditional Redis client opening a TCP socket on every request will blow your latency budget just as fast as a Postgres query would.

The fix is an HTTP-based Redis client. @upstash/ratelimit paired with @upstash/redis talks to Redis over REST instead of a persistent socket, which is what makes it edge-compatible in the first place.

import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

// Lightweight HTTP REST client, compatible with the edge runtime
const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(20, '10 s'), // 20 requests per 10 seconds
});

export async function applyRateLimit(request: NextRequest): Promise<NextResponse | null> {
  const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1';
  const { success, limit, remaining, reset } = await ratelimit.limit(`ratelimit_${ip}`);

  if (!success) {
    return new NextResponse('Too Many Requests', {
      status: 429,
      headers: {
        'X-RateLimit-Limit': limit.toString(),
        'X-RateLimit-Remaining': remaining.toString(),
        'X-RateLimit-Reset': reset.toString(),
      },
    });
  }

  return null;
}

Call this from inside proxy.ts on the routes that need it — login endpoints and public APIs are the obvious candidates. Applying it globally is usually overkill and just adds a Redis round trip to routes that don't need protecting.

What each approach actually costs

Here's the tradeoff laid out plainly, because "just use JWTs" only makes sense once you see the gap:

Strategy Network hops Added latency Edge-compatible? Core Web Vitals impact
Sync database session check 1-2 +150-350ms No — driver fails Severe TTFB degradation
External auth REST endpoint 1 +80-200ms Yes Moderate TTFB delay
Stateless jose JWT verification 0 <3ms Yes Effectively none
Unfiltered static-asset matcher 0 +10ms per file Yes Unnecessary compute cost, no functional benefit

The database check isn't just slower — it's not even a valid option on the edge runtime, since native drivers don't run there. If your proxy currently does this, it's not a performance tweak you're deferring, it's a bug you haven't hit yet.

The pattern that ties it together

Stateless verification, HTTP-based rate limiting, and a tight matcher aren't three separate optimizations you pick and choose from. They're the same principle applied three times: keep the proxy file's own execution cost close to zero, and push anything expensive to somewhere it can be cached or amortized.

Get the matcher wrong and you're running auth logic on your favicon. Get the JWT verification wrong (or skip it in favor of a database check) and you've reintroduced the exact latency waterfall the edge runtime was supposed to eliminate. Get the rate limiter wrong and you've added a TCP handshake to your login page.

None of these are hard to fix individually. The mistake is treating them as unrelated — sort out the matcher, then the auth strategy, then rate limiting, and there's genuinely nothing left in proxy.ts that touches the network on a cold path.

RenovateAPI Engineering Suite

Accelerate your Frontend & Web Architecture Modernization Roadmap

Need custom architecture auditing, automated OpenAPI contract generation, or zero-downtime microservice migration guidance for your engineering team?

Weekly Engineering Dispatch

Subscribe to RenovateAPI

Get weekly architectural guides, API refactoring strategies, and technical SEO updates delivered directly to your inbox.

Discussion (2)

A
Alex Rivera
2 hours ago

Extremely helpful breakdown of the Strangler Fig pattern! We're currently refactoring a legacy Java monolith at work and the OpenAPI gateway routing tips saved us weeks of experimentation.

S
Sophia Chen
1 day ago

The schema JSON-LD and FAQ block structure really helps with indexing. Great technical detail on entity mentions too.

Suggested Related Articles