Queues vs synchronous execution — when and why
Concept
Queues vs synchronous execution — the fundamental decision: does this work need to complete BEFORE the response, or can it happen AFTER?
Synchronous execution: The controller waits for the work to finish before sending the response. Simple, consistent. User sees the result immediately. But if the work is slow (10 seconds to send emails), the request is slow.
Queue execution: The controller dispatches a job to the queue and immediately returns a response. A separate worker process picks up the job and executes it later. User gets a fast response. The work happens asynchronously.
When to use queues:
- Email / notifications: Always. Sending email via SMTP can take 1-5 seconds.
- Report generation: PDFs, CSV exports.
- External API calls: Webhooks, Stripe charges (with retry logic).
- Image processing: Resizing, thumbnail generation.
- Indexing for search: Updating Elasticsearch/Algolia.
- Anything that can fail and should retry.
When to keep synchronous:
- When the user NEEDS the result immediately (create order → show order ID).
- When the operation is fast (simple DB insert).
- When failure must block the action (payment: if charge fails, don't create the order).
Queue + response data: If you queue a payment, how does the user know it succeeded? Pattern: queue the job → poll the job status via /api/jobs/{id}/status → show result when done. Or use Laravel Echo + broadcasting to push the result.
Retry logic: Queued jobs can retry automatically. Synchronous code retries are manual.
Laravel queue drivers: sync (synchronous, for testing), database (simple), redis (fast, recommended), sqs (managed, AWS).
Code Example
<?php
// ============================================================
// SYNCHRONOUS — blocks the request
// ============================================================
class OrderController extends Controller
{
public function store(CreateOrderRequest $request): JsonResponse
{
$order = DB::transaction(function () use ($request) {
$order = Order::create($request->validated());
// ❌ These run synchronously — user waits for all of them
Mail::to($request->user())->send(new OrderConfirmation($order)); // 2s
Http::post('https://slack.com/webhook', ['text' => 'New order!']); // 1s
Pdf::create($order)->save("orders/{$order->id}.pdf"); // 3s
return $order;
}); // Total: ~6 seconds before response!
return response()->json($order, 201);
}
}
// ============================================================
// QUEUED — immediate response, work happens later
// ============================================================
class OrderController extends Controller
{
public function store(CreateOrderRequest $request): JsonResponse
{
$order = DB::transaction(function () use ($request) {
return Order::create($request->validated());
}); // ← fast, synchronous (must succeed before queuing)
// Queue everything else — these return immediately
SendOrderConfirmationEmail::dispatch($order); // dispatched to queue
NotifySlackAboutNewOrder::dispatch($order); // dispatched to queue
GenerateOrderInvoicePdf::dispatch($order); // dispatched to queue
return response()->json($order, 201); // responds in <100ms
}
}
// Job class — runs in a worker process
class SendOrderConfirmationEmail implements \Illuminate\Contracts\Queue\ShouldQueue
{
use \Illuminate\Foundation\Bus\Dispatchable, \Illuminate\Queue\InteractsWithQueue;
public function __construct(private readonly Order $order) {}
public $tries = 3; // retry up to 3 times on failure
public $backoff = [30, 60]; // wait 30s, then 60s between retries
public function handle(): void
{
Mail::to($this->order->user)->send(new OrderConfirmation($this->order));
}
public function failed(\Throwable $e): void
{
// Called after all retries are exhausted
\Log::error("Order email failed permanently", ['order' => $this->order->id, 'error' => $e->getMessage()]);
}
}
// Delay and chaining
SendOrderConfirmationEmail::dispatch($order)->delay(now()->addSeconds(5));
// Chain — run jobs in sequence (next only runs if previous succeeds)
\Illuminate\Support\Facades\Bus::chain([
new ProcessPayment($order),
new SendOrderConfirmationEmail($order),
new GenerateOrderInvoicePdf($order),
])->dispatch();