Skip to content

Compare versions

From:To:
2
# Как это должно было работать
Как это должно было работать

Better Auth умеет ограничивать частоту запросов по ключу API: столько-то обращений за такое-то окно.

Настройка выглядит понятно, и документация показывает пример вроде «10 запросов в сутки». Каждый, кто видел такую настройку, понимает её одинаково: сутки прошли — счётчик обнулился.

## Что пошло не так
Что пошло не так

17 ноября 2025 года приходит пользователь — Ксавье Эмирмон (xaviemirmon) — и открывает задачу #6035:

Because it's based on last_request_at it waits until the number of requests has been reached then will wait the time set and refresh.

Поскольку всё считается от последнего запроса, оно ждёт, пока не наберётся нужное число обращений, а потом отсчитывает окно заново.

И дальше — фраза, ради которой эту задачу стоит читать целиком:

The docs are confusing as it make it appear that every day the user has 10 requests per day. In reality they have 10 requests but have to wait a day from the last request for it to reset.

Документация вводит в заблуждение: кажется, что у пользователя 10 запросов в сутки. На самом деле у него 10 запросов, а сброс — через сутки после последнего обращения.

## Почему это ловушка, а не просто неудобство
Почему это ловушка, а не просто неудобство

Окно привязано к последнему запросу, а не к первому. Значит каждое обращение сдвигает срок сброса вперёд — включая отклонённые.

code
1Фиксированное окно (как ждёт пользователь):
2 |--- час ---|--- час ---|--- час ---|
3 ^ сброс ^ сброс ^ сброс
4
5Окно от последнего запроса (как было):
6 запрос → сброс через час
7 запрос → сброс через час
8 запрос → сброс через час
9 …сброс не наступает никогда

Клиент, который упёрся в лимит и продолжает стучаться, отодвигает собственную разблокировку. Бесконечно. А ретрай с повторами — ровно то, что делает любой нормальный клиент.

Снаружи всё выглядит правильно: лимит срабатывает, счётчик считает, отказы приходят. Не работает только та часть, которую не видно, — возврат к жизни.

## Девять месяцев и две попытки
Девять месяцев и две попытки

Задача открыта в ноябре 2025-го и по состоянию на сентябрь 2026-го всё ещё открыта. Исправление писали дважды: сначала PR #8803 в марте 2026-го, потом #10747 в августе — его прислал разработчик под ником bytaesu, и он прямо говорит, что заменяет первый.

Причина затяжки видна в самой шапке PR:

This adds the nullable rateLimitResetAt field. It requires a database schema migration but does not introduce a breaking API change.

Добавляется поле rateLimitResetAt. Требуется миграция схемы, но ломающих изменений в API нет.

Вот в чём дело. Чтобы окно перестало ездить, надо где-то хранить, когда оно открылось. Одного поля last_request_at для этого не хватает принципиально.

А новое поле в библиотеке, которая стоит у тысяч людей, — это миграция у каждого из них. Отсюда и осторожность, и сроки.

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

Проверить, не продлевает ли отказ запрет

Самый быстрый способ — постучаться больше лимита, а потом подождать чуть меньше окна и постучать снова.

bash
1# упираемся в лимит
2for i in $(seq 1 10); do
3 curl -s -o /dev/null -w "%{http_code} " адрес
4done; echo
5
6# ждём чуть меньше окна, стучимся ещё пару раз,
7# ждём остаток окна и пробуем: должно разблокироваться

Если после полного окна от первого запроса всё ещё приходит 429 — окно у вас ездит.

В коде то же видно по одному признаку: запись времени обращения стоит до проверки лимита, а не после успешного прохода.

Сверить обещание настройки с тем, что она делаетRecommended

Пользователь в той задаче жалуется не только на поведение, но и на то, что документация обещала другое.

«10 запросов в сутки» и «10 запросов, потом сутки тишины от последней попытки» — разные продукты.

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

## Что отсюда следует
Что отсюда следует

Окно ограничения привязывается к первому событию, а не к последнему. Иначе получается не лимит, а наказание, которое продлевает себя само.

Отклонённая попытка не должна тратить квоту и сдвигать срок. Это отдельное решение, и его лучше закрепить тестом с прямым названием — вроде «отказ не продлевает запрет».

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

Removed
Removed