Summary
Webhook payloads are currently processed synchronously during the HTTP request. If the database is slow or under load, AWS SNS may not receive a 200 OK within its 15-second timeout and will retry — potentially causing duplicate processing pressure. Moving to a queued Job returns 200 immediately and decouples processing from the HTTP layer.
Implementation
1. Create app/Jobs/ProcessSesWebhook.php
Move the core logic from SesWebhookController into the Job:
- Payload parsing (SNS-wrapped vs. direct SES format)
Email, EmailRecipient, RecipientEvent creation
- Sentry error capture
2. Update SesWebhookController
The controller becomes thin:
public function __invoke(Request $request, string $token)
{
$project = Project::where('token', $token)->firstOrFail();
ProcessSesWebhook::dispatch($project, $request->all());
return response()->json(['status' => 'queued'], 200);
}
3. Configure the queue
The app defaults to the database queue driver. No infrastructure changes needed for basic use. Document QUEUE_CONNECTION=database in .env.example and add a note about running php artisan queue:work.
4. Flush the per-project cache tag (from issue #5) at the end of the Job after all DB writes succeed.
Acceptance Criteria
Summary
Webhook payloads are currently processed synchronously during the HTTP request. If the database is slow or under load, AWS SNS may not receive a
200 OKwithin its 15-second timeout and will retry — potentially causing duplicate processing pressure. Moving to a queued Job returns200immediately and decouples processing from the HTTP layer.Implementation
1. Create
app/Jobs/ProcessSesWebhook.phpMove the core logic from
SesWebhookControllerinto the Job:Email,EmailRecipient,RecipientEventcreation2. Update
SesWebhookControllerThe controller becomes thin:
3. Configure the queue
The app defaults to the
databasequeue driver. No infrastructure changes needed for basic use. DocumentQUEUE_CONNECTION=databasein.env.exampleand add a note about runningphp artisan queue:work.4. Flush the per-project cache tag (from issue #5) at the end of the Job after all DB writes succeed.
Acceptance Criteria
200within milliseconds regardless of DB loadfailed_jobstable with payload context.env.exampledocumentsQUEUE_CONNECTION