Tijdens Black Friday verwerkte een van onze klanten 4.200 orders in twee uur. Zonder queue-architectuur was dat een uitval geworden. Met Laravel Horizon: geen enkel failed job. Hoe die architectuur eruit ziet.
Laravel Queues voor e-commerce: orderverwerking op schaal
4.200 orders in twee uur. Dat was de piek tijdens Black Friday bij een van onze klanten. Zonder queue-architectuur was dat systeem omgevallen. Met Laravel Horizon, goed ingerichte prioriteiten en een doordachte retry-strategie: nul failed jobs, geen klachten.
Dit artikel beschrijft hoe je queue-based orderverwerking bouwt die schaalt. Niet de theorie achter queues, maar de concrete implementatie: job dispatching, Horizon-configuratie, prioriteiten, retry logic en wat je doet als het toch misgaat.
Wat je leert in dit artikel
- Waarom queue-based orderverwerking onmisbaar is bij schaal
- Job dispatching: structuur en best practices
- Laravel Horizon: configuratie voor e-commerce workloads
- Prioriteiten instellen per jobtype
- Retry logic en failed job afhandeling
- Monitoring die je wakker houdt als het mis gaat
Waarom queues onmisbaar zijn bij schaal
Een order in een webshop triggert meerdere acties: aanmaken in het ERP, doorsturen naar het WMS, e-mail sturen naar de klant, voorraad bijwerken, loyaltypunten berekenen, fraud-check uitvoeren.
Als je dit synchroon doet — de één na de ander, terwijl de klant wacht — zijn twee dingen gegarandeerd: de checkout is traag, en één trage of falende externe API maakt de hele bestelling kapot.
Queues lossen beide problemen op. De checkout slaat de order op en geeft een bevestiging terug. Al het andere wordt asynchroon verwerkt door workers.
De vuistregel: alles wat niet nodig is voor het directe antwoord aan de gebruiker, gaat in een queue.Job structuur
Een Laravel job heeft één verantwoordelijkheid. Niet een job die de order naar het ERP stuurt én de e-mail verstuurt én de voorraad bijwerkt. Drie losse jobs.
// Eén job per verantwoordelijkheid
class ProcessOrderInErp implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
private readonly int $orderId
) {}
public function handle(ErpAdapterInterface $erp): void
{
$order = Order::findOrFail($this->orderId);
// Idempotency check: al verwerkt?
if ($order->erp_order_id !== null) {
return;
}
$erpOrderId = $erp->createSalesOrder($order);
$order->update([
'erp_order_id' => $erpOrderId,
'erp_synced_at' => now(),
]);
}
}
Geef nooit een volledig Eloquent-model mee aan een job als het vermijdbaar is. Gebruik een ID. De job haalt het model zelf op bij uitvoering. Zo vermijd je geserialiseerde stale data als de job pas minuten later wordt uitgevoerd.
Dispatching na een succesvolle order
Order dispatching hoort in een database-transactie. Zo garandeer je dat jobs alleen worden gedispatcht als de order daadwerkelijk is opgeslagen.
class OrderService
{
public function place(Cart $cart, array $paymentData): Order
{
return DB::transaction(function () use ($cart, $paymentData) {
// Order opslaan
$order = Order::create([
'customer_id' => $cart->customer_id,
'total' => $cart->total,
'status' => 'pending',
]);
foreach ($cart->items as $item) {
$order->lines()->create([
'product_id' => $item->product_id,
'quantity' => $item->quantity,
'price' => $item->price,
]);
}
// Jobs dispatchen — worden pas echt gequeued na commit
ProcessOrderInErp::dispatch($order->id)->onQueue('erp-sync');
SendOrderToWms::dispatch($order->id)->onQueue('fulfillment');
SendOrderConfirmationEmail::dispatch($order->id)->onQueue('email');
UpdateInventoryReservation::dispatch($order->id)->onQueue('inventory');
return $order;
});
}
}
Laravel's database-queue driver wacht automatisch op de transactie-commit. Als de transactie wordt teruggedraaid, worden de jobs niet gedispatcht. Dit voorkomt jobs voor orders die niet bestaan.
Prioriteiten met named queues
Niet elke job is even urgent. Een ERP-sync mag wachten. Een betaalbevestiging mag niet.
Stel named queues in en configureer workers die meerdere queues verwerken in volgorde van prioriteit.
// Dispatchen naar specifieke queue
ProcessOrderInErp::dispatch($orderId)->onQueue('erp-sync');
SendOrderConfirmationEmail::dispatch($orderId)->onQueue('email-high');
GenerateInvoicePdf::dispatch($orderId)->onQueue('pdf-low');
RecalculateLoyaltyPoints::dispatch($orderId)->onQueue('background');
# Worker die queues in prioriteitsvolgorde verwerkt
# Pakt eerst 'critical', dan 'high', dan 'default', dan 'low'
php artisan queue:work --queue=critical,high,default,low
De vuistregel voor prioriteiten:
| Queue | Jobtypen | Max vertraging |
|---|---|---|
| critical | Betaalverwerking, fraude-alerts | Seconden |
| high | Order-e-mails, ERP-sync voor nieuwe orders | < 1 minuut |
| default | WMS-sync, inventory-updates | < 5 minuten |
| low | PDF-generatie, rapportages, loyaltypunten | < 30 minuten |
| background | Data-exports, analytics-sync | Uren |
Laravel Horizon: queues beheren op schaal
Horizon is de queue manager van Laravel. Het geeft een dashboard met real-time inzicht in queue-doorvoer, job-duraties en failed jobs. Maar Horizon is meer dan monitoring — het beheert ook automatisch hoeveel workers per queue actief zijn.
// config/horizon.php — geconfigureerd voor een e-commerce workload
return [
'environments' => [
'production' => [
'supervisor-critical' => [
'connection' => 'redis',
'queue' => ['critical'],
'processes' => 3,
'tries' => 3,
'timeout' => 30,
],
'supervisor-high' => [
'connection' => 'redis',
'queue' => ['high'],
'processes' => 5,
'tries' => 5,
'timeout' => 60,
],
'supervisor-default' => [
'connection' => 'redis',
'queue' => ['default'],
'processes' => 4,
'balance' => 'auto',
'minProcesses' => 2,
'maxProcesses' => 10,
'tries' => 5,
'timeout' => 120,
],
'supervisor-low' => [
'connection' => 'redis',
'queue' => ['low', 'background'],
'processes' => 2,
'tries' => 3,
'timeout' => 300,
],
],
],
];
De balance: auto instelling op de default queue laat Horizon het aantal processen automatisch aanpassen op basis van de queue-diepte. Tijdens Black Friday schaalt het op. Op een rustige dinsdagochtend schaalt het terug.
Retry logic en backoff
Een job die faalt, moet het opnieuw proberen. Maar niet direct — een externe API die tijdelijk niet bereikbaar is, is na 1 seconde ook nog niet bereikbaar.
Exponential backoff laat de wachttijd groeien bij elke poging:
class ProcessOrderInErp implements ShouldQueue
{
public int $tries = 5;
public int $maxExceptions = 3;
// Backoff in seconden: 10s, 30s, 90s, 270s, 810s
public function backoff(): array
{
return [10, 30, 90, 270, 810];
}
public function retryUntil(): \DateTime
{
// Maximaal 2 uur proberen
return now()->addHours(2);
}
public function handle(ErpAdapterInterface $erp): void
{
try {
$order = Order::findOrFail($this->orderId);
$erpOrderId = $erp->createSalesOrder($order);
$order->update(['erp_order_id' => $erpOrderId]);
} catch (ErpRateLimitException $e) {
// Rate limit: wacht langer
$this->release(60);
} catch (ErpMaintenanceException $e) {
// Onderhoud: wacht lang
$this->release(600);
}
}
public function failed(\Throwable $exception): void
{
// Definitief mislukt: team notificeren
Log::error("Order {$this->orderId} kon niet naar ERP gestuurd worden", [
'exception' => $exception->getMessage(),
'order_id' => $this->orderId,
]);
Notification::route('slack', config('services.slack.operations_webhook'))
->notify(new OrderSyncFailed($this->orderId, $exception));
Order::find($this->orderId)?->update(['sync_status' => 'failed']);
}
}
Onderscheid in de failed-methode is nuttig: sommige mislukkingen vereisen directe actie (order niet in ERP, klant wacht), andere zijn informeel (loyaltypunten niet berekend, geen haast).
Monitoring en alerting
Horizon heeft ingebouwde metrics, maar je wilt ook externe alerting. Stel alerts in op:
Queue diepte. Als dehigh queue meer dan 500 jobs bevat, zijn er te weinig workers of is er iets structureel mis.
// In een scheduled command, elk uur
$highQueueSize = Queue::size('high');
if ($highQueueSize > 500) {
Notification::route('slack', config('services.slack.ops_webhook'))
->notify(new QueueBacklogAlert('high', $highQueueSize));
}
critical of high queue verdient een notificatie.
Job-duur. Een ERP-sync die normaal 2 seconden duurt maar nu 45 seconden neemt, wijst op een probleem bij de externe API. Horizon toont dit in het dashboard.
De Black Friday voorbereiding
Schaal je queue-verwerking voor Black Friday bewust op. Vijf concrete stappen:
- Verhoog het aantal Horizon-workers een dag van tevoren. Geen auto-scaling onder druk — dan is het al te laat.
- Test je retry logic met een staging-omgeving waarbij je de ERP-API tijdelijk offline haalt.
- Monitor queue-diepte actief, niet alleen via Horizon maar ook via een extern monitoringtool.
- Zet niet-urgente queues op pauze als het heel druk is. Loyaltypunten berekenen kan ook de volgende ochtend.
- Houd een runbook bij met de stappen die je neemt als een bepaald systeem uitvalt. Niet uitdenken op het moment zelf.
Conclusie
Queue-based orderverwerking is geen optimalisatie voor grote shops. Het is de standaardarchitectuur voor elke shop die serieus genomen wil worden bij drukte.
De combinatie van named queues met prioriteiten, Horizon voor worker-beheer en doordachte retry logic geeft je een systeem dat schaalt bij Black Friday en transparant is als er toch iets misgaat.
Laravel Horizon is de standaardoplossing voor queue-monitoring in productie. Zie de Laravel Queues documentatie voor de volledige referentie over job dispatching, middleware en retry-strategieën.
Wil je je queue-architectuur reviewen of een schaalbare orderverwerking bouwen voor je webshop? Bekijk onze Laravel-diensten of neem contact op. Gerelateerde artikelen:- Laravel voor hoge belasting: lessen uit Black Friday
- Event-driven architectuur in Laravel — events en listeners op schaal
- Laravel als e-commerce middleware — de complete architectuuraanpak
- Shopify API rate limits — rate limiting via queues

Geschreven door Ruthger Idema
15+ jaar ervaring in e-commerce development. Gespecialiseerd in Magento, Shopify en Laravel maatwerk.
Meer over ons team →