Filas e Tarefas Assíncronas no Laravel
Filas são a "equipe de bastidores" do Laravel — tarefas demoradas são transferidas para a fila para processamento assíncrono, para que os usuários não precisem esperar e as requisições retornem instantaneamente.
1. O Que Você Vai Aprender
- Configuração de Driver de Fila: Comparação de sync/database/redis/sqs
- Criação e Dispatch de Jobs: dispatch() / Queue::push()
- Mecanismo de retry em caso de falha: a tabela
tries/backoff/failed_jobs - Fila Prioritária, Latência e Tarefas em Lote: Batch/Bused
- Supervisor monitora o processo
queue:work
2. A História de um Usuário
(1) Dor: Exportar relatórios causa o congelamento de todo o sistema
Bob clicou em "Exportar Relatório Mensal" no painel administrativo do ShopMetrics — levou 30 segundos para gerar um arquivo CSV contendo 100.000 registros de pedidos, durante os quais a página ficou carregando, e Alice também enfrentou lentidão ao acessar o dashboard ao mesmo tempo. Para piorar, Bob acionou cinco exportações de relatório simultaneamente, esgotando o pool de processos PHP e causando um erro 502 em todo o site.
(2) Solução Usando Filas
A fila envia tarefas demoradas para o segundo plano — quando um usuário clica em "Exportar", ele imediatamente vê a mensagem "Relatório está sendo gerado", enquanto o job é executado lentamente em segundo plano; assim que termina, um email é enviado com o link de download.
// Sync — bloqueia por 30 segundos
$csv = ReportService::generateMonthlyReport($tenant);
// Async — retorna instantaneamente, Job executa em segundo plano
GenerateReportJob::dispatch($tenant, 'monthly');
// Usuário vê: "Relatório está sendo gerado. Enviaremos um email quando estiver pronto."
(3) Resultado
Depois que Bob implementou a fila, as requisições de exportação de relatório agora retornam em 100 ms, os jobs em segundo plano executam suavemente, e o dashboard de Alice não congela mais.
3. Configuração de Driver de Fila
(1) Comparação de Drivers
| Driver | Persistência | Performance | Adequação | Custo |
|---|---|---|---|---|
sync |
❌ Execução Instantânea | Mais rápido | Desenvolvimento/Testes | Gratuito |
database |
✅ Tabela no BD | Lento | Projeto pequeno | Gratuito |
redis |
✅ Memória | Rápido | Ambiente de Produção | Redis |
sqs |
✅ AWS | Alto | Grande escala | Pagamento por uso |
beanstalkd |
✅ Dedicado | Rápido | Médio | Gratuito |
(2) Configuração
# .env
QUEUE_CONNECTION=redis
# Conexão Redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
// config/queue.php
'connections' => [
'database' => [
'driver' => 'database',
'table' => 'jobs',
'queue' => 'default',
'retry_after' => 90,
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => '{default}',
'retry_after' => 90,
'block_for' => null,
],
],
(3) Criar Tabelas da Fila
# Para driver database
php artisan queue:table
php artisan queue:failed-table
php artisan migrate
# Cria: tabela jobs + tabela failed_jobs
(1) ▶ Exemplo: Configuração de Fila do ShopMetrics
# .env — Desenvolvimento
QUEUE_CONNECTION=database
# .env — Produção
QUEUE_CONNECTION=redis
REDIS_HOST=redis.shopmetrics.internal
REDIS_PORT=6379
# Criar tabelas da fila (driver database)
php artisan queue:table
php artisan queue:failed-table
php artisan queue:batches-table
php artisan migrate
Saída:
# Comando executado com sucesso
4. Criação e Distribuição de Jobs
(1) Criar um Job
php artisan make:job GenerateReportJob
# Cria: app/Jobs/GenerateReportJob.php
(2) Definir um Job
// app/Jobs/GenerateReportJob.php
class GenerateReportJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $backoff = 60;
public bool $deleteWhenMissingModels = true;
public function __construct(
public Tenant $tenant,
public string $reportType,
public string $format = 'csv',
) {}
public function handle(
ReportService $reportService,
Mailer $mailer,
): void {
$path = $reportService->generate(
$this->tenant,
$this->reportType,
$this->format,
);
$url = Storage::disk('s3')->temporaryUrl($path, now()->addDays(7));
$this->tenant->users->each(function ($user) use ($url) {
$mailer->to($user)->send(new ReportReadyNotification($url, $this->reportType));
});
}
public function failed(\Throwable $exception): void
{
Log::error('Falha na geração de relatório', [
'tenant_id' => $this->tenant->id,
'report_type' => $this->reportType,
'error' => $exception->getMessage(),
]);
}
}
(3) Distribuir o Job
// Dispatch básico
GenerateReportJob::dispatch($tenant, 'monthly');
// Dispatch com atraso
GenerateReportJob::dispatch($tenant, 'monthly')
->delay(now()->addMinutes(5));
// Dispatch para fila específica
GenerateReportJob::dispatch($tenant, 'monthly')
->onQueue('reports');
// Dispatch se a condição for atendida
GenerateReportJob::dispatchIf($tenant->subscription?->isActive(), $tenant, 'monthly');
// Dispatch após resposta enviada ao usuário
GenerateReportJob::dispatchAfterResponse($tenant, 'monthly');
| Método de Distribuição | Descrição | Cenários Aplicáveis |
|---|---|---|
dispatch() |
Enfileirar | Tarefa Assíncrona Geral |
dispatchSync() |
Executar sincronamente | Deve ser completado imediatamente |
dispatchAfterResponse() |
Executar após resposta | Tarefa leve |
delay() |
Execução Atrasada | Tarefas Agendadas |
onQueue() |
Fila Especificada | Por Prioridade |
(1) ▶ Exemplo: Job de Processamento de Pedidos do ShopMetrics
// app/Jobs/ProcessOrderJob.php
class ProcessOrderJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $backoff = [30, 60, 120];
public function __construct(public Order $order) {}
public function handle(
OrderService $orderService,
PaymentGateway $payment,
): void {
// Cobrar pagamento
$payment->charge($this->order);
// Atualizar estoque
$orderService->deductInventory($this->order);
// Disparar evento
event(new OrderPlaced($this->order));
// Enviar email de confirmação
$this->order->user->notify(new OrderConfirmationNotification($this->order));
}
public function failed(\Throwable $exception): void
{
$this->order->update(['status' => 'failed']);
$this->order->user->notify(new OrderFailedNotification($this->order));
}
}
Saída:
// Execução bem-sucedida
5. Mecanismo de Retry em Caso de Falha
(1) Configuração de Retry
// Na classe Job
public int $tries = 3; // Máximo de tentativas de retry
public int $backoff = 60; // Segundos entre retentativas
public int $timeout = 120; // Máximo de segundos por tentativa
// Ou backoff exponencial
public array $backoff = [30, 60, 120]; // 30s, 60s, 120s
// Ou backoff dinâmico
public function backoff(): int
{
return 30 * $this->attempts();
}
(2) Tratamento de Erros
# Ver jobs falhados
php artisan queue:failed
# Retentar um job falhado específico
php artisan queue:retry 5
# Retentar todos os jobs falhados
php artisan queue:retry all
# Excluir um job falhado
php artisan queue:forget 5
# Limpar todos os jobs falhados
php artisan queue:flush
(3) Ciclo de Vida da Tarefa na Fila
flowchart LR
A[Job Enfileirado] --> B[Fila]
B --> C[Worker Pega]
C --> D{Sucesso?}
D -->|Sim| E[Job Concluído]
D -->|Não| F{attempts < tries?}
F -->|Sim| G[Backoff + Retry]
G --> B
F -->|Não| H[Tabela failed_jobs]
H --> I[Retry Manual / Flush]
(1) ▶ Exemplo: Configuração de Retry em Caso de Falha do ShopMetrics
// app/Jobs/SendWebhookJob.php
class SendWebhookJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 5;
public array $backoff = [10, 30, 60, 120, 300];
public int $timeout = 30;
public function __construct(
public Shop $shop,
public array $payload,
) {}
public function handle(): void
{
$response = Http::timeout($this->timeout)
->post($this->shop->webhook_url, $this->payload);
if (!$response->successful()) {
$this->release($this->backoff[$this->attempts() - 1] ?? 60);
}
}
public function failed(\Throwable $exception): void
{
$this->shop->tenant->users->each(function ($user) {
$user->notify(new WebhookFailedNotification($this->shop));
});
}
}
Saída:
// Execução bem-sucedida
6. Tarefas em Lote
(1) Criar um Batch
php artisan queue:batches-table
php artisan migrate
// app/Jobs/ProcessTenantAnalyticsJob.php
class ProcessTenantAnalyticsJob implements ShouldQueue
{
use Batchable;
public function __construct(public Tenant $tenant) {}
public function handle(): void
{
if ($this->batch()->cancelled()) {
return;
}
AnalyticsService::computeForTenant($this->tenant);
}
}
(2) Distribuir Batch
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
$batch = Bus::batch(
Tenant::active()->get()->map(
fn ($tenant) => new ProcessTenantAnalyticsJob($tenant)
)
)->then(function (Batch $batch) {
// Todos os jobs completaram com sucesso
Log::info("Batch {$batch->id} completado: {$batch->totalJobs} tenants processados.");
})->catch(function (Batch $batch, \Throwable $e) {
// Primeira falha de job
Log::error("Batch {$batch->id} falhou: {$e->getMessage()}");
})->finally(function (Batch $batch) {
// Sempre executa (sucesso ou falha)
Cache::forget('analytics:computing');
})->name('Processar Analytics Mensal')
->onQueue('analytics')
->dispatch();
(3) Gerenciamento de Batch
// Verificar progresso do batch
$batch = Bus::findBatch($batchId);
$batch->progress(); // 0-100
$batch->processedJobs();
$batch->totalJobs();
$batch->failedJobs();
$batch->finished();
// Cancelar batch
$batch->cancel();
| Método | Descrição |
|---|---|
then() |
Todos os callbacks com sucesso |
catch() |
Callback de falha na primeira tentativa |
finally() |
Callback ao concluir (independentemente de sucesso ou falha) |
progress() |
Porcentagem Completa |
cancel() |
Cancelar Tarefas Restantes |
(1) ▶ Exemplo: Tarefa em Lote de Análise Mensal do ShopMetrics
// app/Console/Commands/ProcessMonthlyAnalytics.php
class ProcessMonthlyAnalytics extends Command
{
protected $signature = 'analytics:process-monthly';
public function handle(): int
{
$tenants = Tenant::active()->get();
$this->info("Processando analytics para {$tenants->count()} tenants...");
Bus::batch(
$tenants->map(fn ($t) => new ProcessTenantAnalyticsJob($t))
)->then(function (Batch $batch) {
$this->info("Todos os {$batch->totalJobs} tenants processados.");
})->catch(function (Batch $batch, \Throwable $e) {
$this->error("Batch falhou: {$e->getMessage()}");
})->name('Analytics Mensal')
->onQueue('analytics')
->allowFailures()
->dispatch();
return self::SUCCESS;
}
}
Saída:
// Execução bem-sucedida
7. Daemon Supervisor
(1) Instalar Supervisor
# Ubuntu/Debian
sudo apt-get install supervisor
# Criar configuração
sudo nano /etc/supervisor/conf.d/shopmetrics-worker.conf
(2) Configuração do Supervisor
[program:shopmetrics-worker-default]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/shopmetrics/artisan queue:work redis --queue=default --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/log/shopmetrics/worker-default.log
stopwaitsecs=3600
[program:shopmetrics-worker-reports]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/shopmetrics/artisan queue:work redis --queue=reports --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
numprocs=1
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/shopmetrics/worker-reports.log
(3) O Comando supervisor
# Ler nova configuração
sudo supervisorctl reread
sudo supervisorctl update
# Iniciar/parar/reiniciar workers
sudo supervisorctl start shopmetrics-worker-default:*
sudo supervisorctl stop shopmetrics-worker-default:*
sudo supervisorctl restart shopmetrics-worker-default:*
# Verificar status
sudo supervisorctl status
| Número de Processos | Fila | CPU | Memória | Descrição |
|---|---|---|---|---|
| 2 | default | Baixo | Médio | Tarefas Gerais |
| 1 | reports | Alto | Alto | Geração de Relatórios |
| 1 | analytics | Alto | Alto | Análise de Dados |
| 1 | notifications | Baixo | Baixo | Email/Push |
(1) ▶ Exemplo: Iniciando um Worker Multi-Fila do ShopMetrics
# Desenvolvimento — worker único, todas as filas
php artisan queue:work --queue=default,reports,notifications
# Produção — workers separados por prioridade de fila
# Prioridade: high > default > low
php artisan queue:work redis --queue=high,default
php artisan queue:work redis --queue=reports,low
# Processar jobs com limite de tempo (auto-restart para vazamentos de memória)
php artisan queue:work --max-time=3600 --max-jobs=1000
# Monitorar fila
php artisan queue:monitor redis:default,redis:reports
Saída:
# Comando executado com sucesso
8. Exemplo Compreensivo: Sistema de Relatórios Assíncronos do ShopMetrics
// ============================================
// Compreensivo: Sistema de Relatórios Assíncronos do ShopMetrics
// Abrange: jobs, batches, retries, supervisor, notifications
// ============================================
// app/Jobs/GenerateReportJob.php
class GenerateReportJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public array $backoff = [60, 180, 600];
public int $timeout = 600;
public bool $deleteWhenMissingModels = true;
public function __construct(
public Tenant $tenant,
public string $reportType,
public string $format = 'csv',
public ?int $userId = null,
) {
$this->onQueue('reports');
}
public function handle(ReportService $reportService): void
{
$path = $reportService->generate(
$this->tenant,
$this->reportType,
$this->format,
);
$downloadUrl = Storage::disk('s3')->temporaryUrl($path, now()->addDays(7));
$user = $this->userId ? User::find($this->userId) : $this->tenant->users->first();
$user?->notify(new ReportReadyNotification(
downloadUrl: $downloadUrl,
reportType: $this->reportType,
expiresAt: now()->addDays(7),
));
}
public function failed(\Throwable $exception): void
{
$user = $this->userId ? User::find($this->userId) : $this->tenant->users->first();
$user?->notify(new ReportFailedNotification(
reportType: $this->reportType,
error: $exception->getMessage(),
));
}
}
// Uso no controller
class ReportController extends Controller
{
public function generate(Request $request): JsonResponse
{
$validated = $request->validate([
'report_type' => 'required|in:monthly,weekly,custom',
'format' => 'sometimes|in:csv,xlsx,pdf',
'date_from' => 'sometimes|date',
'date_to' => 'sometimes|date|after:date_from',
]);
GenerateReportJob::dispatch(
tenant(),
$validated['report_type'],
$validated['format'] ?? 'csv',
auth()->id(),
);
return response()->json([
'message' => 'Geração de relatório iniciada. Você receberá um email quando estiver pronto.',
'estimated_time' => '5-15 minutos',
], 202);
}
}
❓ Perguntas Frequentes
queue:work e queue:listen?queue:work executa em memória sem reiniciar o framework (rápido, mas requer restart manual após alterações de código); queue:listen reinicia o framework para cada tarefa (lento, mas carrega automaticamente o novo código). Use queue:work + Supervisor em produção, e queue:listen ou queue:work --once durante o desenvolvimento.SerializesModels serializa automaticamente os IDs dos modelos e os recupera do banco de dados durante a deserialização. O benefício é que os Jobs têm menor pegada de dados; o risco é que se um modelo for excluído, o Job falhará (isso pode ser ignorado usando deleteWhenMissingModels).allowFailures() para permitir que o batch continue executando apesar de falhas parciais. Use Bus::findBatch() para ver detalhes das falhas e retentar manualmente as tarefas falhadas.QUEUE_CONNECTION=sync para executar o job sincronamente (erros são exibidos imediatamente); em produção, use php artisan queue:failed para ver mensagens de erro dos jobs falhados; você também pode registrar informações detalhadas no método failed() do job.queue:work como daemon.📖 Resumo
- Filas tornam tarefas demoradas assíncronas, permitindo que requisições de usuários sejam respondidas em segundos
- Redis é o melhor driver de fila para ambientes de produção, oferecendo alta performance e persistência
- O Job usa a configuração
tries/backoffpara definir a estratégia de retry; backoff exponencial previne avalanche - Processamento em lote de grandes quantidades de tarefas similares, com suporte a acompanhamento de progresso e falhas parciais
- O Supervisor monitora processos Worker e os reinicia automaticamente em caso de crash
- Múltiplas filas com níveis de prioridade: high/default/reports/low
📝 Exercícios
-
Exercício Básico (⭐): Crie um
GenerateReportJob, faça dispatch de uma requisição assíncrona para gerar um relatório CSV no controller, teste usando o driver database e executequeue:workpara verificar que o job é executado. -
Exercício Avançado (⭐⭐): Configure o driver de fila Redis para implementar 3 retentativas com backoff exponencial (30s/60s/120s), envie notificações aos usuários quando um job falhar e teste retentativas manuais (queue:retry).
-
Desafio (⭐⭐⭐): Use
Bus::batch()para implementar uma tarefa em lote de análise mensal multi-tenant — itere por todos os tenants ativos, crie um job por tenant, monitore a porcentagem de progresso, limpe o cache ao concluir e configure o Supervisor para monitorar o worker.



