Привет, Хабр!
Принято считать, что лимит у REST API Битрикс24 один: не больше двух запросов в секунду. Интеграцию пишут с паузой в полсекунды между вызовами, ловят редкие QUERY_LIMIT_EXCEEDED повтором через секунду и больше о лимитах не думают. Для небольшого портала этого и правда хватает.
Со временем портал вырастает до сотен тысяч сделок. Ночная выгрузка сделок в хранилище, которая год отрабатывала за несколько минут, начинает идти часами и падать с HTTP 429.
Повтор через секунду не помогает, повтор через минуту тоже. А crm.item.get, задачи, контакты — всё остальное отвечает как обычно, и со стороны портал выглядит совершенно здоровым.
Причина во втором лимите, который считает не запросы, а время. И в постраничной выборке. По умолчанию она тратит это время на подсчёт строк, о котором её никто не просил.
Два лимита с разными кодами
Первый лимит касается частоты запросов. Битрикс24 считает его как дырявое ведро: каждый HTTP‑запрос добавляет единицу, каждую секунду из ведра вытекает Y единиц, а когда счётчик доходит до X, следующий запрос получает статус 503 и код QUERY_LIMIT_EXCEEDED.
По документации:
Для Enterprise: X = 250 и Y = 5.
Для остальных тарифов: X = 50 и Y = 2.
Ведро своё у каждого портала. Приложения с одного сервера делят его между собой. Считает оно HTTP‑запросы, а не строки в ответе.
Второй лимит называется operating, и он устроен по‑другому. Портал суммирует, сколько секунд выполнялся каждый метод, отдельно для каждого приложения или вебхука, в 10 одноминутных корзинах.
Перед очередным вызовом сумма сверяется с порогом. Вызов, который переваливает через порог, выполняется до конца. Следующий вызов того же метода тем же приложением получает 429 и код OPERATION_TIME_LIMIT. Остальные методы и остальные приложения продолжают работать.
Справка Битрикс24 называет конкретный порог: 480 секунд за последние 10 минут.
Документация для разработчиков формулирует аккуратнее: «для примера предположим, что установленный лимит равен 480 секундам», а в облаке «значение лимита задаётся конфигурацией Битрикс24».
Обе формулировки сходятся в главном. Порог измеряется в секундах серверного времени. В коробочной версии ограничение по умолчанию выключено, а если его включить, по умолчанию оно равно 420 секундам и меняется в настройках модуля rest.
Это объясняет симптом из начала. Повтор через секунду бесполезен, потому что корзины освобождаются поминутно, и самая старая выпадет из суммы, только когда ей исполнится 10 минут. А соседние методы работают, потому что считаются отдельно.
По этому признаку два лимита и различаются в логах:
QUERY_LIMIT_EXCEEDED(503) — ведро отказывает всем методам портала сразу, и отказ длится секунды.OPERATION_TIME_LIMIT(429) —operatingотказывает одному методу одного приложения. Остальные вызовы того же приложения проходят.
Если в логе 429 только у crm.item.list, а crm.item.get в ту же минуту отвечает 200, ведро тут ни при чём.
Где видно, сколько времени уже потрачено
Каждый ответ REST содержит блок time. В документации он выглядит так:
"time": { "start": 1743057653.725821, "finish": 1743057654.0894129, "duration": 0.36359190940856934, "processing": 0.3278491497039795, "date_start": "2025-03-27T09:40:53+03:00", "date_finish": "2025-03-27T09:40:54+03:00", "operating_reset_at": 1743058253, "operating": 0.32782983779907227 }
Здесь важны два поля:
operating— накопленное время выполнения этого метода для этого приложения, число, которое сравнивается с порогом.operating_reset_at— момент, когда из суммы выпадет самая старая корзина.
В примере он на 600 секунд позже start: корзина живёт 10 минут с первого вызова в своей минуте.
Для диагностики хватает записывать эти два поля в лог интеграции рядом с именем метода.
Если operating у crm.item.list растёт на секунды с каждым вызовом и подходит к сотням, блокировка — вопрос нескольких минут. Если растёт на десятые доли секунды, лимит упрётся в частоту запросов задолго до того, как сработает operating.
Куда уходят секунды
Базовая выгрузка написана по образцу примеров к методам списка: метод списка, start и поле next из ответа:
$start = 0; do { [$status, $res] = b24($webhook, 'crm.deal.list', [ 'select' => ['ID', 'TITLE', 'OPPORTUNITY', 'STAGE_ID'], 'start' => $start, ]); foreach ($res['result'] as $deal) { $sink($deal); } $start = $res['next'] ?? null; usleep(500000); } while ($start !== null);
Код рабочий и годами не ломается.
Страница самого crm.item.list в документации учит тому же: размер страницы всегда 50 записей, для N‑й страницы передавайте start = (N-1) * 50, а когда записей больше, в ответе появится next.
Про start = -1 там не сказано ни слова, он описан на отдельной странице про большие объёмы данных.
Только при start >= 0 метод на каждой странице заново считает общее число записей для поля total.
Документация показывает, во что это обходится: на выборке из 2 387 743 элементов crm.item.list с подсчётом отвечал 49,9 секунды, а с start = -1 — 0,097 секунды.
Разница в 500 раз, и это одна страница из 50 записей.
Подсчёт происходит на каждой странице, а не один раз, поэтому выгрузка из 2,4 миллиона сделок превращается в 47 755 страниц, и каждая из них заново пересчитывает все 2,4 миллиона записей, хотя за время выгрузки число сделок меняется в лучшем случае на несколько десятков.
Пока сделок было 20 000, подсчёт стоил доли секунды и терялся в паузе между запросами. С ростом портала он растёт вместе с ним, а лимит operating остаётся тем же.
Сколько это стоит на разных порталах
Чтобы понять, где граница, я собрал модель выгрузки по правилам из документации.
В ней дырявое ведро на 50 запросов с вытеканием 2 в секунду, operating с порогом 480 секунд в 10 одноминутных корзинах и клиент с паузой в полсекунды, который после 429 ждёт выпадения старой корзины.
Стоимость страницы взята из того же замера документации: 0,098 секунды на выборку и подсчёт total, пропорциональный числу сделок, из расчёта 49,9 секунды на 2,4 миллиона.
Пропорциональность — допущение модели, да и вообще реальный подсчёт зависит от фильтра и прав.
Порядок она даёт верный:
сделок с подсчётом total start=-1 и фильтр по ID 20000 6.8 мин, ответов 429: 0 4.0 мин, 429: 0 100000 1.5 ч , ответов 429: 9 19.9 мин, 429: 0 500000 36.2 ч , ответов 429: 217 1.7 ч , 429: 0 2387743 33.2 сут, ответов 429: 4775 7.9 ч , 429: 0
На 20 000 сделок разницы почти нет, и в этой точке интеграцию обычно и пишут.
На 100 000 выгрузка с подсчётом уже упирается в operating и ловит первые 429. На полумиллионе она не укладывается в ночь, а на 2,4 миллионах формально заканчивается через месяц.
На деле она не заканчивается никогда.
Без подсчёта та же работа упирается только в частоту запросов, и operating даже не приближается к порогу.
Порога в коде нет, выгрузка замедляется месяц за месяцем вместе с числом сделок и однажды перестаёт успевать до утра. Ошибок до этого момента в логах нет.
Как померить цену подсчёта на своём портале
Модель показывает порядок, а свой портал проще померить двумя запросами. Подойдёт любой входящий вебхук с правами на CRM:
W=https://example.bitrix24.ru/rest/1/xxxxxxxxxxxx curl -s "$W/crm.item.list.json" -H 'Content-Type: application/json' \ -d '{"entityTypeId":2,"select":["id"],"start":0}' \ | jq -c '[.total, .time.duration, .time.operating]' curl -s "$W/crm.item.list.json" -H 'Content-Type: application/json' \ -d '{"entityTypeId":2,"select":["id"],"order":{"id":"ASC"},"filter":{">id":0},"start":-1}' \ | jq -c '[.total, .time.duration, .time.operating]'
Первый запрос вернёт число сделок и время с подсчётом.
Второй вернёт null вместо total и время без подсчёта.
Разница в duration и есть цена одной страницы. Её остаётся умножить на число страниц и сравнить с порогом. Если одна страница с подсчётом стоит 5 секунд, порог в 480 секунд кончится примерно на сотой странице, на 5000-й сделке.
Третье число полезно в другом смысле. operating во втором ответе уже включает время первого, потому что это тот же метод того же вебхука.
На портале, где этим вебхуком пользуется что‑то ещё, по нему видно, сколько из 10-минутного бюджета уже занято.
Выгрузка без подсчёта
Документация описывает правильный способ: выбирать страницы фильтром по последнему полученному идентификатору и передавать start = -1.
Сортировка по id, фильтр >id и цикл до первой неполной страницы:
function exportDeals(string $webhook, callable $sink): int { $lastId = 0; $resetAt = time(); $count = 0; while (true) { [$status, $res] = b24($webhook, 'crm.item.list', [ 'entityTypeId' => 2, 'order' => ['id' => 'ASC'], 'filter' => ['>id' => $lastId], 'select' => ['id', 'title', 'opportunity', 'stageId'], 'start' => -1, ]); if ($status === 429 && ($res['error'] ?? '') === 'OPERATION_TIME_LIMIT') { sleep(max(1, $resetAt - time())); continue; } if ($status === 503 && ($res['error'] ?? '') === 'QUERY_LIMIT_EXCEEDED') { sleep(1); continue; } $resetAt = $res['time']['operating_reset_at'] ?? $resetAt; $items = $res['result']['items'] ?? []; foreach ($items as $item) { $sink($item); $lastId = $item['id']; $count++; } if (count($items) < 50) { return $count; } } }
entityTypeId = 2 — это сделки в универсальном методе crm.item.list, поля у него в camelCase.
В старом crm.deal.list то же самое пишется через 'order' => ['ID' => 'ASC'] и 'filter' => ['>ID' => $lastId].
Функция b24 — обычный POST на адрес вебхука с JSON в теле, которая возвращает пару из HTTP‑статуса и разобранного ответа.
Обработка 429 сделана так, как советует документация: при блокировке ждать до operating_reset_at из последнего успешного ответа, а не повторять запрос через секунду.
Повтор раньше этого момента ничего не даст, а каждый отказ всё равно стоит HTTP‑запроса из ведра частоты.
Портала под рукой у меня не было. Логику я проверил на заглушке: небольшом HTTP‑сервере, который отвечает на crm.item.list в формате Битрикс24, держит 1 234 сделки с дырами в нумерации, как в живой CRM, и на седьмом вызове один раз возвращает 429 с OPERATION_TIME_LIMIT.
Функция дождалась сброса, продолжила с того же lastId и выгрузила все 1 234 сделки без дублей и пропусков.
Если разрабатываете под Битрикс24, попробуйте пройти вступительный тест. Он поможет оценить текущий уровень знаний и понять, какие темы стоит подтянуть.
batch не обходит operating
Следующая естественная мысль — завернуть вызовы в batch по 50 штук.
Это действительно помогает с первым лимитом: ведро считает HTTP‑запросы, а batch с 50 вызовами внутри — один запрос.
Со вторым лимитом не помогает.
Документация оговаривает это отдельно:
Внешний вызов
batchвoperatingне учитывается.Вложенные вызовы считаются как отдельные методы, каждый со своим учётом времени и своей проверкой порога.
Для выгрузки без подсчёта batch всё равно полезен.
Страницы по >id идут последовательно, потому что каждой нужен последний идентификатор предыдущей. Но если идентификаторы плотные, выгрузку можно резать на диапазоны и отправлять 50 диапазонов одним запросом.
Я добавил этот вариант в ту же модель.
Выгрузка 2,4 миллиона сделок сократилась с 7,9 часа до 97 минут.
Зато operating впервые за всю модель стал заметен: 9 ответов 429 за выгрузку.
Каждый вложенный вызов стоит те же 0,098 секунды, и 50 таких вызовов в одном запросе добавляют к сумме почти 5 секунд. Сто таких запросов за 10 минут уже дают больше порога.
Кого ещё задевает блокировка
Учёт operating ведётся на пару «метод и приложение» или «метод и вебхук».
С точки зрения портала ночная выгрузка и виджет в карточке сделки, который показывает менеджеру связанные сделки, — одно и то же, если они работают через одно приложение и оба вызывают crm.item.list.
Выгрузка выбрала порог, и следующие 10 минут виджет получает тот же 429. В логах ночной задачи этого не видно: 429 достаётся другому коду.
Для фоновых задач разумно заводить отдельный вебхук или отдельное приложение.
Порог от этого не вырастет. Зато тяжёлая выгрузка будет тратить своё время, а не время пользовательских сценариев.
Заодно в логах станет видно, какая часть интеграции сколько тратит, по тому же полю operating.

Выгрузить тысячи сделок из Битрикс24 и не упереться в лимиты API — только одна из задач разработчика. А как автоматизировать согласования, настроить бизнес‑процессы или создать собственный модуль?
На бесплатных уроках разберём эти задачи на практике и покажем, какие возможности Битрикс24 стоит использовать.
26 октября в 20:00. «Бизнес‑процессы в Битрикс24». Записаться
17 ноября в 20:00. «Пассивный доход для программиста возможен? Написание модуля для Битрикс24 и его публикация на маркетплейс». Записаться
Больше вебинаров — в дайджесте OTUS.

