Очереди и фоновые задачи на PHP: сравниваем варианты
Отправка письма на 800 мс блокирует HTTP-ответ, пользователь ждёт. Разбираем, чем гонять задачи в фоне на PHP: от crontab до RabbitMQ, и во что каждый вариант обходится.

Пользователь жмёт «Оформить заказ», а PHP в этот момент синхронно отправляет письмо через SMTP (300–800 мс), пушит вебхук в CRM (ещё столько же) и генерирует PDF. HTTP-ответ уходит через полторы секунды, хотя рендер страницы занял 40 мс. Всё «лишнее» надо вынести в фон — тогда ответ вернётся сразу, а тяжёлая работа доедет отдельно.
Проблема в том, что PHP по своей природе не демон: скрипт живёт от запроса до ответа и умирает. Держать в нём фоновый поток нечем — pcntl_fork в вебе не работает, а fastcgi_finish_request помогает только для мелочей. Ниже — четыре подхода к очередям и фоновым задачам, у каждого своя цена по инфраструктуре, надёжности и порогу входа.
Что вообще считаем «фоновой задачей»
Речь о работе, результат которой не нужен прямо в ответе: письма, уведомления, экспорт, обработка загруженного видео, синхронизация с внешним API. Задачу кладут в очередь (продюсер), отдельный процесс её забирает и выполняет (воркер). Между ними — брокер: место, где задача лежит и ждёт.
Инвалидация результата и распределённые транзакции — отдельная большая тема, здесь её не трогаем. Смотрим только на механику доставки задачи до воркера и цену выбора брокера.
Четыре подхода, которые реально встречаются
- cron + таблица в БД — задачи пишутся в таблицу, cron раз в минуту дёргает скрипт, который выбирает необработанные строки.
- Redis как брокер — списки (
LPUSH/BRPOP) или готовая обвязка. Быстро, но Redis по умолчанию хранит данные в памяти. - RabbitMQ / AMQP — полноценный брокер сообщений: подтверждения доставки, маршрутизация, устойчивость к падению воркера.
- Фреймворковые решения — Laravel Queue, Symfony Messenger. Абстракция поверх любого из брокеров выше плюс готовый воркер и retry.
cron + таблица: минимум зависимостей
Самый дешёвый по инфраструктуре вариант: ничего нового ставить не надо, всё уже есть. Продюсер делает INSERT в таблицу jobs, воркер по крону читает строки со статусом pending.
Ключевая ловушка — конкуренция воркеров. Если крон запускается каждую минуту, а задача выполняется дольше минуты, два процесса схватят одну строку. Решается блокировкой на уровне БД:
-- PostgreSQL 16, забираем задачу атомарно
UPDATE jobs
SET status = 'processing', started_at = now()
WHERE id = (
SELECT id FROM jobs
WHERE status = 'pending'
ORDER BY created_at
FOR UPDATE SKIP LOCKED -- пропускаем строки, занятые другим воркером
LIMIT 1
)
RETURNING id, payload;FOR UPDATE SKIP LOCKED — то, ради чего этот подход вообще жизнеспособен под нагрузкой: два воркера не подерутся за одну строку. В MySQL 8.0.1+ этот синтаксис тоже есть; в MySQL 5.7 его нет — там придётся городить статусную блокировку вручную и ловить гонки.
Очередь на БД работает, пока задач единицы в секунду. На сотнях в секунду таблица jobs превращается в самую горячую точку в базе, и вы платите за это временем ответа на обычных запросах.Когда этого хватает
Маленький проект, десятки задач в минуту, нежелание тащить лишний сервис. Задержка в «до минуты» (пока крон проснётся) приемлема. Плюс — задачи в той же транзакции, что и бизнес-данные: не бывает ситуации, когда заказ создан, а задача на письмо потерялась.
Redis: быстро, но помни про память
Redis отдаёт BRPOP за микросекунды, воркер получает задачу мгновенно, без опроса раз в минуту. Именно поэтому Laravel и Symfony по умолчанию рекомендуют Redis для очередей.
Цена — модель хранения. Redis держит данные в памяти. Без включённой персистентности (RDB-снапшоты или AOF-лог) перезагрузка сервера уносит всю очередь. С AOF данные сохраняются, но пишутся на диск не мгновенно — точную политику appendfsync проверяйте в конфиге своей версии, дефолт может отличаться. И даже с персистентностью голый LPUSH/BRPOP не даёт подтверждения обработки: воркер забрал задачу, упал на середине — задача потеряна.
Отсюда практика: не гоняйте по Redis-спискам задачи, потеря которых недопустима (списание денег, отгрузка). Для писем и уведомлений — нормально, повторная отправка не катастрофа.
RabbitMQ: подтверждения и устойчивость
RabbitMQ строился как брокер сообщений, а не как кеш, поэтому решает то, чего не умеет голый Redis:
- ack/nack — воркер подтверждает обработку. Упал до подтверждения — сообщение вернётся в очередь и уйдёт другому воркеру.
- durable-очереди и persistent-сообщения — переживают перезапуск брокера.
- маршрутизация — exchange раскидывает сообщения по очередям по ключу, одно событие уходит нескольким потребителям.
Плата — эксплуатация. Это Erlang-сервис, который надо мониторить, кластеризовать и понимать. Для «отправить письмо в фоне» это из пушки по воробьям. Оправдан, когда задач много, они разнородные и терять их нельзя.
Фреймворковые решения: абстракция поверх всего
Laravel Queue и Symfony Messenger не заменяют брокер — они дают единый интерфейс поверх БД, Redis, RabbitMQ, SQS. Пишете код один раз, брокер меняете в конфиге. Плюс — готовый воркер с перезапуском, retry с задержкой, dead-letter для задач, которые падают раз за разом.
Одна общая ловушка воркера в PHP: он не умирает после запроса, значит утечки памяти и «залипший» контейнер зависимостей копятся между задачами. Laravel queue:work и Symfony Messenger умеют перезапускать воркер после N задач или по лимиту памяти — включайте эти лимиты, иначе через несколько часов воркер съест всю RAM. Точные имена флагов (--max-jobs, --memory и аналоги) сверяйте в документации своей версии фреймворка, они менялись между мажорными релизами.
Сравнение
| Подход | Инфраструктура | Задержка | Потеря задач | Порог входа |
|---|---|---|---|---|
| cron + БД | ничего нового | до интервала крона | нет (в транзакции) | низкий |
| Redis-списки | Redis | мгновенно | возможна без ack | средний |
| RabbitMQ | отдельный брокер | мгновенно | защищено ack | высокий |
| Laravel/Symfony | любой брокер + воркер | зависит от брокера | зависит от брокера | средний |
Когда что выбирать
- Задач единицы в минуту, важна простота, задержка «до минуты» ок — cron + БД.
- Нужна мгновенность, задачи не критичны к потере (письма, кеши, уведомления) — Redis с включённым AOF.
- Задачи терять нельзя, их много и они разнородные — RabbitMQ.
- Проект уже на Laravel или Symfony — берите штатную очередь фреймворка и не изобретайте: она сама выберет брокер под нагрузку.
Что ломается на проде
Три источника ночных инцидентов независимо от выбранного подхода:
- Воркер не перезапущен после деплоя. Старый воркер держит в памяти старый код и обрабатывает задачи по устаревшей логике. После каждого деплоя воркеры надо рестартовать — в Laravel это
queue:restart, который посылает сигнал живым воркерам завершиться. - Ядовитая задача (poison message). Задача, которая падает всегда. Без лимита попыток и dead-letter-очереди она блокирует обработку и наматывает retry по кругу. Настройте максимум попыток и отдельную очередь для сдохших задач.
- Воркер под supervisor, а supervisor не настроен. Процесс воркера упал — и никто его не поднял. Держите воркеры под
supervisorили systemd с автоперезапуском, иначе очередь тихо перестаёт разбираться.
Что делать дальше
Начните с малого: если задач немного — таблица с FOR UPDATE SKIP LOCKED закроет вопрос без единого нового сервиса. Как только упрётесь в задержку крона или количество задач — переезжайте на Redis или штатную очередь фреймворка, код продюсера при грамотной абстракции менять не придётся. На проде повесьте мониторинг на две метрики: длину очереди (растёт — воркеры не справляются) и возраст самой старой необработанной задачи (растёт — воркеры встали).
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: Laravel Queues — официальная документация, Symfony Messenger Component
Частые вопросы
А если у меня MySQL 5.7, а не 8.0 — сработает FOR UPDATE SKIP LOCKED?
Почему нельзя просто запустить фоновую задачу через exec() или shell_exec()?
Что делать, если воркер завис и не обрабатывает задачи?
Redis потеряет мои задачи при перезагрузке?
Задача упала на середине — она выполнится повторно или потеряется?
Нужно ли перезапускать воркеры после каждого деплоя?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.