Quatro segundos olhando um spinner
O usuário preenche o cadastro, clica em "Criar conta" e... espera. Um. Dois. Três. Quatro segundos com o botão em loading.
Do lado de cá, o INSERT no banco levou 12 milissegundos. O resto foi o servidor de e-mail respondendo com a pressa de quem não tem pressa. Depois teve a chamada pro CRM. E o PDF de boas-vindas sendo gerado.
O usuário não sabe de nada disso. Ele só sabe que o site é lento. E se a conexão cair no meio, ele clica de novo — e agora você tem dois cadastros.
A pergunta que resolve isso é bem simples: o usuário realmente precisa esperar por isso?
Na esmagadora maioria das vezes, não.
O problema na prática
Esse controller aqui provavelmente existe no seu projeto com outro nome:
public function store(CriarContaRequest $request)
{
$user = User::create($request->validated());
// 2s (se o SMTP estiver de bom humor)
Mail::to($user)->send(new BoasVindas($user));
// 1,5s e depende de uma API que não é sua
$this->crm->sincronizar($user);
// mais 1s, e ninguém vai abrir esse PDF hoje
$this->gerarPdfDeBoasVindas($user);
return redirect()->route('dashboard');
}
Quatro operações. Só a primeira é urgente.
E tem um detalhe pior que a lentidão: se o CRM estiver fora do ar, o cadastro inteiro explode. O usuário toma um erro 500 numa conta que já foi criada no banco. Você acabou de acoplar o sucesso do seu cadastro à disponibilidade de um sistema de terceiro.
O conceito: agora ou depois?
Fila é uma lista de tarefas que ficam guardadas em algum lugar (banco, Redis, SQS) esperando um processo separado — o worker — pegar e executar.
A regra pra decidir o que vai pra fila é quase sempre essa: se o usuário não precisa ver o resultado na tela seguinte, pode ir pra fila.
E-mail, notificação, sincronização com sistema externo, geração de relatório, redimensionamento de imagem, webhook — tudo isso é "depois". Criar o registro que a próxima tela vai mostrar é "agora".
A solução: um job por tarefa
php artisan make:job EnviarEmailDeBoasVindas
namespace App\Jobs;
use App\Models\User;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class EnviarEmailDeBoasVindas implements ShouldQueue
{
use Queueable;
// tenta 3 vezes antes de desistir
public int $tries = 3;
// espera 10s, depois 30s, depois 60s entre as tentativas
public array $backoff = [10, 30, 60];
public function __construct(public User $user) {}
public function handle(): void
{
Mail::to($this->user)->send(new BoasVindas($this->user));
}
}
O implements ShouldQueue é a chave. Sem ele, o job roda na hora, igual antes.
E o controller volta a ser rápido:
public function store(CriarContaRequest $request)
{
$user = User::create($request->validated());
EnviarEmailDeBoasVindas::dispatch($user);
SincronizarComCrm::dispatch($user);
GerarPdfDeBoasVindas::dispatch($user);
return redirect()->route('dashboard');
}
Três dispatch custam alguns milissegundos — é só um INSERT na tabela de jobs. O usuário cai no dashboard quase instantaneamente, e o trabalho pesado acontece por trás.
Melhor ainda: se o CRM estiver fora, só aquele job falha. O cadastro seguiu firme.
Colocando pra rodar
Pra começar, o driver database resolve bem (você troca por Redis quando o volume pedir). No .env:
QUEUE_CONNECTION=database
A tabela de jobs já vem na migration padrão do Laravel. Se seu projeto for antigo e não tiver:
php artisan make:queue-table
php artisan migrate
E aí você sobe o worker:
php artisan queue:work
Em produção esse comando precisa de alguém tomando conta dele — Supervisor, systemd, ou o Horizon se você usa Redis. Worker que morre e ninguém reinicia é fila que enche em silêncio.
As pegadinhas (todas custam caro pelo menos uma vez)
1. Job não guarda o objeto, guarda o ID. O trait Queueable serializa só a chave primária do model e busca no banco de novo na hora de executar. Isso é ótimo — o dado nunca fica velho — mas tem um efeito colateral: se o registro for deletado entre o dispatch e a execução, o job estoura ModelNotFoundException. Por isso o padrão é passar o model, não um array com os dados: você ganha o dado fresco de graça.
2. Deploy sem queue:restart não muda nada. O worker carrega seu código na memória quando sobe e fica lá rodando. Corrigiu um bug no job, subiu pra produção, e continua com o comportamento antigo? É isso. Coloca no final do seu script de deploy:
php artisan queue:restart
Todo mundo passa por essa. Uma vez.
3. Job falhou e ninguém viu. Depois de esgotar as tentativas, o job vai pra tabela failed_jobs e some da sua vida. Se você nunca olha essa tabela, e-mail nenhum foi enviado e você não sabe. Trate a falha:
public function failed(\Throwable $e): void
{
Log::error("Falha ao enviar boas-vindas pro user {$this->user->id}", [
'erro' => $e->getMessage(),
]);
}
Se a tabela não existir no seu projeto: php artisan make:queue-failed-table. Pra reprocessar tudo depois de corrigir: php artisan queue:retry all.
4. Dispatch dentro de transaction. Se você despacha o job antes do commit, o worker pode ser rápido demais e ir buscar um registro que ainda não existe. O afterCommit() resolve:
EnviarEmailDeBoasVindas::dispatch($user)->afterCommit();
Ou você liga 'after_commit' => true na conexão dentro de config/queue.php e esquece o assunto.
Bônus: e se eu não quiser manter um worker?
Nem todo projeto tem infra pra Supervisor rodando. Pra esses casos, o Laravel 12 trouxe duas conexões que ajudam bastante:
RegistrarAcesso::dispatch($user)->onConnection('deferred');
A deferred executa o job no mesmo processo, mas depois que a resposta HTTP já foi enviada pro navegador. A background dispara um processo PHP separado. Nos dois casos o usuário não espera — e você não precisa de worker nenhum.
Não substitui uma fila de verdade (não tem retry robusto, não tem monitoramento, não aguenta volume), mas pra um projeto pequeno que só quer tirar o e-mail do caminho do request, é uma mão na roda.
Antes de você fechar a aba
Abre o controller mais lento do seu projeto e lê linha por linha se perguntando: "o usuário precisa ver isso na próxima tela?".
Cada "não" é um dispatch esperando pra acontecer. E provavelmente uns 2 segundos a menos no botão. ⚡
E você, já tomou a do deploy sem queue:restart ou ainda tem esse prazer pela frente? Conta nos comentários — e se seu projeto tem failed_jobs acumulando poeira, essa é a hora de dar uma olhada lá.
Top comments (0)