Нужно отдать страницу или AJAX-ответ быстро, а тяжёлую мелочь (лог, webhook, пересчёт счётчика, письмо) добить уже после того, как браузер получил контент. В D7 для этого есть Application::addBackgroundJob(). Это не Redis-очередь и не замена cron: job живёт в том же PHP-процессе и выполняется сразу после отдачи ответа.
Рядом по теме: Session и LocalSession, агенты CAgent и cron, Application, Context, Request, Mail\\Event: send и sendImmediate.
Если нужна помощь с модулем или разбором «фоновых» задач на проде — услуги и контакты на karoche.ru.
Что это и чем не является
Официальная формулировка API: метод добавляет фоновое задание, которое выполняется после отдачи контента браузеру. Метод нестатический — вызывайте через Application::getInstance().
Документация BitrixFramework уточняет контракт жёстче:
- Задача ставится в очередь текущего запроса.
- После отправки контента пользователю система выполняет задачу.
- Ответ сервера при этом не блокируется ожиданием job.
- Выполнение не гарантируется: при аварийном завершении скрипта job может не выполниться.
Отсюда границы:
| Ожидание | Реальность |
|---|---|
| «Уйдёт в воркер Redis/Rabbit» | Нет, тот же PHP-процесс после ответа |
| «Переживёт падение FPM» | Нет, durable-хранилища нет |
| «Можно писать в $_SESSION» | Обычно нет: сессия уже закрыта |
| «Замена агента раз в час» | Нет, для расписания — CAgent/cron |
| «Есть ретраи и backoff» | Нет; для этого смотрите Messenger (alpha) или свою очередь |
Подходит для короткого хвоста: метрика, лёгкий webhook, дописать лог, дернуть CAgent::CheckAgents с низким приоритетом (так делает пример в API). Не подходит для генерации PDF на минуту, массового импорта и всего, что нельзя потерять молча.
Минимальный контракт
Сигнатура по D7 API:
<?php
public function addBackgroundJob(
callable $job,
array $args = [],
int $priority = \Bitrix\Main\Application::JOB_PRIORITY_NORMAL
);
Приоритеты в классе Application:
JOB_PRIORITY_NORMAL = 100— значение по умолчанию;JOB_PRIORITY_LOW = 50.
Базовый вызов:
<?php
$app = \Bitrix\Main\Application::getInstance();
$app->addBackgroundJob(
[\Vendor\Module\Service\Notifier::class, 'ping'],
[15420],
\Bitrix\Main\Application::JOB_PRIORITY_NORMAL
);
Пример из официальной документации API — проверка агентов после ответа:
<?php
\Bitrix\Main\Application::getInstance()->addBackgroundJob(
["CAgent", "CheckAgents"],
[],
\Bitrix\Main\Application::JOB_PRIORITY_LOW
);
Callable принимается в привычных PHP-формах: имя функции, [$object, 'method'], [ClassName::class, 'staticMethod'], замыкание. Для замыканий есть отдельная ловушка с use — ниже.
В Framework-доке есть сценарий с файлом: сначала сохранить upload во временный путь на диске, потом в job передать путь и имя — не держать большой бинарник в памяти до конца ответа без нужды.
Когда BackgroundJob, когда агент, когда Messenger
На karoche уже есть разбор агентов (таблица b_agent, хиты, cron). Короткая матрица выбора:
| Задача | Инструмент |
|---|---|
| Короткий хвост текущего хита (лог, метрика, лёгкий webhook) | addBackgroundJob |
| Повторять каждый час / сутки | CAgent (+ лучше cron, не только хиты) |
| Долгая/гарантированная работа с ретраями | Bitrix\Main\Messenger (с main 25.100.300, alpha, без обещания BC) или внешняя очередь |
| Письмо «прямо сейчас» в том же хите | Mail\Event::sendImmediate (см. статью про Mail\\Event) |
| Письмо можно отложить до эпилога/почтовика | Mail\Event::send или BackgroundJob вокруг своей отправки |
Messenger официально: сообщение кладётся в брокер (сейчас тип db), обработчик забирает его; режим run_mode = web или cli (php bitrix.php messenger:consume); есть retry_strategy. Это уже другая архитектура. Достаточно помнить: если нельзя потерять job при падении воркера — BackgroundJob не ваш инструмент.
Комбинация из framework-дока допустима: BackgroundJob регистрирует одноразового агента для длинного поэтапного процесса. Так вы не держите FPM на длинной работе в хвосте того же запроса.
Сессия закрыта, master-only и аргументы
К моменту запуска background jobs ядро уже отдало ответ. В типичном terminate() перед пулом jobs сессия закрывается (session_write_close / штатное сохранение), а connection pool часто переключается в режим «только мастер». Практические следствия для кода модуля:
- Не пишите в
$_SESSION/getSession()из job. Данные либо не сохранятся, либо вы получите сюрприз на следующем хите. Нужен флаг пользователю — передайте id в$argsи пишите в БД/кеш/LocalSession до постановки job, либо обновляйте сущность в БД из самого job. - Все нужные скаляры кладите в
$args. Не рассчитывайте, что замыкание «увидит» живой$USERи открытую сессию так же, как в основном хите. - Замыкание с
useпо ссылке на огромный массив удержит память до конца job. Лучше id сущности и повторное чтение из БД внутри метода. - Реплика: job может принудительно идти на master. Не стройте в job «чтение со слегка отстающей реплики как источник истины» без явной проверки своего окружения.
Связка с Session/LocalSession: LocalSession тоже не предназначен как «очередь после ответа». Для post-response работы передавайте аргументы явно.
Плохо:
<?php
$orderId = 15420;
$app = \Bitrix\Main\Application::getInstance();
$session = $app->getSession();
$app->addBackgroundJob(function () use ($orderId, $session) {
// сессия уже закрыта - set здесь не то место
$session->set('last_pinged_order', $orderId);
// ...
});
Лучше:
<?php
$app = \Bitrix\Main\Application::getInstance();
$app->addBackgroundJob(
[\Vendor\Module\Service\OrderPing::class, 'run'],
[15420]
);
// Vendor\Module\Service\OrderPing.php
final class OrderPing
{
public static function run(int $orderId): void
{
// читаем заказ из БД, шлём webhook, пишем свой лог-таблицу
}
}
Сигнатура call_user_func_array разворачивает $args как список параметров. Если метод ждёт один массив — передайте один элемент: [$payload]. Если ждёт именованные поля как отдельные аргументы — следите за порядком в массиве $args (числовые ключи 0..n). Для массива вида [15420] удобнее метод с одним аргументом-массивом или явный список [15420].
Практичный безопасный стиль:
<?php
$app->addBackgroundJob(
[OrderPing::class, 'run'],
[15420] // попадет в run(int $orderId)
);
Приоритеты и jobs из jobs
Очередь внутри запроса приоритетная: сначала выше priority (NORMAL=100 выше LOW=50). Системные «не срочные» вещи вроде CAgent::CheckAgents в примере API ставят в LOW, чтобы ваши NORMAL-job успели раньше в том же хвосте.
Ядро обрабатывает очередь циклом: jobs могут добавлять новые jobs, и они тоже будут выполнены, пока очередь не опустеет. Это удобно для цепочки «после ответа: шаг A → поставил шаг B». Это же опасно: легко устроить длинный хвост и упереться в max_execution_time уже после того, как пользователь получил 200 OK. Пользователь счастлив, FPM-воркер ещё занят — и при аварии часть цепочки молча пропадает.
Правило: один job — одна короткая ответственность. Длинную цепочку выносите в агента или Messenger.
Исключения внутри job ядро логирует через exception handler; поведение «упал один — остальные» зависит от версии/ветки, поэтому не стройте бизнес-инварианты вида «job 2 выполнится всегда, даже если job 1 кинул Throwable». Проверяйте на своём main.
Типичные ловушки
| Симптом | Вероятная причина | Что сделать |
|---|---|---|
| Job «иногда не бывает» | Падение/kill PHP после ответа; это не durable queue | Критичное — в агент/Messenger/свою очередь |
set в сессии из job «молчит» |
Сессия закрыта до runBackgroundJobs |
Аргументы + БД/кеш до постановки job |
| Замыкание не видит свежие данные | Захватили значение use до изменения |
Передавать id в $args, читать в job заново |
| FPM «висит» после 200 OK | Слишком тяжёлый или цепочка jobs | Укоротить; тяжёлое — в агент/очередь |
| Путаете с cron «раз в N минут» | Неверный инструмент | CAgent / cron |
Статический вызов Application::addBackgroundJob(...) |
Метод нестатический | Только getInstance()->addBackgroundJob(...) |
| Ждёте ретраев как в Rabbit | Их нет в BackgroundJob | Messenger retry_strategy (alpha) или свой брокер |
Передали ассоциативный $args в метод с int $id |
Разъезд сигнатуры call_user_func_array |
Список позиционных аргументов или один payload-массив |
Чеклист перед коммитом
- Job короткий (сотни миллисекунд, не минуты).
- Потеря одного выполнения допустима для бизнеса (или есть идемпотентный повтор другим механизмом).
- Данные переданы через
$args/ диск / БД, не через открытую сессию. - Нет статического вызова
Application::addBackgroundJob. - Приоритет осознан: NORMAL для вашего хвоста, LOW для системного «можно позже в этом же запросе».
- Не подменили агент/cron BackgroundJob-ом.
- Для гарантий и ретраев запланирован Messenger (с оговоркой alpha) или внешняя очередь — не «надежда на хвост FPM».
Источники
- Application::addBackgroundJob (D7 API)
- Агенты и фоновые задачи (BitrixFramework)
- Очереди сообщений Messenger (alpha)
Практические заметки и услуги по Битрикс — на karoche.ru.

