Инцидент 2026-07-12: PayBot 502
Утром 12 июля клиент сообщил: GET /v2/payments с боевым ключом работает, но оба create-эндпоинта — POST /v2/payment-links и POST /v2/qr — отдают 502. Параллельно несколько Enterprise-клиентов пожаловались: «при создании ошибка и лимит 1 QR в минуту, хотя тариф Enterprise — должно быть безлимитно».
Оказалось — две независимые причины.
Причина 1 — регрессия конфигурации после переезда в Kubernetes
За день до инцидента PayBot переехал со standalone-VPS в наш k3s-кластер. На старом сервере переменные окружения приложению отдавал systemd через EnvironmentFile. В Kubernetes .env смонтировали файлом: pydantic его читал, но в os.environ значения не попадали — load_dotenv в коде не было.
Часть параметров интеграции с Kaspi читается именно через os.getenv и тихо откатилась на устаревшие значения по умолчанию. Итог: несогласованные параметры сессии, Kaspi отвергает повторный вход («обновите приложение»), почти полсотни отказов за 15 минут — и 502 на каждой попытке обновить сессию. Коварство в том, что уже живые сессии продолжали работать, поэтому большинство запросов проходило и проблема выглядела плавающей.
Фикс: критичные параметры закреплены как env контейнера прямо в манифесте деплоймента, а не в .env-файле. Отказы входа и 502 ушли в ноль. Урок: при переезде проверяйте не только «файл конфига на месте», но и каким именно механизмом каждая переменная доходит до кода.
Причина 2 — 429 «1 QR/2мин» в дашборде (не в API)
Уточнение от клиентов: ошибка возникала в веб-панели ручного создания платежа, а не в API. Эндпоинты POST /me/payments/create-qr и /me/payments/create-invoice имели собственный IP-cooldown (120 секунд), который проверялся и ставился до загрузки объекта клиента. План тарифа на этот момент был неизвестен — освобождение для Enterprise просто не могло сработать, в отличие от основного API-пути, где проверка стояла после загрузки клиента.
Фикс: загрузка клиента перенесена выше cooldown; cooldown применяется только для не-Enterprise тарифов и не-тестовых ключей:
result = await db.execute(select(Client).where(Client.id == cid))
client = result.scalar_one_or_none()
if not client:
raise ApiError(404, ...client_not_found...)
if client.plan != "enterprise" and client.api_key_mode != "test":
if await redis.exists(cooldown_key):
raise ApiError(429, ...creation_cooldown...)
await redis.setex(cooldown_key, 120, "1")
Обе правки уехали одним релизом бэкенда, cooldown-ключи в Redis очищены. Проверено: 502 → 0, Enterprise создаёт QR и инвойсы без лимита.
Часть 3 — ещё один 502, который не был аварией
Позже в тот же день один клиент снова получил 502 на POST /v2/qr. Диагностика показала: Kaspi-сессию этого аккаунта вытеснил вход с другого устройства — клиент залогинился в приложении Kaspi на телефоне, а Kaspi держит одну сессию на устройство. Восстановить её автоматически нельзя: нужен полный перелогин в панели PayBot, это действие клиента.
Аварией это выглядело из-за нашего маппинга ошибок: обработчик превращал почти любой отказ Kaspi в generic 502. Плюс health-монитор такого вытеснения не видит — оно всплывает только на реальной операции.
Фикс: для «вход с другого устройства» теперь возвращается явный 409 с сообщением «переподключите Kaspi-аккаунт» вместо 502. Урок: generic 502 на все случаи жизни превращает штатную ситуацию клиента в ложную тревогу инфраструктуры.