Кожен туторіал по контактній формі Next.js включає rate limiting. Більшість рекомендують Upstash Redis зі ковзним вікном, 3 відправки на хвилину на IP. Я відвантажив цей патерн, прогнав місяць і видалив.
Це пост-компаньйон до чотирирядкового спам-фільтра, що замінив reCAPTCHA на цьому сайті. Та сама контактна форма. Ще одне спрощення.
Що я мав
Канонічний стек «безпечної контактної форми»:
- Honeypot + time-floor + ліміти розміру (пост за посиланням вище)
- Upstash Redis rate limiter, 3/хв на IP
- Cloudflare перед усім
- Telegram-сповіщення при валідній відправці
Приблизно 80 рядків коду плюс Redis-залежність плюс Cloudflare worker. Для контактної форми, що отримує ~5 відправок на тиждень.
Що я помітив через місяць
Три точки даних змусили переглянути.
1. Rate limiter жодного разу не спрацював на реальній атаці.
Honeypot і time-floor ловили кожну спам-спробу, яку я зміг підтвердити. Я грепнув логи на «rate limit exceeded» — нуль за 31 день. Боти, що намагалися відправити, всі фейлилися на honeypot або time-floor до того, як доходили до rate limit. Rate limiter стояв після фільтрів, які реально працювали.
2. Він спрацював на двох легітимних користувачах.
Обидва рази — люди в корпоративних мережах. Великий офіс, спільний IP, одна людина відправляє за себе, інша — одразу після. Друга відправка потрапила в ліміт 3/хв. Вони отримали ввічливе «спробуйте пізніше» і, ймовірно, не спробували.
Два втрачених ліди на формі з ~20 відправками на місяць — це 10% хибних спрацювань на штуці, яка має ловити ботів. Це гірше, ніж не обмежувати.
3. Redis додав режим відмови.
Upstash відмінний. Але це ще одна залежність, ще один API-виклик на відправку, ще одне місце, що може гальмувати або бути недоступним. Двічі за місяць відповідь Upstash була >500ms, що зробило відправку відчутно гальмівною. Один раз (коротко) Upstash повернув помилку, яку мій код обробив як fail open — rate limit не застосувався тієї хвилини. На цьому етапі: що він реально захищав?
Математика, коли rate limiting окупається
Rate limiting реально корисний, коли:
- Ендпоінт форми робить дорогу роботу. Відправка листа дешева (Resend обробляє). ML-модель — ні. Якщо кожна відправка коштує 50ms CPU або €0.002 API-виклику, обмежуй.
- Тебе цілеспрямовано атакує конкретний атакуючий. Один IP, що довбає 500 разів на хвилину — рівно те, для чого потрібні rate limiter'и. Cloudflare зазвичай ловить це на edge, але application-level rate limiting — ремінь плюс підтяжки.
- Ендпоінт створює видимий користувачу стан. Форма реєстрації, що створює акаунти, або форма коментарів, що публікує публічно, потребує rate limiting як захисту від абʼюзу — бо дублікати не просто дратують, вони публічні.
- Комплаєнс або аудит вимагає. Деякі enterprise-клієнти запитають. Це реальна причина, навіть якщо математика інакше не підтримує.
Rate limiting — рефлекс-без-окупності, коли:
- Форма вже обмежена upstream. Cloudflare на 100 req/хв на IP ловить 99% абʼюзу до того, як твій код це бачить. Додавання другого шару на 3/хв на IP — зазвичай тертя без маржинального захисту.
- Ендпоінт вже фільтрується дешевими контент-перевірками. Мій honeypot + time-floor відкидає ботів за <1ms без downstream-вартості. Rate limiting стоїть після фільтрів, які реально вирішують.
- Обсяг низький. Портфоліо-форма на 5 відправок на тиждень не буде DDoS'ована мотивованим противником до Stripe-рівня рахунків. Найгірший випадок — кілька сотень спам-відправок за ніч — незручність, не інцидент.
Чим я замінив
Нічим. Ланцюжок фільтрів тепер:
- Cloudflare на edge (поставив і забув)
- Honeypot
- Time-floor (мінімум 1500ms)
- Ліміти розміру (
name < 100,email < 200,message < 5000) - Telegram-сповіщення
Весь пайплайн. Нема Redis, нема стану, нема ковзного вікна. 60ms p95 час відповіді.
Найгірший випадок, у якому я можу помилятися
Мотивований атакуючий міг би, у теорії, заскриптувати headless-браузер, що монтує сторінку, чекає 1600ms, заповнює форму і відправляє — обходячи time-floor. Тепер у мене бот, що відправляє 100 разів на хвилину.
Що реально трапляється тоді:
- Telegram починає вібрувати. Я бачу.
- Я додаю rate limiting за 15 хвилин. Код все ще в git history; просто
git revertвидалення. - Або баню IP-діапазон у Cloudflare.
Ключовий інсайт: rate limiting — це фікс для конкретної форми атаки. Я можу додати його назад менш ніж за годину, коли побачу атаку. Платити за нього превентивно, назавжди, на формі, якій він ніколи не був потрібен — це податок на загрозу, що не матеріалізувалася.
Узагальнене правило
Раніше я додавав кожен security-middleware з топового туторіалу. Тепер я ставлю три питання перед додаванням:
- Від якої конкретної атаки це захищає? Якщо не можу назвати одним реченням — захист не потрібен.
- Скільки коштує — у складності коду, у залежностях, у латенсі, у хибних спрацюваннях? Кожен захист — це податок.
- Чи можу я додати це пізніше, коли атака реально зʼявиться? Для більшості захистів на малотрафікових системах відповідь — так, і превентивний захист — перевитрата.
Rate limiting на контактній формі провалив усі три для мене:
- Атака, від якої він захищає, в основному ловиться дешевшими upstream-фільтрами.
- Він додав Redis, ~20 рядків коду і 2 хибних спрацювання на 20 відправок.
- Я абсолютно можу додати його назад за годину, коли (якщо) реальний атакуючий зʼявиться.
Видалено. Не сумую.
Застереження
Це працює, бо форма моя, обсяг маленький, і downstream-дія (Telegram-сповіщення мені) не має публічних побічних ефектів. Якщо ти будуєш:
- Високотрафіковий публічний API
- Форму реєстрації, що створює користувацькі акаунти
- Форму коментарів, що публікує публічно
- Ендпоінт, суміжний із платежами
- Форму, що тригерить SMS (реальна вартість на відправку)
— залишай rate limiter. Він існує з причини, і причина — форма твого конкретного випадку.
Цей пост конкретно про малотрафікові, owner-viewed, без побічних ефектів ендпоінти форм. Інтернет переважно пише гайди «захист за замовчуванням», що правильно для медіанного проєкту і неправильно для конкретних. Знати, коли «конкретний» застосовний до твого проєкту — це як ти перестаєш платити за захист, що не потрібен.
Чесний трейд, який я роблю
Я міняю defence-in-depth проти атаки, що не сталася, на простішу кодову базу з меншою кількістю залежностей. Цей трейд правильний для цієї форми сьогодні. Він був би неправильним для іншої форми завтра.
Режим відмови, якщо я помиляюся: вихідні спаму в Telegram і пʼятнадцять хвилин git revert. Я переживу.
Режим відмови, якщо б я залишив rate limiter: дві втрачених реальних розмови на місяць, назавжди, мовчки. Не можу.
Якщо ти ревʼюїш свій власний security-стек і хочеш друге мнение, що залишити, а що видалити — я прочитаю твою модель загроз і відповім однією сторінкою чесного зворотного звʼязку. Не намагаюся продати аудит — у половині випадків моя відповідь «ви перевитрачаєте». Контактна форма.