billing hacks
Edge & MiddlewareMiddleware Patterns

Middleware Patterns

Middleware runs on every request. Get the matcher wrong and costs explode.

Cost Trap

Middleware runs on every request - including static assets, images, API calls. A page with 30 assets = 30 middleware executions. Get the matcher wrong and costs explode.

MIDDLEWARE EXECUTES ON EVERY REQUESTPage Loadpage.htmlstyles.cssbundle.jslogo.pnghero.jpg...+25 more assetsmiddleware.ts30 executions per page!vs/dashboard/*/api/*middleware.ts2 executions onlystatic assetsskipped ✓

The Matcher Is Everything

// ❌ BAD: Runs on everything
export const config = {
  matcher: '/:path*',
};

// ⚠️ BETTER: Excludes static
export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\..*).*)'],
};

// ✅ BEST: Only paths that need it
export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*'],
};

Pattern: Geo Routing

export function middleware(request: NextRequest) {
  const country = request.geo?.country || 'US';

  if (['DE', 'AT', 'CH'].includes(country)) {
    return NextResponse.rewrite(new URL('/de' + request.nextUrl.pathname, request.url));
  }
  return NextResponse.next();
}

export const config = {
  matcher: ['/((?!_next|favicon.ico|.*\\..*).*)',],
};

Pattern: A/B Testing

export function middleware(request: NextRequest) {
  let bucket = request.cookies.get('ab-bucket')?.value;

  if (!bucket) {
    bucket = Math.random() > 0.5 ? 'control' : 'variant';
    const response = NextResponse.next();
    response.cookies.set('ab-bucket', bucket, { maxAge: 60 * 60 * 24 * 30 });
    return response;
  }

  if (bucket === 'variant' && request.nextUrl.pathname === '/pricing') {
    return NextResponse.rewrite(new URL('/pricing-v2', request.url));
  }

  return NextResponse.next();
}

Pattern: Bot Blocking

const BLOCKED = ['AhrefsBot', 'SemrushBot', 'DataForSeoBot'];

export function middleware(request: NextRequest) {
  const ua = request.headers.get('user-agent') || '';

  if (BLOCKED.some(bot => ua.includes(bot))) {
    return new NextResponse('Blocked', { status: 403 });
  }
  return NextResponse.next();
}

Saves money by preventing scrapers from hitting your serverless functions.

Pattern: Auth Guard

middleware.ts
export function middleware(request: NextRequest) {
  // Quick check only
  const token = request.cookies.get('session')?.value;

  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*'],
};

Do full token verification in the page (serverless), not middleware.

Audit Checklist

  1. Check Analytics → Edge Requests - burning through 10M quota?
  2. Review matcher - is it too broad?
  3. Question necessity - can auth be in layout? Redirects in next.config.js?

Do You Even Need Middleware?

Ask yourself:

  • Can auth be checked in a layout instead?
  • Can redirects go in next.config.js?
  • Can geo logic be client-side for non-critical cases?

Often the answer is yes, and you save edge requests.


With runtime decisions made, see Caching for the highest-leverage optimization.

On this page