Maintaining a bounce rate under 2% is critical for keeping your AWS SES account active. If your bounce rate exceeds 10%, AWS will suspend your sending capabilities entirely.
Setting up automated listeners using Amazon SNS (Simple Notification Service) and Amazon SQS (Simple Queue Service) is the enterprise-grade solution to handle bounces and complaints.
1. The Feedback Loop Architecture
Using a webhook directly from SES works, but SQS provides a queue buffer. If your app goes down for maintenance, SQS stores the bounce event and redelivers it when your app goes back online.
[AWS SES Bounce] ---> [SNS Topic] ---> [SQS Queue] ---> [Laravel App Worker]
2. Creating the SNS Topic and SQS Queue
A. AWS SNS Configuration
- Go to the Amazon SNS Console.
- Click Topics -> Create topic.
- Select Standard, name it
ses-bounce-topic, and click create.
B. AWS SQS Configuration
- Go to the Amazon SQS Console.
- Click Queues -> Create queue.
- Select Standard, name it
ses-bounce-queue. - After creation, select the queue, click Queue Actions, and select Subscribe queue to SNS topic. Select
ses-bounce-topic.
3. Mapping AWS SES Notifications
Now link your verified SES domain to the SNS topic:
- Open the AWS SES Console.
- Select your verified domain under Verified identities.
- Under Feedback notifications, click Edit.
- Set Bounces, Complaints, and Deliveries to target your newly created SNS Topic (
ses-bounce-topic).
4. Setting Up the Laravel Listener Job
Install the AWS SDK via composer:
composer require aws/aws-sdk-php
Create a command php artisan make:command ProcessEmailBounces to poll the SQS queue:
public function handle()
{
$sqs = new SqsClient([...]);
$result = $sqs->receiveMessage([
'QueueUrl' => env('AWS_SQS_BOUNCE_URL'),
'MaxNumberOfMessages' => 10,
]);
if (!empty($result->get('Messages'))) {
foreach ($result->get('Messages') as $message) {
$body = json_decode($message['Body'], true);
$notification = json_decode($body['Message'], true);
if ($notification['eventType'] === 'Bounce') {
foreach ($notification['bounce']['bouncedRecipients'] as $recipient) {
// Save to local blocklist database table
SuppressionList::create(['email' => $recipient['emailAddress']]);
}
}
// Delete message from queue
$sqs->deleteMessage(['ReceiptHandle' => $message['ReceiptHandle']]);
}
}
}
Schedule this command to run every minute in routes/console.php to clean your mailing lists in real-time.