Next.js + Vercel Stack6 min read

Next.js Rate Limiting: Upstash Redis Implementation Tutorial

Learn to implement production-ready API rate limiting in Next.js using Upstash Redis. Complete tutorial covering middleware setup, sliding window algorithms, and deployment best practices.

By John Hashem

Next.js Rate Limiting: Upstash Redis Implementation Tutorial

Building a production Next.js application without proper rate limiting is like leaving your front door wide open. API abuse, DDoS attacks, and resource exhaustion can kill your app before it gains traction. This tutorial walks you through implementing robust rate limiting using Upstash Redis and Next.js middleware.

Upstash Redis provides a serverless Redis solution that scales automatically with your Next.js application. Unlike traditional Redis hosting, you pay only for what you use, making it perfect for MVPs and growing applications. We'll implement a sliding window rate limiter that tracks requests over time periods, providing more accurate rate limiting than simple token buckets.

By the end of this guide, you'll have a production-ready rate limiting system that protects your API routes, handles edge cases gracefully, and scales with your traffic.

Prerequisites

Before starting, ensure you have:

  • A Next.js 13+ application with App Router
  • An Upstash account (free tier available)
  • Basic understanding of Next.js middleware
  • Node.js 18+ installed locally

Step 1: Set Up Upstash Redis Database

Create your Upstash Redis instance through their dashboard. Navigate to the Upstash console and click "Create Database". Choose a region close to your Vercel deployment region for optimal latency.

Once created, copy your database URL and token from the database details page. These credentials will connect your Next.js application to the Redis instance. Upstash provides REST API access, eliminating the need for persistent connections that don't work well in serverless environments.

Add your credentials to your environment variables:

UPSTASH_REDIS_REST_URL=your_database_url_here
UPSTASH_REDIS_REST_TOKEN=your_database_token_here

Step 2: Install Required Dependencies

Install the Upstash Redis SDK and necessary utilities:

npm install @upstash/redis @upstash/ratelimit

The @upstash/ratelimit package provides pre-built algorithms including sliding window, fixed window, and token bucket implementations. We'll use the sliding window algorithm for more precise rate limiting that doesn't suffer from the "burst at boundary" problem of fixed windows.

Create a Redis client configuration file at lib/redis.ts:

import { Redis } from '@upstash/redis'

export const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
})

Step 3: Create Rate Limiting Middleware

Next.js middleware runs at the edge, making it perfect for rate limiting before requests reach your API routes. Create middleware.ts in your project root:

import { NextRequest, NextResponse } from 'next/server'
import { Ratelimit } from '@upstash/ratelimit'
import { redis } from './lib/redis'

const ratelimit = new Ratelimit({
  redis: redis,
  limiter: Ratelimit.slidingWindow(10, '1 m'),
  analytics: true,
})

export async function middleware(request: NextRequest) {
  // Only apply rate limiting to API routes
  if (!request.nextUrl.pathname.startsWith('/api/')) {
    return NextResponse.next()
  }

  const ip = request.ip ?? '127.0.0.1'
  const { success, limit, reset, remaining } = await ratelimit.limit(ip)

  if (!success) {
    return new NextResponse('Rate limit exceeded', {
      status: 429,
      headers: {
        'X-RateLimit-Limit': limit.toString(),
        'X-RateLimit-Remaining': remaining.toString(),
        'X-RateLimit-Reset': new Date(reset).toISOString(),
      },
    })
  }

  const response = NextResponse.next()
  response.headers.set('X-RateLimit-Limit', limit.toString())
  response.headers.set('X-RateLimit-Remaining', remaining.toString())
  response.headers.set('X-RateLimit-Reset', new Date(reset).toISOString())

  return response
}

export const config = {
  matcher: '/api/:path*',
}

This configuration allows 10 requests per minute per IP address using a sliding window. The sliding window tracks requests over a rolling time period, providing smoother rate limiting compared to fixed windows that reset abruptly.

Step 4: Implement Custom Rate Limiting Logic

For more complex scenarios, you might need different limits for different endpoints or user types. Create a flexible rate limiting utility at lib/rate-limit.ts:

import { Ratelimit } from '@upstash/ratelimit'
import { redis } from './redis'

type RateLimitConfig = {
  requests: number
  window: string
  identifier: string
}

export async function checkRateLimit(config: RateLimitConfig) {
  const ratelimit = new Ratelimit({
    redis: redis,
    limiter: Ratelimit.slidingWindow(config.requests, config.window),
    analytics: true,
  })

  return await ratelimit.limit(config.identifier)
}

export const rateLimitConfigs = {
  api: { requests: 100, window: '1 h' },
  auth: { requests: 5, window: '15 m' },
  upload: { requests: 10, window: '1 h' },
} as const

This approach lets you define different rate limits for different API endpoints. Authentication endpoints get stricter limits to prevent brute force attacks, while general API endpoints allow more requests.

Use this in your API routes:

// app/api/auth/login/route.ts
import { checkRateLimit, rateLimitConfigs } from '@/lib/rate-limit'

export async function POST(request: Request) {
  const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1'
  
  const { success, reset, remaining } = await checkRateLimit({
    ...rateLimitConfigs.auth,
    identifier: `auth_${ip}`,
  })

  if (!success) {
    return Response.json(
      { error: 'Too many login attempts' },
      { status: 429 }
    )
  }

  // Continue with authentication logic
}

Step 5: Add User-Based Rate Limiting

IP-based rate limiting works for anonymous users, but authenticated users need more sophisticated tracking. Implement user-based rate limiting for better user experience:

import { auth } from '@/lib/auth' // Your auth system
import { checkRateLimit } from '@/lib/rate-limit'

export async function middleware(request: NextRequest) {
  if (!request.nextUrl.pathname.startsWith('/api/')) {
    return NextResponse.next()
  }

  const session = await auth(request)
  const ip = request.ip ?? '127.0.0.1'
  
  // Use user ID for authenticated users, IP for anonymous
  const identifier = session?.user?.id ? `user_${session.user.id}` : `ip_${ip}`
  
  // Authenticated users get higher limits
  const config = session?.user?.id 
    ? { requests: 1000, window: '1 h' }
    : { requests: 100, window: '1 h' }

  const result = await checkRateLimit({ ...config, identifier })

  if (!result.success) {
    return new NextResponse('Rate limit exceeded', { status: 429 })
  }

  return NextResponse.next()
}

Authenticated users typically get higher rate limits since they're less likely to be malicious. This approach also prevents users from bypassing limits by switching IP addresses.

Step 6: Handle Rate Limit Headers and Client Response

Proper rate limiting includes informative headers that help clients understand their usage. Implement comprehensive header management:

export function createRateLimitResponse(
  success: boolean,
  limit: number,
  remaining: number,
  reset: number
) {
  const headers = {
    'X-RateLimit-Limit': limit.toString(),
    'X-RateLimit-Remaining': remaining.toString(),
    'X-RateLimit-Reset': new Date(reset).toISOString(),
    'Retry-After': Math.ceil((reset - Date.now()) / 1000).toString(),
  }

  if (!success) {
    return new NextResponse(
      JSON.stringify({
        error: 'Rate limit exceeded',
        retryAfter: Math.ceil((reset - Date.now()) / 1000),
      }),
      {
        status: 429,
        headers: {
          ...headers,
          'Content-Type': 'application/json',
        },
      }
    )
  }

  return { headers }
}

These headers follow HTTP standards and help client applications implement proper backoff strategies. The Retry-After header tells clients exactly when they can make their next request.

Step 7: Deploy and Test Your Rate Limiting

Deploy your application to Vercel and test the rate limiting functionality. Use tools like curl or Postman to send rapid requests:

# Test rate limiting
for i in {1..15}; do
  curl -i https://your-app.vercel.app/api/test
  sleep 1
done

Monitor your Upstash dashboard to see request patterns and adjust limits based on real usage. The analytics feature provides insights into which IPs or users are hitting limits most frequently.

For production monitoring, consider implementing alerts when rate limits are frequently exceeded. This could indicate either abuse or the need to adjust your limits for legitimate usage patterns.

Common Mistakes and Troubleshooting

One frequent mistake is applying rate limiting too broadly, which can hurt user experience. Avoid rate limiting static assets or pages that don't consume significant resources. Focus on API endpoints, especially those that perform database operations or external API calls.

Another common issue is not handling the distributed nature of edge functions. Each edge location maintains its own rate limit counters, which can lead to higher effective limits than expected. If you need strict global limits, consider implementing a centralized counting mechanism or accept that edge-distributed limits provide approximate protection.

IP address detection can be tricky behind proxies and CDNs. Always check multiple headers for the real client IP:

function getClientIP(request: NextRequest): string {
  const forwarded = request.headers.get('x-forwarded-for')
  const realIP = request.headers.get('x-real-ip')
  const cfConnectingIP = request.headers.get('cf-connecting-ip')
  
  return cfConnectingIP || realIP || forwarded?.split(',')[0] || '127.0.0.1'
}

Next Steps

With basic rate limiting implemented, consider these enhancements for production applications. Add rate limit bypass for trusted IPs or API keys for your own services. Implement different limits based on subscription tiers or user roles.

Consider implementing more sophisticated algorithms like token bucket for burst handling or adaptive rate limiting that adjusts based on system load. For high-traffic applications, you might need to implement rate limiting at the CDN level using services like Cloudflare.

Monitor your rate limiting effectiveness through metrics and adjust limits based on actual usage patterns. Remember that rate limiting should protect your system without significantly impacting legitimate users. The goal is finding the right balance for your specific application and user base.

For more advanced Next.js security patterns, check out our guide on Next.js Middleware Authentication: Protecting Routes in 2025. If you're building this as part of an MVP, our MVP Database Design: Essential Schema Patterns for Fast Launch covers complementary backend architecture decisions.

Ready to build something great?

Let's talk about your project. I offer 1-week MVP sprints, fractional CTO services, and Claude Code consulting.

View All Services