Загрузка
Все статьи

Битрикс24: batch-запросы и обработка 10 000 элементов без таймаута

Обычный crm.deal.list возвращает 50 элементов. Для синхронизации 10 000 сделок нужно 200 запросов — и это при условии, что ни один не упадёт по таймауту. Batch-метод Битрикс24 позволяет отправить до 50 команд в одном запросе, но у него свои подводные камни: лимит на размер запроса, ошибки частичной обработки и полное отсутствие транзакционности.

Началось всё с задачи: синхронизировать 10 000 сделок из Битрикс24 в нашу аналитическую базу. Звучит просто — вызвал crm.deal.list, получил данные, записал. Но на практике стандартный метод возвращает максимум 50 записей за раз. То есть для 10 000 сделок мне нужно 200 запросов. При среднем времени ответа Битрикс24 в 300–500 мс это 60–100 секунд. А если портал перегружен — можно и по таймауту поймать. Спойлер: мы их ловили регулярно.

Наивный подход и почему он не работает

Сначала я написал цикл с пагинацией через start-параметр. Классика:

$allDeals = [];
$step = 50;
$start = 0;

while (true) {
    $response = $webhook->call('crm.deal.list', [
        'select' => ['ID', 'TITLE', 'STAGE_ID', 'OPPORTUNITY', 'DATE_CREATE'],
        'start' => $start,
    ]);

    if (empty($response['result'])) {
        break;
    }

    $allDeals = array_merge($allDeals, $response['result']);
    $start += $step;

    // Пауза, чтобы не словить лимит (50 req/sec для облака)
    usleep(100000);
}

echo 'Всего сделок: ' . count($allDeals) . PHP_EOL;

Этот код работает для сотен элементов. Но для 10 000 он падал у нас примерно в 30% случаев. Причины: таймаут cURL (по умолчанию 30 секунд для некоторых хостингов), временные ошибки 502/503 от Битрикс24, и главное — нет никакого механизма повторных попыток. Потерял один запрос на 201-й итерации — весь цикл сломался.

Batch API: что это и как работает

Batch-метод Битрикс24 позволяет объединить до 50 команд в один HTTP-запрос. Вместо 50 отдельных вызовов вы отправляете один POST на batch с набором команд. Это снижает количество HTTP-соединений в 50 раз и сильно уменьшает суммарное время.

class BitrixBatchClient
{
    private string $webhookUrl;
    private int $maxBatchSize = 50;
    private int $maxRetries = 3;
    private int $retryDelayMs = 500;
    private int $rateLimitDelayMs = 1100;

    public function __construct(string $webhookUrl)
    {
        $this->webhookUrl = rtrim($webhookUrl, '/') . '/batch';
    }

    public function fetchAllDeals(array $select = ['ID', 'TITLE', 'STAGE_ID']): array
    {
        $allDeals = [];
        $batchIndex = 0;
        $start = 0;
        $step = 50;
        $hasMore = true;

        while ($hasMore) {
            $commands = [];

            for ($i = 0; $i < $this->maxBatchSize && $hasMore; $i++) {
                $cmdName = 'deal_' . $batchIndex . '_' . $i;
                $commands[$cmdName] = 'crm.deal.list?' . http_build_query([
                    'select' => $select,
                    'start' => $start + ($i * $step),
                ]);
            }

            $result = $this->callBatchWithRetry($commands);

            foreach ($result as $cmdName => $deals) {
                if (empty($deals)) {
                    $hasMore = false;
                    break;
                }
                $allDeals = array_merge($allDeals, $deals);

                if (count($deals) < $step) {
                    $hasMore = false;
                    break;
                }
            }

            $start += $this->maxBatchSize * $step;
            $batchIndex++;

            usleep($this->rateLimitDelayMs * 1000);
        }

        return $allDeals;
    }

    private function callBatchWithRetry(array $commands): array
    {
        for ($attempt = 1; $attempt <= $this->maxRetries; $attempt++) {
            $ch = curl_init($this->webhookUrl);
            curl_setopt_array($ch, [
                CURLOPT_POST => true,
                CURLOPT_POSTFIELDS => json_encode(['cmd' => $commands]),
                CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT => 60,
            ]);

            $response = curl_exec($ch);
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $error = curl_error($ch);
            curl_close($ch);

            if ($error) {
                trigger_error("cURL ошибка (попытка $attempt): $error", E_USER_WARNING);
                usleep($this->retryDelayMs * 1000 * $attempt);
                continue;
            }

            $data = json_decode($response, true);

            if ($httpCode !== 200 || isset($data['error'])) {
                $errorMsg = $data['error_description'] ?? 'HTTP ' . $httpCode;
                trigger_error("API ошибка (попытка $attempt): $errorMsg", E_USER_WARNING);
                usleep($this->retryDelayMs * 1000 * $attempt);
                continue;
            }

            return $data['result']['result'] ?? [];
        }

        throw new \RuntimeException('Batch-запрос не удался после ' . $this->maxRetries . ' попыток');
    }
}

Ключевые моменты в этом коде. Во-первых, callBatchWithRetry — обёртка с exponential backoff. Битрикс24 может временно вернуть 502 или 429, и мы должны это перехватить. Во-вторых, rateLimitDelayMs — пауза между batch-запросами. Облачный Битрикс24 ограничивает 50 запросами в секунду, и один batch считается за один запрос. Но на практике я ставлю 1.1 секунды — с запасом.

Подводные камни batch-запросов

Первый и самый больной — ограничение на размер тела запроса. У Битрикс24 нет официального лимита, но на практике batch больше 1 МБ начинает падать. Если вы запрашиваете много полей с длинными значениями, 50 команд могут не влезть. Я стабильно беру 25 команд на batch, когда запрашиваю описания сделок и пользовательские поля.

Второй подводный камень — частичные ошибки. Batch может выполниться наполовину: 30 команд прошли, 20 упали. В ответе будет result_error с номерами упавших команд. Нужно проверять это отдельно:

$data = json_decode($response, true);

$successfulResults = $data['result']['result'] ?? [];
$batchErrors = $data['result']['result_error'] ?? [];

if (!empty($batchErrors)) {
    foreach ($batchErrors as $cmdName => $errorInfo) {
        echo "Ошибка в команде $cmdName: {$errorInfo['error_description']}\n";
    }
    // Можно переотправить только упавшие команды
    $failedCommands = array_intersect_key($commands, $batchErrors);
    $retried = $this->callBatchWithRetry($failedCommands);
    $successfulResults = array_merge($successfulResults, $retried);
}

Третья проблема — отсутствие транзакционности. Если ваш скрипт упал на середине обработки 200-го batch, у вас в базе часть данных синхронизирована, часть — нет. Решение: завести таблицу логов синхронизации с sync_batch_id и при запуске проверять, до какого batch дошли в прошлый раз.

Итоги

После внедрения batch-клиента время синхронизации 10 000 сделок упало с 90–120 секунд до 12–15 секунд. Количество таймаутов — с 30% до нуля (за счёт retry). Главное правило: никогда не полагайтесь на стабильность API Битрикс24, всегда добавляйте retry-логику и логирование. И уменьшайте размер batch при работе с тяжёлыми пользовательскими полями — лучше сделать 8 запросов по 25 команд, чем 4 запроса, из которых 2 упадут.

Поделиться статьёй

Комментарии

0
0 / 2000

Пока нет комментариев

Будьте первым, кто оставит комментарий к этой статье

Читайте также

Сложности с разделами каталога в Битрикс24 через REST API: игра с деревом

Обновляете товар через API, передаете SECTION_ID, но он не появляется в нужной папке. В этой статье я объясняю разницу между простым SECTION_ID и сложным деревом инфоблоков, как правильно выстроить иерархию и почему метод crm.product.update не умеет создавать папки.

5 июля 2026 1 мин

Битрикс24: бизнес-процессы и роботы — автоматизация CRM без написания кода

Как мы автоматизировали воронку сделок в Битрикс24 с помощью бизнес-процессов и роботов — реальный кейс с SLA, автоназначением ответственных и мониторингом сбоев.

13 июня 2026 8 мин

Битрикс24: вебхуки и ограничение 1000 событий — как мы потеряли данные

На портале Битрикс24 можно создать максимум 2000 входящих вебхуков. Но мало кто знает, что каждый вебхук обрабатывает максимум 1000 событий в очереди. Если ваша интеграция не успевает — события удаляются. Рассказываю, как мы обнаружили потерю данных и построили систему надёжной обработки.

6 мая 2026 7 мин