Инцидент 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 на все случаи жизни превращает штатную ситуацию клиента в ложную тревогу инфраструктуры.