Next.js Rate Limiting: ограничение запросов к API

16 октября 2026
Автор

Антон Ларичев

Что такое Rate Limiting и зачем он нужен

Rate Limiting (ограничение частоты запросов) — механизм защиты API, который ограничивает количество запросов от одного источника за определённый промежуток времени. Без него любой желающий может отправить миллионы запросов к вашему API, что приведёт к перегрузке сервера, увеличению расходов и деградации сервиса для легитимных пользователей.

Типичные сценарии, где rate limiting необходим:

  • Форма регистрации или авторизации — защита от брутфорса паролей
  • Отправка email-уведомлений — предотвращение спама
  • Платные API-операции (OpenAI, платёжные шлюзы) — контроль расходов
  • Публичные эндпоинты — защита от автоматизированного скрейпинга
  • Любые вычислительно-тяжёлые операции — стабильность под нагрузкой

Курсы PurpleSchool

Учитесь у практикующих разработчиков — без воды

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Стратегии ограничения запросов

Существует несколько алгоритмов rate limiting, каждый со своими характеристиками:

Fixed Window (фиксированное окно) — считает запросы в фиксированных временных окнах (например, 100 запросов в минуту). Прост в реализации, но уязвим к всплескам на границе окон.

Sliding Window (скользящее окно) — более точная версия, учитывает запросы за последние N секунд относительно текущего момента. Устраняет проблему граничных всплесков.

Token Bucket (ведро токенов) — клиент получает токены с фиксированной скоростью, каждый запрос тратит токен. Позволяет короткие всплески, сохраняя среднюю скорость.

Leaky Bucket (дырявое ведро) — запросы обрабатываются с постоянной скоростью, очередь ограничена. Обеспечивает максимально ровный поток.

Для большинства веб-приложений оптимальны Sliding Window или Token Bucket.

Идентификация источника запроса

Прежде чем ограничивать запросы, нужно понять, кого ограничивать. Стандартные подходы:

import { NextRequest } from 'next/server'

function getIdentifier(request: NextRequest): string {
  // По IP-адресу (для публичных эндпоинтов)
  const forwarded = request.headers.get('x-forwarded-for')
  const ip = forwarded ? forwarded.split(',')[0].trim() : '127.0.0.1'

  // По API-ключу (для аутентифицированных клиентов)
  const apiKey = request.headers.get('x-api-key')
  if (apiKey) return `api:${apiKey}`

  // По токену сессии (для залогиненных пользователей)
  const sessionToken = request.cookies.get('session')?.value
  if (sessionToken) return `session:${sessionToken}`

  return `ip:${ip}`
}

Важно: за реверс-прокси (Nginx, Vercel, Cloudflare) реальный IP клиента приходит в заголовке x-forwarded-for. Заголовку нельзя безоговорочно доверять — клиент может подделать его. На Vercel используйте x-real-ip или пакет @vercel/functions.

Простой in-memory rate limiter

Для несерьёзных проектов или локальной разработки подойдёт хранилище в памяти процесса. Главный недостаток — не работает при нескольких экземплярах приложения (нет общего состояния).

// lib/rate-limit.ts

interface RateLimitEntry {
  count: number
  resetAt: number
}

const store = new Map<string, RateLimitEntry>()

export function rateLimit(
  identifier: string,
  limit: number,
  windowMs: number
): { success: boolean; remaining: number; resetAt: number } {
  const now = Date.now()
  const entry = store.get(identifier)

  if (!entry || now > entry.resetAt) {
    const resetAt = now + windowMs
    store.set(identifier, { count: 1, resetAt })
    return { success: true, remaining: limit - 1, resetAt }
  }

  if (entry.count >= limit) {
    return { success: false, remaining: 0, resetAt: entry.resetAt }
  }

  entry.count += 1
  return { success: true, remaining: limit - entry.count, resetAt: entry.resetAt }
}

Применение в Route Handler (App Router):

// app/api/contact/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { rateLimit } from '@/lib/rate-limit'

export async function POST(request: NextRequest) {
  const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1'
  const result = rateLimit(ip, 5, 60_000) // 5 запросов в минуту

  if (!result.success) {
    return NextResponse.json(
      { error: 'Слишком много запросов. Повторите позже.' },
      {
        status: 429,
        headers: {
          'Retry-After': String(Math.ceil((result.resetAt - Date.now()) / 1000)),
          'X-RateLimit-Limit': '5',
          'X-RateLimit-Remaining': '0',
        },
      }
    )
  }

  // Обработка запроса...
  return NextResponse.json({ success: true })
}

Rate Limiting через Middleware

Middleware в Next.js выполняется на Edge Runtime до того, как запрос достигнет Route Handler. Это идеальное место для rate limiting — можно блокировать запросы ещё на входе, не нагружая обработчики.

// middleware.ts
import { NextRequest, NextResponse } from 'next/server'

const rateLimitMap = new Map<string, { count: number; resetAt: number }>()

function checkRateLimit(identifier: string, limit: number, windowMs: number): boolean {
  const now = Date.now()
  const entry = rateLimitMap.get(identifier)

  if (!entry || now > entry.resetAt) {
    rateLimitMap.set(identifier, { count: 1, resetAt: now + windowMs })
    return true
  }

  if (entry.count >= limit) return false

  entry.count += 1
  return true
}

export function middleware(request: NextRequest) {
  // Применяем rate limiting только к API-маршрутам
  if (!request.nextUrl.pathname.startsWith('/api/')) {
    return NextResponse.next()
  }

  const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1'
  const allowed = checkRateLimit(ip, 100, 60_000) // 100 запросов в минуту

  if (!allowed) {
    return new NextResponse('Too Many Requests', { status: 429 })
  }

  return NextResponse.next()
}

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

Продакшн-решение с Redis и Upstash

Для продакшн-приложений с несколькими инстансами нужно внешнее хранилище состояния. Redis — стандарт де-факто для rate limiting благодаря атомарным операциям и встроенному TTL.

Upstash предоставляет serverless Redis, который работает с Edge Runtime Next.js. Установите зависимости:

npm install @upstash/ratelimit @upstash/redis

Настройте переменные окружения в .env.local:

UPSTASH_REDIS_REST_URL=https://your-url.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-token

Создайте конфигурацию rate limiter:

// lib/rate-limiter.ts
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'

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

// Sliding window: 10 запросов за 10 секунд
export const apiLimiter = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(10, '10 s'),
  analytics: true,
  prefix: 'api',
})

// Строже для чувствительных эндпоинтов: 5 запросов в минуту
export const authLimiter = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(5, '1 m'),
  analytics: true,
  prefix: 'auth',
})

// Token bucket для публичного API: позволяет кратковременные всплески
export const publicApiLimiter = new Ratelimit({
  redis,
  limiter: Ratelimit.tokenBucket(20, '1 m', 5),
  analytics: true,
  prefix: 'public',
})

Применение в Middleware с Upstash:

// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import { apiLimiter, authLimiter } from '@/lib/rate-limiter'

export async function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname

  if (!pathname.startsWith('/api/')) {
    return NextResponse.next()
  }

  const ip = request.headers.get('x-forwarded-for') ?? '127.0.0.1'

  // Отдельный лимит для эндпоинтов авторизации
  const limiter = pathname.startsWith('/api/auth') ? authLimiter : apiLimiter

  const { success, limit, remaining, reset } = await limiter.limit(ip)

  const response = success
    ? NextResponse.next()
    : NextResponse.json(
        { error: 'Слишком много запросов', retryAfter: Math.ceil((reset - Date.now()) / 1000) },
        { status: 429 }
      )

  response.headers.set('X-RateLimit-Limit', String(limit))
  response.headers.set('X-RateLimit-Remaining', String(remaining))
  response.headers.set('X-RateLimit-Reset', String(reset))

  return response
}

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

Гранулярный rate limiting на уровне Route Handler

Иногда нужны разные лимиты для разных операций внутри одного маршрута или лимиты по пользователю (не по IP):

// app/api/generate/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'

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

// Для платных AI-операций — жёсткий лимит по пользователю
const generateLimiter = new Ratelimit({
  redis,
  limiter: Ratelimit.fixedWindow(10, '1 h'),
  prefix: 'generate',
})

export async function POST(request: NextRequest) {
  const session = await getServerSession()

  if (!session?.user?.id) {
    return NextResponse.json({ error: 'Требуется авторизация' }, { status: 401 })
  }

  const { success, remaining } = await generateLimiter.limit(session.user.id)

  if (!success) {
    return NextResponse.json(
      {
        error: 'Исчерпан лимит генераций на этот час',
        remaining: 0,
      },
      { status: 429 }
    )
  }

  const body = await request.json()
  // Вызов OpenAI или другой AI-операции...

  return NextResponse.json({ result: '...', remaining })
}

Ответ клиенту при превышении лимита

Правильный ответ на 429 включает заголовки, помогающие клиенту понять, когда повторить запрос:

// lib/rate-limit-response.ts
import { NextResponse } from 'next/server'

interface RateLimitHeaders {
  limit: number
  remaining: number
  reset: number
}

export function rateLimitExceededResponse(headers: RateLimitHeaders): NextResponse {
  const retryAfter = Math.ceil((headers.reset - Date.now()) / 1000)

  return NextResponse.json(
    {
      error: 'Too Many Requests',
      message: `Превышен лимит запросов. Повторите через ${retryAfter} секунд.`,
      retryAfter,
    },
    {
      status: 429,
      headers: {
        'Retry-After': String(retryAfter),
        'X-RateLimit-Limit': String(headers.limit),
        'X-RateLimit-Remaining': '0',
        'X-RateLimit-Reset': String(Math.ceil(headers.reset / 1000)),
      },
    }
  )
}

Обработка на стороне клиента:

// hooks/useApiRequest.ts
async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 1) {
  const response = await fetch(url, options)

  if (response.status === 429 && maxRetries > 0) {
    const retryAfter = Number(response.headers.get('Retry-After') ?? 5)
    await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000))
    return fetchWithRetry(url, options, maxRetries - 1)
  }

  return response
}

Тестирование rate limiting

Не забудьте покрыть rate limiting тестами — особенно логику граничных случаев:

// __tests__/rate-limit.test.ts
import { rateLimit } from '@/lib/rate-limit'

describe('rateLimit', () => {
  beforeEach(() => {
    jest.useFakeTimers()
  })

  afterEach(() => {
    jest.useRealTimers()
  })

  it('разрешает запросы в пределах лимита', () => {
    for (let i = 0; i < 5; i++) {
      const result = rateLimit('test-user', 5, 60_000)
      expect(result.success).toBe(true)
    }
  })

  it('блокирует запросы сверх лимита', () => {
    for (let i = 0; i < 5; i++) {
      rateLimit('test-user-2', 5, 60_000)
    }
    const result = rateLimit('test-user-2', 5, 60_000)
    expect(result.success).toBe(false)
    expect(result.remaining).toBe(0)
  })

  it('сбрасывает счётчик после истечения окна', () => {
    for (let i = 0; i < 5; i++) {
      rateLimit('test-user-3', 5, 60_000)
    }

    jest.advanceTimersByTime(61_000)

    const result = rateLimit('test-user-3', 5, 60_000)
    expect(result.success).toBe(true)
    expect(result.remaining).toBe(4)
  })
})

Лучшие практики

Устанавливайте заголовки всегда, даже для успешных запросов — клиенты смогут реализовать превентивное замедление запросов до достижения лимита.

Разделяйте лимиты по типам операций: чтение данных допускает больше запросов, чем запись; AI-генерация — строже, чем получение профиля.

Не применяйте rate limiting к статике — middleware должен срабатывать только для /api/ маршрутов, иначе вы замедлите загрузку страниц.

Логируйте превышения лимитов — это ценные данные для выявления атак и настройки порогов.

Предусмотрите обход для доверенных источников — внутренние сервисы, CI/CD, health-checks должны быть в allowlist:

const TRUSTED_IPS = new Set([
  process.env.INTERNAL_SERVICE_IP,
  '127.0.0.1',
])

if (TRUSTED_IPS.has(ip)) {
  return NextResponse.next()
}

Используйте разные Redis-ключи для разных контекстов — через параметр prefix в Upstash Ratelimit или вручную при работе с нативным Redis, чтобы лимиты не пересекались.

Комбинируйте с защитой Cloudflare или аналогами — rate limiting на уровне приложения — второй эшелон защиты, а не единственный. Cloudflare, AWS WAF или Nginx снимут нагрузку ещё до Next.js.

Мониторинг эффективности

Upstash Ratelimit с параметром analytics: true сохраняет статистику в Redis. Можно выводить дашборд:

// app/api/admin/rate-limit-stats/route.ts
import { NextResponse } from 'next/server'
import { apiLimiter } from '@/lib/rate-limiter'

export async function GET() {
  const analytics = await apiLimiter.getAnalytics()
  return NextResponse.json(analytics)
}

Или подключить внешний мониторинг, добавив счётчики в Prometheus/Datadog при каждом срабатывании rate limit.


Rate limiting — важная часть production-готового Next.js приложения. Освоить построение надёжных fullstack-приложений с Next.js, включая защиту API, аутентификацию и деплой, можно на курсе PurpleSchool:

Next.js — полный курс

Стрелочка влевоRoute Handlers в Next.js App RouterNext.js с OpenAI API — ИИ-функции в приложенииСтрелочка вправо

Все гайды по Next-js

useSearchParams и usePathname в Next.js App RouterStreaming и Suspense в Next.jsServer Actions в Next.jsrevalidateTag и revalidatePath — инвалидация кэша в Next.jsPrisma ORM в Next.jsnotFound и redirect в серверных компонентах Next.jsNext.js с Supabase — авторизация и база данныхNext.js с GraphQL и Apollo ClientNext.js 15: новые возможности и breaking changesБлог и документация на Next.js с MDXloading.js и error.js в Next.js App RouterАутентификация с JWT в App Router без сторонних библиотекИнтернационализация i18n в Next.js App RoutergenerateStaticParams — статическая генерация динамических маршрутовNext.js cookies и headers в App RouterNext.js с Clerk: аутентификация пользователейNext.js after() — выполнение кода после отправки ответа
Открыть базу знаний

Отправить комментарий