PAS7 Studio

Rate limiting у Bun.js: in-memory, Redis, sliding window і edge cases для production API

Практичний deep dive по rate limiting middleware у Bun.js: fixed window, sliding window, token bucket, Redis, distributed limits, 429 Retry-After, abuse protection, Hono/Elysia інтеграції, best practices і погані практики.

14 трав. 2026 р.· 13 хв читання· Технології
Кому підійдеBackend інженериFull-stack розробникиTech leadsКоманди, що будують public API, SaaS або webhook endpoints на Bun
Технічна ілюстрація rate limiting pipeline у Bun.js з лічильниками, Redis і контрольованим потоком запитів
Гайд / СеріяСтаття серії

Bun.js Middleware Production Guide 2026

Серія про production middleware у Bun.js: огляд, безпека, performance, observability, rate limiting, body parsing, WebSocket/SSE і тестування request pipeline.

Наївний ліміт “100 запитів на IP за хвилину” виглядає нормально рівно до першого офісу за NAT, першого мобільного оператора, першого webhook retry storm або першого enterprise tenant з сотнею користувачів за одним egress IP.

Rate limiting має відповідати не тільки на питання “скільки запитів?”, а й на питання “хто саме лімітується?”, “який route?”, “який tenant?”, “який credential?”, “що робити, коли Redis недоступний?”, “чи є Retry-After?”, “чи не ламаємо ми нормальний burst?”.

Bun робить HTTP layer швидким, але rate limiting завжди впирається в дизайн ключів, атомарність storage і політику відмови. Саме це і розбираємо.

In-memory limiter підходить тільки для одного процесу або локального baseline.
Redis потрібен, коли API має кілька інстансів або горизонтальне масштабування.
Sliding window і token bucket вирішують різні проблеми.
429 без Retry-After ускладнює життя добрим клієнтам.

Це третій chapter серії про Bun middleware. Після overview і auth логічно перейти до abuse protection: саме rate limiting часто стоїть між вашим API і дорогою хвилею зайвих запитів.

Ментальна модель rate limiting middleware: identify key, choose policy, check counter, allow або reject.
Fixed window, sliding window і token bucket: де кожен алгоритм працює добре, а де створює edge cases. [5][6]
Native Bun приклад in-memory limiter для одного процесу.
Redis-backed limiter для distributed API і чому потрібна атомарність. [5][6]
Hono/Elysia варіанти: коли брати ready-made middleware/plugin. [1][2]
HTTP 429 Too Many Requests і Retry-After: що має отримати клієнт. [7]
Погані практики: IP-only limits, global limits для всіх routes, fail-open без alerts, plaintext API key у limiter key.

Rate limiter має бути коротким state machine, а не випадковою перевіркою в середині handler. Якщо розкласти його на кроки, більшість помилок стає видимою ще до коду.

01

Визначити identity key

Це може бути user id, API key id, tenant id, route group, IP або комбінація. Для authenticated API key зазвичай кращий ключ apiKeyId + routeGroup, ніж просто IP.

02

Вибрати policy

Різні routes потребують різних лімітів: login, search, export, webhook, public read, admin mutation. Один глобальний ліміт майже завжди або занадто слабкий, або занадто агресивний.

03

Атомарно оновити counter

В одному процесі це Map. У distributed API це Redis або інший shared store. Для Redis важливо, щоб increment + expiry або sliding-window update були атомарними.

04

Повернути корисний 429

Клієнт має отримати стабільний JSON error, Retry-After і бажано rate-limit headers. Інакше добрі клієнти не знають, коли повторювати запит.

05

Логувати без секретів

Логуйте limiter key hash/prefix, route group, tenant, decision, remaining і reset time. Не логайте full API key або Authorization header.

Висновок

Якщо у вас немає явної відповіді на кожен з цих кроків, limiter ще не production-ready.

Алгоритм визначає не тільки точність, а й UX. Два клієнти можуть мати однакову кількість запитів за хвилину, але один з них створить burst на межі вікна, а інший буде рівномірним.

АлгоритмЯк працюєКоли підходитьСлабке місце
Fixed windowЛічильник за фіксоване вікно, наприклад 100 запитів за хвилинуПрості internal endpoints, low-risk API, cheap baselineBoundary burst: клієнт може зробити багато запитів на межі двох вікон
Sliding window logЗберігає timestamps запитів і рахує тільки ті, що потрапляють у рухоме вікноКритичні API, login, checkout, expensive operationsБільше storage і cleanup work на кожен request
Sliding window counterАпроксимує рухоме вікно через поточне і попереднє bucket-вікноБаланс точності й вартості для high-traffic APIМенш точний за log-варіант, потребує акуратної математики reset
Token bucketКлієнт має bucket токенів, які поповнюються з часомAPI, де короткий burst нормальний, але середній rate має бути контрольованийПотрібно правильно обрати capacity і refill rate

Fixed window простий, sliding window точніший, token bucket краще переносить легальні bursts.

Скріншот секції algorithm-choice

Висновок

Почніть із fixed window для простого baseline, але для публічних дорогих routes зазвичай краще sliding window або token bucket.

Для одного Bun процесу можна написати простий in-memory fixed-window limiter. Він корисний для local dev, internal tools, single-instance deployments або як fallback, але він не синхронізується між інстансами.

Мінімальний приклад:

TS
type LimitEntry = { count: number; resetAt: number };
const limits = new Map<string, LimitEntry>();

const WINDOW_MS = 60_000;
const MAX_REQUESTS = 120;

function rateLimitKey(req: Request) {
  const apiKeyId = req.headers.get("x-api-key-id");
  const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
  return apiKeyId ? `api:${apiKeyId}` : `ip:${ip}`;
}

function checkLimit(key: string, now = Date.now()) {
  const current = limits.get(key);

  if (!current || current.resetAt <= now) {
    limits.set(key, { count: 1, resetAt: now + WINDOW_MS });
    return { allowed: true, remaining: MAX_REQUESTS - 1, resetAt: now + WINDOW_MS };
  }

  if (current.count >= MAX_REQUESTS) {
    return { allowed: false, remaining: 0, resetAt: current.resetAt };
  }

  current.count += 1;
  return { allowed: true, remaining: MAX_REQUESTS - current.count, resetAt: current.resetAt };
}

Bun.serve({
  async fetch(req) {
    const decision = checkLimit(rateLimitKey(req));

    if (!decision.allowed) {
      const retryAfter = Math.ceil((decision.resetAt - Date.now()) / 1000);
      return Response.json(
        { error: "rate_limited", retryAfter },
        { status: 429, headers: { "Retry-After": String(retryAfter) } },
      );
    }

    return Response.json({ ok: true, remaining: decision.remaining });
  },
});

Це fixed-window baseline. Він не очищає старі ключі активно, не працює між кількома processes, не має tenant-specific policies і не захищає від boundary bursts. Але він показує правильну форму: key, counter, decision, 429, Retry-After.

Де це доречно

In-memory limiter годиться для одного процесу або як дешевий локальний guard. Для production API з autoscaling потрібен shared store.

Як тільки Bun API запускається у кількох інстансах, in-memory counters перестають бути глобальним лімітом. Один клієнт може розподіляти запити між інстансами й отримувати множник до свого ліміту. Redis вирішує це як shared counter store.

Критична деталь: counter update має бути атомарним. Для fixed window це часто INCR + EXPIRE, але треба гарантувати, що expiry встановлюється коректно при першому increment. Для sliding window log часто використовують sorted sets і cleanup старих timestamps. Для token bucket часто потрібен Lua script або інший атомарний механізм, який рахує refill і consume в одній операції. Redis rate limiting patterns зазвичай будуються навколо атомарних counters або Lua. [5][6]

Redis також додає failure mode: що робити, коли він недоступний. Для public expensive routes часто краще fail closed або degraded limit. Для critical internal control plane може бути інша політика. Але будь-який fail-open має мати alert, бо інакше rate limiter зникає саме тоді, коли потрібен найбільше.

Для кількох Bun інстансів ліміт має жити у shared store. Інакше кожен інстанс дає клієнту окремий allowance.

Скріншот секції redis-distributed

Практичний baseline

Distributed limiter має мати shared store, атомарний update, key design, TTL cleanup, latency budget і fail policy.

Готовий middleware або plugin корисний, якщо ваша задача типова. Але він не вирішує за вас key strategy, tenant policy і distributed storage.

Hono ecosystem має rate limiter middleware з configurable window, limit, key generator і store options. Це добре для швидкого baseline у Hono app. [1]
Hono rate limiter middleware
Elysia ecosystem має rate-limit plugin для Bun-first Elysia apps. Він природно лягає на lifecycle/plugins модель Elysia. [2]
Elysia rate-limit plugin
OWASP API Security Top 10 виділяє unrestricted resource consumption як окремий ризик. Rate limiting має захищати CPU, memory, storage, network і downstream ресурси. [3]
OWASP API Security

Висновок

Готовий limiter скорочує boilerplate. Production якість визначають ключі, storage, policies, observability і edge cases.

Rate limit key - це головне рішення. Якщо key неправильний, алгоритм уже не врятує. IP-only ліміти часто карають нормальних користувачів за NAT і не ловлять authenticated abuse.

Для public anonymous routes IP може бути стартовим ключем. Для authenticated API краще лімітувати за subject id, API key id, tenant id або комбінацією tenantId + routeGroup. Для login flow іноді потрібні одночасно IP limit, account/email limit і device fingerprint policy.

Для multi-tenant SaaS ліміт тільки по user id може бути занадто м'яким, бо один tenant з багатьма users здатен перевантажити ресурс. Ліміт тільки по tenant може бути занадто жорстким, бо один noisy user блокує всю компанію. Часто потрібна ієрархія: per-user, per-tenant, per-route і global emergency limit.

Public read: IP + route group.
Login: IP + account/email + device/risk signal.
API key: key id + route group + tenant.
SaaS tenant: tenant budget + per-user budget.
Expensive exports/search: окремий вузький route limit.

HTTP 429 Too Many Requests означає, що користувач надіслав забагато запитів за певний проміжок часу. MDN зазначає, що response може містити Retry-After, який підказує, скільки чекати перед повтором. [7]

У production API 429 без Retry-After змушує добрих клієнтів гадати. Вони або повторюють занадто швидко, або роблять exponential backoff там, де можна було просто зачекати до reset. Це погіршує UX і збільшує зайве навантаження.

Окрім Retry-After, багато API додають rate-limit headers, наприклад RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset, або власні X-RateLimit-*. Головне - стабільність contract і документація для клієнтів.

429 має бути корисним: клієнт повинен знати, коли повторити запит, а не вгадувати backoff.

Скріншот секції headers-contract

Висновок

Rate limiter має бути зрозумілим для клієнта. 429 без retry contract створює повторний traffic і погану інтеграцію.

Rate limiting має багато неприємних деталей, які не видно в happy path. Нижче ті, що найчастіше з'являються після запуску.

NAT і corporate networks

IP-only ліміт може заблокувати десятки нормальних користувачів за одним egress IP. Для authenticated API краще key by subject/API key/tenant.

Webhook retry storms

Партнер може чесно retry-ити failed webhooks і потрапити під limiter. Для webhooks потрібні окремі policies, idempotency і retry-aware contract.

Clock skew

Якщо limiter розкиданий між різними systems, reset time і sliding windows мають рахуватися стабільно. Redis server time або centralized store часто надійніші за локальні clocks.

Burst after deploy

Після downtime або deploy клієнти можуть синхронно повторити запити. Token bucket або queued backoff може бути кращим, ніж жорсткий fixed window.

Admin і internal routes

Не давайте internal tools безлімітний доступ за замовчуванням. Вони часто запускають найважчі exports і batch operations.

Redis failure

Fail-open без alerts робить захист невидимим. Fail-closed без деградації може покласти продукт. Policy має бути різною для route classes.

Ці помилки не унікальні для Bun, але в Bun API вони часто ховаються за швидким runtime і простим middleware wrapper.

Один глобальний ліміт для всіх routes.

IP-only ліміт для authenticated API.

In-memory limiter у multi-instance production.

Redis INCR без коректного TTL або атомарності.

Немає Retry-After у 429 response.

Ліміти не враховують tenant, API key або route cost.

Full API key або Authorization header потрапляє в logs як limiter key.

Fail-open при Redis outage без alerting.

Rate limiter стоїть після body parsing для дорогих payload routes.

Немає тестів на boundary burst, reset, Redis failure і concurrent requests.

Рев'ю правило

Rate limiter має стояти рано, мати правильний key, атомарний counter, зрозумілий 429 і observable decision.

Перед запуском rate limiting у staging або production пройдіть цей список. Він допомагає знайти проблеми ще до того, як їх знайдуть клієнти.

Route classes визначені

Public, auth, login, webhook, export, admin і internal routes мають різні policies.

Key strategy не IP-only

Для authenticated routes використовуйте subject/API key/tenant/route group, а IP залишайте як додатковий signal.

Distributed store є для multi-instance

Якщо Bun API має кілька інстансів, counters живуть у Redis або іншому shared store.

Операції атомарні

Increment, expiry, sliding window cleanup або token consume виконуються без race conditions.

429 має retry contract

Response містить стабільний JSON error і Retry-After; rate-limit headers документовані.

Limiter стоїть до дорогих операцій

Rate check відбувається до body parsing, DB calls, remote calls і важких transforms, якщо route це дозволяє.

Redis failure policy зафіксована

Для кожного route class відомо, fail-open чи fail-closed, і є alerting.

Observability є

Логується decision, route group, limiter key hash, remaining, reset time, storage latency і Redis failures.

Bun дає швидкий HTTP runtime, але rate limiting не є runtime feature, яку можна додати одним рядком і забути. Це security і reliability policy, яка має знати, кого лімітує, за який route, з яким storage, яким алгоритмом і яким retry contract.

Для одного процесу in-memory fixed window може бути нормальним baseline. Для production з кількома інстансами потрібен Redis або інший shared store. Для user-facing API fixed window часто занадто грубий, sliding window або token bucket дають кращий UX.

Найважливіше: не карайте нормальних користувачів через поганий key. IP-only ліміт, один global limit і 429 без Retry-After зазвичай створюють більше проблем, ніж вирішують.

Чи достатньо in-memory rate limiter у Bun.js?

Тільки для одного процесу, local dev, internal tools або простого baseline. Якщо API має кілька інстансів, in-memory ліміт множиться на кількість інстансів і не є глобальним захистом.

Що краще: fixed window, sliding window чи token bucket?

Fixed window найпростіший, але має boundary burst. Sliding window точніший для критичних routes, але дорожчий. Token bucket добре дозволяє короткі легальні bursts, контролюючи середній rate.

Чому IP-only rate limiting поганий?

IP-only ліміт може блокувати нормальних користувачів за NAT, corporate proxy або мобільним оператором, і водночас погано працювати для authenticated abuse. Для API краще лімітувати за subject, API key, tenant і route group.

Що має повертати Bun API при перевищенні ліміту?

Стабільний JSON error з HTTP `429`, header `Retry-After` і бажано rate-limit headers на кшталт remaining/reset. Це допомагає добрим клієнтам повторити запит коректно. [7]

Чи треба Redis для rate limiting?

Для одного процесу не обов'язково. Для production з кількома інстансами або serverless/concurrent deployment Redis чи інший shared store практично необхідний, щоб counters були спільними.

Що робити, якщо Redis недоступний?

Заздалегідь визначте fail policy. Для дорогих public routes часто краще fail closed або degraded strict local limit. Для окремих internal/control-plane routes може бути fail-open, але тільки з alerting і audit.

Джерела підтверджують готові middleware/plugin варіанти, security rationale для resource limiting, Redis rate limiting patterns і HTTP semantics для 429.

Перевірено: 14 трав. 2026 р.Актуально для: Bun 1.3.xАктуально для: Bun.serve routesАктуально для: Hono 4.xАктуально для: Elysia 1.xАктуально для: Redis-backed APIsПеревірено з: Bun.serve middleware compositionПеревірено з: Redis countersПеревірено з: Hono rate limiter middlewareПеревірено з: Elysia rate-limit pluginПеревірено з: HTTP 429 Retry-After response

PAS7 Studio може допомогти спроєктувати rate limiting для Bun, Hono або Elysia: route classes, Redis store, sliding window або token bucket, API-key/tenant budgets, abuse monitoring і коректний 429 contract.

Це особливо корисно для SaaS, public API, webhook endpoints, AI/automation продуктів і міграцій з Express/Fastify, де старі ліміти не враховують tenant, API keys або horizontal scaling.

Ви тут03/05

Rate limiting у Bun.js: in-memory, Redis, sliding window і edge cases

Пов'язані статті

ai-assistants

Скільки коштує розробка AI асистента у 2026: RAG чатбот, база знань, CRM, Telegram та підтримка

Практичний гід для бізнесу: від чого залежить ціна розробки AI асистента у 2026 році, що входить у RAG чатбот, інтеграції з CRM, Telegram, guardrails, оцінювання, моніторинг і супровід.

blogs

AI для розробки лендінгів: де він реально прискорює запуск, а де псує конверсію

Дослідження про використання AI у розробці лендінгів: v0, Webflow AI, Builder.io, Framer-подібні AI builders, генерація UX, copy, SEO, персоналізація, A/B тести, ризики шаблонності, безпеки, доступності та технічного боргу.

growth

AI SEO / GEO у 2026: ваші наступні клієнти — не люди, а агенти

Пошук зміщується від кліків до відповідей. Боти та AI-агенти сканують, цитують, рекомендують і дедалі частіше купують. Дізнайтесь, що таке AI SEO / GEO, чому класичного SEO вже недостатньо, і як PAS7 Studio допомагає брендам перемагати у «агентному» вебі.

blogs

Найпотужніший чіп від Apple? M5 Pro і M5 Max б'ють рекорди

Аналітичний розбір Apple M5 Pro і M5 Max станом на березень 2026 року. Пояснюємо, чому ці чіпи можна вважати найпотужнішими професійними ноутбучними SoC від Apple, як вони виглядають на тлі M4 Pro, M4 Max, M1 Pro, M1 Max і що показують у порівнянні з актуальними Intel та AMD.

Професійна розробка для вашого бізнесу

Створюємо сучасні веб-рішення та боти для бізнесу. Дізнайтеся, як ми можемо допомогти вам досягти цілей.