Architecture and Processing
Webhook Concurrency and Race Conditions
Webhook events often arrive in bursts. When multiple events modify the same resource at the same time, race conditions can corrupt data or cause incorrect state transitions.
Example race condition
Imagine a payment provider sending two events:
- invoice.paid
- subscription.updated
If both webhooks update the same database record simultaneously, one write may overwrite the other.
Why concurrency happens
Most webhook providers deliver events asynchronously and in parallel.
Large systems frequently send multiple events for a single action. This makes concurrency unavoidable.
Queue webhook processing
The first rule of production webhook handling is:
never process heavy logic inside the HTTP request.
dispatch(new ProcessWebhookEvent($payload));
Queues isolate webhook ingestion from business logic execution.
Database locking
When multiple events update the same resource, use database locks.
DB::transaction(function () use ($userId) {
$user = User::lockForUpdate()->find($userId);
$user->subscription_status = 'active';
$user->save();
});
This prevents concurrent updates from corrupting the record.
Optimistic concurrency
Another strategy is optimistic locking using version numbers.
update users
set version = version + 1
where id = ? and version = ?
If the version changes, the update fails and the job retries.
Idempotent webhook handlers
Idempotency is essential for concurrency safety.
Each webhook event should be processed only once.
WebhookEvent::firstOrCreate([
'event_id' => $eventId
]);
If the event was already processed, the handler simply exits.
Observing concurrency problems
Race conditions are difficult to detect because failures happen under load. Monitoring webhook execution timelines helps identify overlapping events.
A monitoring tool can show exactly when multiple events were delivered and how long each handler took to execute.