Skip to content

Compare versions

From:To:
2
11¶ # Как это должно было работать
22
33 [Better Auth](https://github.com/better-auth/better-auth) умеет ограничивать частоту запросов по ключу API: столько-то обращений за такое-то окно.
44
55 Настройка выглядит понятно, и документация показывает пример вроде «10 запросов в сутки». Каждый, кто видел такую настройку, понимает её одинаково: сутки прошли — счётчик обнулился.
66¶ ## Что пошло не так
77
88 17 ноября 2025 года приходит пользователь — Ксавье Эмирмон (xaviemirmon) — и открывает задачу [#6035](https://github.com/better-auth/better-auth/issues/6035):
99
1010 > 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.
1111 >
1212 > *Поскольку всё считается от последнего запроса, оно ждёт, пока не наберётся нужное число обращений, а потом отсчитывает окно заново.*
1313
1414 И дальше — фраза, ради которой эту задачу стоит читать целиком:
1515
1616 > 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.
1717 >
1818 > *Документация вводит в заблуждение: кажется, что у пользователя 10 запросов в сутки. На самом деле у него 10 запросов, а сброс — через сутки после последнего обращения.*
1919¶ ## Почему это ловушка, а не просто неудобство
2020
2121 Окно привязано к **последнему** запросу, а не к первому. Значит каждое обращение сдвигает срок сброса вперёд — включая **отклонённые**.
2222
2323 ```
2424 Фиксированное окно (как ждёт пользователь):
2525 |--- час ---|--- час ---|--- час ---|
2626 ^ сброс ^ сброс ^ сброс
2727
2828 Окно от последнего запроса (как было):
2929 запрос → сброс через час
3030 запрос → сброс через час
3131 запрос → сброс через час
3232 …сброс не наступает никогда
3333 ```
3434
3535 Клиент, который упёрся в лимит и продолжает стучаться, **отодвигает собственную разблокировку**. Бесконечно. А ретрай с повторами — ровно то, что делает любой нормальный клиент.
3636
3737 Снаружи всё выглядит правильно: лимит срабатывает, счётчик считает, отказы приходят. Не работает только та часть, которую не видно, — возврат к жизни.
3838¶ ## Девять месяцев и две попытки
3939
4040 Задача открыта в ноябре 2025-го и по состоянию на сентябрь 2026-го всё ещё открыта. Исправление писали дважды: сначала PR #8803 в марте 2026-го, потом [#10747](https://github.com/better-auth/better-auth/pull/10747) в августе — его прислал разработчик под ником bytaesu, и он прямо говорит, что заменяет первый.
4141
4242 Причина затяжки видна в самой шапке PR:
4343
4444 > This adds the nullable `rateLimitResetAt` field. It requires a database schema migration but does not introduce a breaking API change.
4545 >
4646 > *Добавляется поле `rateLimitResetAt`. Требуется миграция схемы, но ломающих изменений в API нет.*
4747
4848 Вот в чём дело. Чтобы окно перестало ездить, надо где-то хранить, **когда оно открылось**. Одного поля `last_request_at` для этого не хватает принципиально.
4949
5050 А новое поле в библиотеке, которая стоит у тысяч людей, — это миграция у каждого из них. Отсюда и осторожность, и сроки.
5151
5252 Мораль здесь не про библиотеку, а про цену ошибки в схеме данных: пока проект маленький, поле добавляется за минуту; когда вырастет — за девять месяцев.
5353## Проверь у себя
54541. Проверить, не продлевает ли отказ запрет
5555 Самый быстрый способ — постучаться больше лимита, а потом подождать чуть меньше окна и постучать снова.
5656
5757 ```bash
5858 # упираемся в лимит
5959 for i in $(seq 1 10); do
6060 curl -s -o /dev/null -w "%{http_code} " адрес
6161 done; echo
6262
6363 # ждём чуть меньше окна, стучимся ещё пару раз,
6464 # ждём остаток окна и пробуем: должно разблокироваться
6565 ```
6666
6767 Если после полного окна от первого запроса всё ещё приходит 429 — окно у вас ездит.
6868
6969 В коде то же видно по одному признаку: запись времени обращения стоит **до** проверки лимита, а не после успешного прохода.
7070 why: Это единственная часть ограничителя, которую не видно в обычной жизни: пока лимит не исчерпан, оба варианта ведут себя одинаково. Разница проявляется только у того, кто уже заблокирован, — а он редко пишет в поддержку, он просто уходит.
7171 - [ ] После полного окна от первого запроса доступ восстанавливается
7272 - [ ] Отклонённые запросы не сдвигают срок сброса
7373 - [ ] В ответе 429 есть чёткое «когда пробовать снова»
74742. Сверить обещание настройки с тем, что она делает [recommended]
7575 Пользователь в той задаче жалуется не только на поведение, но и на то, что документация обещала другое.
7676
7777 «10 запросов в сутки» и «10 запросов, потом сутки тишины от последней попытки» — разные продукты.
7878
7979 Пройдите по всем подписям к настройкам с числами и сроками и проверьте каждую руками. Одна неверная подпись стоит дороже одной неверной строки кода: код чинят, когда заметят, а по неверной подписи люди строят планы.
8080 why: Ошибка в обещании работает дольше ошибки в коде: код проверяется тестами, подпись — никем.
8181 - [ ] Каждая подпись с числом или сроком проверена руками
8282 - [ ] Где поведение сложнее подписи — подпись уточнена
8383¶ ## Что отсюда следует
8484
8585 **Окно ограничения привязывается к первому событию, а не к последнему.** Иначе получается не лимит, а наказание, которое продлевает себя само.
8686
8787 **Отклонённая попытка не должна тратить квоту и сдвигать срок.** Это отдельное решение, и его лучше закрепить тестом с прямым названием — вроде «отказ не продлевает запрет».
8888
8989 **Структуру данных дешевле поменять сейчас.** Если видно, что одного поля для честного поведения не хватает, второе поле добавляется за минуту — пока у вас одна установка, а не тысяча.
90
91