Заказать бота

Сетевые запросы в VK-боте: timeout, retry, backoff для стабильного прода

Практическое руководство по надежным сетевым запросам в VK-ботах: таймауты, retry, backoff, идемпотентность и логирование. Как снизить сбои перед прод-запуском.

Содержание статьи

Большинство «случайных падений» VK-бота — это не логика команд, а сетевые проблемы:

  • таймаут внешнего API,
  • кратковременная недоступность сервиса,
  • rate limit,
  • повторная отправка одного и того же действия.

В этом материале соберём рабочий шаблон, который закрывает эти риски.

Почему это критично

Если не контролировать сеть, бот:

  1. зависает на долгих запросах;
  2. дублирует действия при повторных отправках;
  3. теряет лиды при кратковременных сбоях;
  4. уходит в «тихий» фейл без понятных логов.

Базовые правила надежных запросов

1) Всегда ставь timeout

Без timeout один плохой хост может блокировать весь обработчик.

2) Retry только для временных ошибок

Повторяем запрос при timeout, 5xx, 429.

Не повторяем при валидационных ошибках (400, 401, 403).

3) Backoff + jitter

Между попытками нужна пауза с ростом интервала, иначе бот «ддосит» проблемный сервис.

4) Идемпотентность

Если действие нельзя выполнить дважды (создание заявки, платеж), нужен idempotency key.

Универсальный helper для HTTP

Ниже минимальный async-шаблон на aiohttp.

pip install aiohttp
import asyncio
import json
import random
import logging
from typing import Any
import aiohttp

logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")

RETRYABLE_STATUS = {429, 500, 502, 503, 504}


async def request_with_retry(
    method: str,
    url: str,
    *,
    json_body: dict[str, Any] | None = None,
    headers: dict[str, str] | None = None,
    timeout_sec: float = 8.0,
    max_attempts: int = 4,
    base_delay: float = 0.4,
) -> dict[str, Any]:
    """Надежный HTTP-запрос: timeout + retry + exponential backoff + jitter."""

    timeout = aiohttp.ClientTimeout(total=timeout_sec)

    for attempt in range(1, max_attempts + 1):
        try:
            async with aiohttp.ClientSession(timeout=timeout) as session:
                async with session.request(method, url, json=json_body, headers=headers) as resp:
                    text = await resp.text()

                    if resp.status in RETRYABLE_STATUS and attempt < max_attempts:
                        delay = base_delay * (2 ** (attempt - 1)) + random.uniform(0, 0.2)
                        logging.warning(
                            "HTTP retry: status=%s attempt=%s/%s delay=%.2fs",
                            resp.status,
                            attempt,
                            max_attempts,
                            delay,
                        )
                        await asyncio.sleep(delay)
                        continue

                    if resp.status >= 400:
                        raise RuntimeError(f"HTTP {resp.status}: {text[:300]}")

                    # если ответ не JSON — вернем как raw
                    # text уже прочитан выше, повторно не читаем
                    try:
                        return json.loads(text)
                    except Exception:
                        return {"raw": text}

        except (aiohttp.ClientConnectionError, asyncio.TimeoutError) as exc:
            if attempt >= max_attempts:
                raise RuntimeError(f"Network failed after retries: {exc}") from exc

            delay = base_delay * (2 ** (attempt - 1)) + random.uniform(0, 0.2)
            logging.warning(
                "Network retry: err=%s attempt=%s/%s delay=%.2fs",
                exc,
                attempt,
                max_attempts,
                delay,
            )
            await asyncio.sleep(delay)

    raise RuntimeError("Unreachable")

Интеграция в обработчик бота

Пример использования в vkbottle-хендлере:

from vkbottle.bot import Bot, Message

bot = Bot(token="VK_TOKEN")


@bot.on.message(text="проверка")
async def check_api(message: Message):
    payload = {"user_id": message.from_id, "text": message.text}

    try:
        data = await request_with_retry(
            "POST",
            "https://example.internal/api/check",
            json_body=payload,
            headers={"X-Idempotency-Key": f"msg-{message.id}"},
            timeout_sec=6,
            max_attempts=4,
        )
    except Exception as exc:
        await message.answer("Сервис временно недоступен. Попробуйте чуть позже.")
        return

    await message.answer(f"OK: {data}")

Rate limit и очередь

Если бот массово шлёт запросы, добавь очередь с ограничением параллелизма.

import asyncio

SEM = asyncio.Semaphore(5)  # не более 5 одновременных запросов


async def safe_call(*args, **kwargs):
    async with SEM:
        return await request_with_retry(*args, **kwargs)

Это резко снижает пики и ошибки 429.

Что логировать обязательно

Минимальный набор:

  • endpoint,
  • статус код,
  • время ответа,
  • attempt number,
  • correlation/idempotency key,
  • текст ошибки (без утечки токенов).

Частые ошибки

Retry на все 4xx

Так вы только нагружаете API. Повторять нужно не все ошибки, а только временные.

Нет ограничения параллелизма

Даже хороший retry не спасает, если одновременно стрелять сотнями запросов.

Нет idempotency key

При повторе можно получить дубли заявок/заказов.

Мини-FAQ

Какой timeout ставить?

Обычно 5–10 секунд на внешний API. Для критичных операций лучше меньше + retry.

Сколько попыток retry оптимально?

Практичный диапазон — 3–5 попыток с exponential backoff.

Нужен ли отдельный circuit breaker?

Для простого проекта можно начать с timeout/retry/queue. При росте нагрузки — да, добавлять circuit breaker полезно.

Нужен надёжный прод-запуск под вашу нагрузку?

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

Если нужен быстрый старт на типовой логике, посмотрите каталог готовых ботов.

Что читать дальше

Реклама

Комментарии

Загрузка...