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 і погані практики.

Bun.js Middleware Production Guide 2026
Серія про production middleware у Bun.js: огляд, безпека, performance, observability, rate limiting, body parsing, WebSocket/SSE і тестування request pipeline.
Усі статті в цьому гайді
01
Bun.js middleware у 2026: overview, best practices і анти-патерни
Базова ментальна модель middleware у native Bun, Hono та Elysia, з прикладами, оптимізацією і roadmap наступних deep dive статей.
02
Auth middleware у Bun.js: JWT, sessions, API keys і multi-tenant контекст
Як правильно будувати auth middleware у Bun: порядок перевірок, cache, token rotation, tenant context, помилки 401/403 і тестування.
03
Rate limiting у Bun.js: in-memory, Redis, sliding window і edge cases
Детальний розбір rate limiting для Bun API: алгоритми, Redis, distributed limits, abuse protection і graceful degradation.
04
Observability middleware у Bun.js: logs, request id, tracing і latency budgets
Як додати request id, structured logs, timing headers, OpenTelemetry-подібний flow і не перетворити логування на bottleneck.
05
Body parsing і validation у Bun.js: JSON, uploads, streams і payload limits
Як безпечно читати body у Bun, де ставити limits, як не зламати streams, uploads, idempotency і schema validation.
Наївний ліміт “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 і політику відмови. Саме це і розбираємо.
429 без Retry-After ускладнює життя добрим клієнтам.Це третій chapter серії про Bun middleware. Після overview і auth логічно перейти до abuse protection: саме rate limiting часто стоїть між вашим API і дорогою хвилею зайвих запитів.
Rate limiter має бути коротким state machine, а не випадковою перевіркою в середині handler. Якщо розкласти його на кроки, більшість помилок стає видимою ще до коду.
Визначити identity key
Це може бути user id, API key id, tenant id, route group, IP або комбінація. Для authenticated API key зазвичай кращий ключ apiKeyId + routeGroup, ніж просто IP.
Вибрати policy
Різні routes потребують різних лімітів: login, search, export, webhook, public read, admin mutation. Один глобальний ліміт майже завжди або занадто слабкий, або занадто агресивний.
Атомарно оновити counter
В одному процесі це Map. У distributed API це Redis або інший shared store. Для Redis важливо, щоб increment + expiry або sliding-window update були атомарними.
Повернути корисний 429
Клієнт має отримати стабільний JSON error, Retry-After і бажано rate-limit headers. Інакше добрі клієнти не знають, коли повторювати запит.
Логувати без секретів
Логуйте 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 baseline | Boundary 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, але він не синхронізується між інстансами.
Мінімальний приклад:
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]
Elysia ecosystem має rate-limit plugin для Bun-first Elysia apps. Він природно лягає на lifecycle/plugins модель Elysia. [2]
OWASP API Security Top 10 виділяє unrestricted resource consumption як окремий ризик. Rate limiting має захищати CPU, memory, storage, network і downstream ресурси. [3]
Висновок
Готовий 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.
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.
Висновок
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 зазвичай створюють більше проблем, ніж вирішують.
Тільки для одного процесу, local dev, internal tools або простого baseline. Якщо API має кілька інстансів, in-memory ліміт множиться на кількість інстансів і не є глобальним захистом.
Fixed window найпростіший, але має boundary burst. Sliding window точніший для критичних routes, але дорожчий. Token bucket добре дозволяє короткі легальні bursts, контролюючи середній rate.
IP-only ліміт може блокувати нормальних користувачів за NAT, corporate proxy або мобільним оператором, і водночас погано працювати для authenticated abuse. Для API краще лімітувати за subject, API key, tenant і route group.
Стабільний JSON error з HTTP `429`, header `Retry-After` і бажано rate-limit headers на кшталт remaining/reset. Це допомагає добрим клієнтам повторити запит коректно. [7]
Для одного процесу не обов'язково. Для production з кількома інстансами або serverless/concurrent deployment Redis чи інший shared store практично необхідний, щоб counters були спільними.
Заздалегідь визначте 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.
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.
Пов'язані статті
Скільки коштує розробка AI асистента у 2026: RAG чатбот, база знань, CRM, Telegram та підтримка
Практичний гід для бізнесу: від чого залежить ціна розробки AI асистента у 2026 році, що входить у RAG чатбот, інтеграції з CRM, Telegram, guardrails, оцінювання, моніторинг і супровід.
AI для розробки лендінгів: де він реально прискорює запуск, а де псує конверсію
Дослідження про використання AI у розробці лендінгів: v0, Webflow AI, Builder.io, Framer-подібні AI builders, генерація UX, copy, SEO, персоналізація, A/B тести, ризики шаблонності, безпеки, доступності та технічного боргу.
AI SEO / GEO у 2026: ваші наступні клієнти — не люди, а агенти
Пошук зміщується від кліків до відповідей. Боти та AI-агенти сканують, цитують, рекомендують і дедалі частіше купують. Дізнайтесь, що таке AI SEO / GEO, чому класичного SEO вже недостатньо, і як PAS7 Studio допомагає брендам перемагати у «агентному» вебі.
Найпотужніший чіп від Apple? M5 Pro і M5 Max б'ють рекорди
Аналітичний розбір Apple M5 Pro і M5 Max станом на березень 2026 року. Пояснюємо, чому ці чіпи можна вважати найпотужнішими професійними ноутбучними SoC від Apple, як вони виглядають на тлі M4 Pro, M4 Max, M1 Pro, M1 Max і що показують у порівнянні з актуальними Intel та AMD.
Професійна розробка для вашого бізнесу
Створюємо сучасні веб-рішення та боти для бізнесу. Дізнайтеся, як ми можемо допомогти вам досягти цілей.