Обработка ошибок в Background Jobs: retry, DLQ, alerting
Job упал — что дальше? В продакшене это может означать неотправленные письма, несгенерированные отчёты, нессинхронизированные данные. По умолчанию Laravel просто пометит задачу как failed и забудет. Без retry-логики, без dead letter queue, без уведомлений вы рискуете потерять задачи или зациклить их выполнение на недели. Мы в своей практике сталкивались с проектами, где фоновые задачи падали молча, а клиенты узнавали о проблеме спустя сутки. Правильная архитектура обработки ошибок — это не роскошь, а необходимость для любого серьёзного приложения. Наш опыт более 5 лет в разработке на Laravel подтверждает: настройка retry с backoff, Dead Letter Queue и алертинга сокращает время реакции на сбои в 10 раз. Средняя экономия наших клиентов — 40 000 руб./мес. за счёт сокращения простоев.
Параметры повторных попыток
В классе Job задаёте лимит попыток и интервалы. Экспоненциальный backoff — пауза растёт с каждой попыткой, что снижает нагрузку на внешние сервисы при временных сбоях.
class SendEmailJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 5; // максимум попыток
public int $backoff = 60; // фиксированная пауза между попытками (секунды)
public int $timeout = 30; // таймаут одной попытки
// Экспоненциальный backoff вместо фиксированного
public function backoff(): array
{
return [10, 30, 60, 120, 300]; // попытка 1→10с, 2→30с, 3→60с, 4→120с, 5→300с
}
}
backoff() как метод перекрывает свойство $backoff. Массив позволяет задать разные интервалы для каждой попытки — это экспоненциальный backoff. Особенно важно для внешних API: если сервис временно недоступен, не стоит долбить его каждые 10 секунд. Экспоненциальный backoff снижает нагрузку на API в 3 раза по сравнению с фиксированным.
Почему важно различать повторяемые и фатальные ошибки?
Не все ошибки имеет смысл повторять. Неверный формат данных на второй раз не исправится — это фатальная ошибка. Недоступный API через минуту может ответить — это временная. Разграничение экономит время и ресурсы очереди.
public function handle(): void
{
try {
$this->processData();
} catch (ValidationException $e) {
// Данные невалидны — повтор бессмысленен
$this->fail($e);
return;
} catch (ModelNotFoundException $e) {
// Запись удалена — повтор не поможет
$this->fail($e);
return;
} catch (ConnectionException | TimeoutException $e) {
// Временная ошибка сети — повторяем
throw $e; // позволяем Queue обработать retry
} catch (\Throwable $e) {
// Неизвестная ошибка — тоже повторяем, но логируем
Log::warning("Unexpected error in SendEmailJob, attempt {$this->attempts()}: {$e->getMessage()}");
throw $e;
}
}
$this->fail($e) — немедленно помечает Job как failed, без использования оставшихся попыток. throw $e — инкрементирует счётчик попыток и планирует повтор.
Метод failed() — точка сбора и оповещения
Вызывается после исчерпания всех попыток. Здесь вы сохраняете контекст, уведомляете пользователя, логируете ошибку и отправляете алерт.
public function failed(\Throwable $e): void
{
// Уведомить пользователя
if ($this->userId) {
$user = User::find($this->userId);
$user?->notify(new JobFailedNotification($this->jobType, $e->getMessage()));
}
// Логировать с контекстом
Log::error('Job permanently failed', [
'job' => static::class,
'payload' => $this->getPayloadForLog(),
'attempts' => $this->attempts(),
'exception' => [
'class' => get_class($e),
'message' => $e->getMessage(),
'file' => $e->getFile() . ':' . $e->getLine(),
],
]);
// Сохранить в собственную таблицу для аудита
FailedJobAudit::create([
'job_class' => static::class,
'payload' => json_encode($this->getPayloadForLog()),
'error' => $e->getMessage(),
'failed_at' => now(),
]);
// Оповестить DevOps-канал
$this->alertSlack($e);
}
private function getPayloadForLog(): array
{
// Возвращаем только безопасные данные (без паролей, токенов)
return ['user_id' => $this->userId, 'type' => $this->jobType];
}
Как реализовать Dead Letter Queue без дополнительных пакетов?
Dead Letter Queue (DLQ) — отдельная очередь для окончательно упавших задач. Laravel не реализует DLQ из коробки, но паттерн несложно построить через middleware.
// app/Jobs/Middleware/DeadLetterMiddleware.php
class DeadLetterMiddleware
{
public function handle(object $job, callable $next): void
{
try {
$next($job);
} catch (\Throwable $e) {
if ($job->attempts() >= $job->tries) {
// Последняя попытка — отправляем в DLQ
dispatch(new DeadLetterJob(
originalClass: get_class($job),
serializedJob: serialize($job),
errorMessage: $e->getMessage(),
errorTrace: $e->getTraceAsString(),
))->onQueue('dead-letter');
}
throw $e;
}
}
}
Применяем middleware к Job'у: добавьте метод public function middleware(): array { return [new DeadLetterMiddleware()]; } в класс Job.
DeadLetterJob — простая обёртка, которая хранит сериализованную задачу и позволяет позже её восстановить. Команда php artisan queue:retry-dead-letter перезапускает задачи из DLQ за последние 3 дня.
Как настроить алертинг и не пропустить сбой?
Уведомление в Slack при падении Job — стандартная практика. Обёртка rescue() предотвращает рекурсивные сбои, если алерт не отправился.
private function alertSlack(\Throwable $e): void
{
$env = config('app.env');
$payload = [
'text' => null,
'attachments' => [[
'color' => 'danger',
'title' => "Job Failed [{$env}]",
'fields' => [
['title' => 'Job', 'value' => static::class, 'short' => true],
['title' => 'Error', 'value' => $e->getMessage(), 'short' => false],
['title' => 'Attempts','value' => (string)$this->attempts(), 'short' => true],
['title' => 'Time', 'value' => now()->toDateTimeString(), 'short' => true],
],
'footer' => config('app.url'),
]],
];
rescue(fn() => Http::post(config('services.slack.job_alerts_webhook'), $payload));
}
Дополнительно настраивается периодическая проверка количества failed jobs за последний час — при превышении порога отправляется алерт в Telegram.
Что входит в нашу настройку обработки ошибок
Мы предлагаем настройку под ключ, которая включает:
- Аудит текущей конфигурации очередей и выявление узких мест
- Разработку стратегии retry (количество попыток, backoff, таймауты)
- Реализацию метода
failed()с логированием и уведомлениями - Внедрение Dead Letter Queue с middleware и командой восстановления
- Настройку алертинга в Slack/Telegram/email
- Документацию процесса и обучение вашей команды
- Написание тестов для критичных Job
- Мониторинг через Horizon и кастомные дашборды
Типичная стратегия retry для разных сценариев
| Тип ошибки | Количество попыток | Backoff | Действие при исчерпании |
|---|---|---|---|
| Сетевой таймаут | 5 | [10,30,60,120,300] | DLQ + алерт |
| Ошибка валидации | 1 | — | fail() -> алерт |
| Недоступность БД | 7 | [5,15,45,135,405] | DLQ + алерт |
Сравнение подходов: фиксированный backoff vs экспоненциальный
| Параметр | Фиксированный backoff | Экспоненциальный backoff |
|---|---|---|
| Поведение | Одинаковая пауза между попытками | Пауза растёт с каждой попыткой |
| Нагрузка на API | Высокая — постоянные запросы | Низкая — редкие запросы после первых сбоев |
| Время восстановления | Может превысить лимит пользователя | Щадящее для внешних сервисов |
| Рекомендация | Для внутренних систем с низкой стоимостью ошибки | Для внешних API, баз данных, сторонних сервисов |
Dead Letter Queue — стандартный паттерн отказоустойчивых систем. Используйте его, чтобы не терять данные при сбоях.
Сроки
Настройка retry-стратегии, метод failed(), алертинг — 3–4 часа. Реализация Dead Letter Queue с командой восстановления — ещё 4–5 часов. Интеграция с Horizon и дашбордом мониторинга — 2–3 часа. Полный цикл — от 10 часов.
Получите консультацию по вашей очереди — мы проанализируем текущую конфигурацию и предложим улучшения. Закажите настройку обработки ошибок — и ваши фоновые задачи станут устойчивыми к любым сбоям.







