
September 07, 2026
14 min read
By Kokil Thapa | Last reviewed: September 2026
Laravel Mail with Mailgun Postmark and SES is how most production Laravel applications send password resets, order receipts, booking confirmations, and client notifications without running their own SMTP server. Whether you deploy Laravel 13 on PHP 8.3+ or maintain Laravel 12 on PHP 8.2, all three providers plug into the same Mail facade and Mailable classes through Symfony Mailer transports. I've wired this stack on API-driven Laravel projects and legal-tech portals where a failed reset email blocks real users from completing a workflow. The goal is not to pick a winner in abstract—it's to configure each provider correctly, queue outbound mail, and know when to switch.
MAIL_MAILER to mailgun, postmark, or ses in .env, installing the matching first-party transport package, and verifying SPF, DKIM, and DMARC on your sending domain. Your Mailable classes stay identical across all three.How does Laravel Mail work with Mailgun, Postmark, and SES?
Laravel's mail layer sits on top of Symfony Mailer. You write Mailable classes; Laravel resolves a transport from config/mail.php based on MAIL_MAILER. Each provider ships a dedicated transport that translates Laravel's message object into that vendor's HTTP API call. That separation matters: business logic lives in your app, deliverability tooling lives at the provider.
On a typical production Laravel application, the flow looks like this:
- A controller, job, or listener calls
Mail::to($user)->send(new OrderConfirmed($order)). - If
ShouldQueueis implemented, the serialized Mailable lands on your queue driver (Redis 8.10 is a common choice). - A queue worker rebuilds the Mailable and hands it to the configured transport.
- The transport POSTs to Mailgun, Postmark, or SES and returns a message ID.
- The provider handles bounces, complaints, and delivery events via webhooks you can log or act on.
Your config/mail.php defines the mailers array. Laravel 13 ships with stubs for all three providers. The default mailer reads from the environment:
// config/mail.php (excerpt)
'default' => env('MAIL_MAILER', 'log'),
'mailers' => [
'mailgun' => [
'transport' => 'mailgun',
],
'postmark' => [
'transport' => 'postmark',
],
'ses' => [
'transport' => 'ses',
],
'log' => [
'transport' => 'log',
'channel' => env('MAIL_LOG_CHANNEL'),
],
], Keep MAIL_MAILER=log in local development unless you intentionally test against a sandbox domain. For staging, use each provider's sandbox or a dedicated test subdomain so you never pollute production sender reputation. The Laravel 13 mail documentation covers the full configuration surface, including failover mailers introduced in recent releases.
How do you configure Mailgun for Laravel Mail?
Mailgun fits teams that want flexible domain routing, EU data residency, and detailed event logs without AWS lock-in. Setup is straightforward once DNS is correct—most production incidents I've debugged trace back to missing DKIM, not application code.
Install the package and set environment variables
Install the official transport via Composer 2.10:
composer require symfony/mailgun-mailer symfony/http-client Then configure .env:
MAIL_MAILER=mailgun
MAIL_FROM_ADDRESS=noreply@yourdomain.com
MAIL_FROM_NAME="${APP_NAME}"
MAILGUN_DOMAIN=mg.yourdomain.com
MAILGUN_SECRET=key-xxxxxxxxxxxxxxxx
MAILGUN_ENDPOINT=api.mailgun.net For EU-hosted accounts, set MAILGUN_ENDPOINT=api.eu.mailgun.net. The endpoint must match where you created the domain in the Mailgun dashboard; mixing US credentials with an EU endpoint produces opaque 401 errors that look like Laravel bugs but are really region mismatches.
Verify DNS records
Mailgun provides TXT records for SPF, CNAME records for DKIM, and optionally a DMARC policy. Add them at your DNS host before sending production traffic. A common mistake on client projects is verifying the root domain while sending from a subdomain—pick one sending domain and align MAIL_FROM_ADDRESS with it.
Send a test Mailable
php artisan make:mail WelcomeUser --markdown=emails.users.welcome
// app/Mail/WelcomeUser.php
public function envelope(): Envelope
{
return new Envelope(
subject: 'Welcome to ' . config('app.name'),
);
} Trigger it from Tinker or a feature test:
Mail::to('you@example.com')->send(new WelcomeUser($user)); On booking systems like Adventure Third Pole Trek, confirmation emails must queue immediately after payment—never send synchronously inside the HTTP request that handles the gateway callback. Wrap the Mailable with ShouldQueue and ensure your worker processes the mail queue. Patterns from Laravel payment integrations apply directly here: the payment succeeds even if the mail worker is briefly down, because the job retries.
How do you set up Postmark and AWS SES in Laravel?
Postmark and SES serve different operational profiles. Postmark optimises for transactional-only sending with excellent default deliverability and minimal configuration. SES is the cost leader at scale and integrates naturally when your Laravel app already runs on AWS EC2 or RDS.
Postmark configuration
Install the transport:
composer require symfony/postmark-mailer symfony/http-client Environment variables:
MAIL_MAILER=postmark
POSTMARK_TOKEN=your-server-api-token Postmark uses a Server model: one server token per application environment. Create separate Postmark servers for staging and production—never share tokens. Verify your sender signature or domain in the Postmark dashboard, then add the DKIM CNAME records they provide.
Postmark rejects mail that looks promotional if your server is configured for transactional traffic. That discipline is useful: it forces you to route newsletters through a separate provider or server, which protects password-reset deliverability. For a deeper provider comparison, see the related write-up on AWS SES vs SendGrid vs Postmark for Laravel Mail.
AWS SES configuration
SES requires the AWS SDK and Symfony transport:
composer require aws/aws-sdk-php symfony/amazon-mailer Environment variables:
MAIL_MAILER=ses
AWS_ACCESS_KEY_ID=your-key
AWS_SECRET_ACCESS_KEY=your-secret
AWS_DEFAULT_REGION=ap-south-1 New SES accounts start in sandbox mode—you can only send to verified addresses until you request production access through the AWS console. For Laravel apps deployed on AWS EC2 with RDS, use an IAM role attached to the instance instead of long-lived access keys in .env. The SDK picks up instance metadata automatically when keys are omitted.
Verify your domain in SES, publish the three DKIM CNAME records, and enable SPF alignment through the included MAIL FROM subdomain. If you already send from the same domain via another provider, remove old DKIM records before adding SES's—duplicate DKIM selectors cause intermittent authentication failures that are painful to diagnose.
Failover between providers
Laravel supports a failover mailer that tries transports in sequence. This is useful during provider outages:
// config/mail.php
'failover' => [
'transport' => 'failover',
'mailers' => ['postmark', 'ses', 'log'],
],
// .env
MAIL_MAILER=failover Failover is not a substitute for monitoring. Log the final transport used and alert when the primary fails. On systems I maintain with GitLab CI deploy pipelines, mail credentials live in environment-specific secrets, never in the repository.
Which provider should you choose for Laravel Mail in production?
The right choice depends on volume, infrastructure, compliance, and who owns deliverability when something breaks. All three work with the same Mailable code—your decision is operational, not architectural.
| Criteria | Mailgun | Postmark | AWS SES |
|---|---|---|---|
| Best for | Multi-region apps, dev-friendly logs, flexible routing | Transactional-only apps prioritising inbox placement | High volume, AWS-native stacks, cost sensitivity |
| Typical cost at 50k emails/month | ~USD 35 / Rs 4,700 | ~USD 50 / Rs 6,700 | ~USD 5 / Rs 670 (plus data transfer) |
| EU / GDPR data residency | EU endpoint available | EU servers available | Region-selectable (e.g. eu-west-1) |
| Setup complexity | Moderate — domain + API key | Low — server token + DNS | Higher — IAM, sandbox exit, SNS for bounces |
| Marketing + transactional mix | Supported (separate domains recommended) | Transactional only on one server | Both, but reputation management is on you |
| Bounce handling | Webhooks + event API | Webhooks + detailed dashboard | SNS notifications → Lambda or HTTP endpoint |
Practical guidance from production work:
- Choose Postmark when you send mostly transactional mail—password resets, invoice PDFs, appointment confirmations—and want the fastest path to strong deliverability without becoming an email engineer.
- Choose SES when you already run on AWS, send high volume (100k+ monthly), and have someone who can manage SNS bounce pipelines and IAM policies.
- Choose Mailgun when you need EU routing, granular event logs, or a single API that also handles inbound parsing and route rules.
For a legal-tech portal like Mijar Law Associates, I default to Postmark or Mailgun because clients can read bounce dashboards without learning AWS. For Nepal Gift Card—a high-volume digital delivery platform—SES or Mailgun's pay-as-you-go pricing wins on unit economics. None of these choices require rewriting your Mailable layer; you change env vars, DNS, and webhook routes.
How do you queue, test, and debug transactional email in Laravel?
Configuration is half the job. Production reliability comes from queuing, testing, and handling bounces so you do not mail dead addresses for months.
Always queue outbound mail
Implement ShouldQueue on every Mailable that is not safety-critical and instant:
use Illuminate\Contracts\Queue\ShouldQueue;
class OrderConfirmed extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
public function __construct(public Order $order) {}
} Run a dedicated worker for the mail queue in production:
php artisan queue:work redis --queue=mail,default --tries=3 --backoff=60 Supervise that process with systemd or your process manager. After deploy, reload workers—stale workers are a recurring cause of "mail worked yesterday" reports on support and maintenance contracts.
Local and CI testing
Use MAIL_MAILER=array or log in PHPUnit. Assert mail was sent with:
Mail::fake();
Mail::assertSent(OrderConfirmed::class, function ($mail) use ($order) {
return $mail->order->id === $order->id;
}); For local preview, Postmark's sandbox server and Mailgun's test mode accept API calls without delivering to real inboxes. Mailtrap and similar inbox simulators are fine for UI review but do not validate DNS authentication—you still need a staging send through the real provider before launch.
When debugging payload issues, paste JSON webhook bodies into the JSON formatter tool to inspect bounce and complaint structures quickly.
Handle bounces and suppressions
Each provider sends webhook events for bounces, complaints, and unsubscribes. Create a signed route and mark users undeliverable:
// routes/web.php
Route::post('/webhooks/postmark', [PostmarkWebhookController::class, 'handle']);
// Disable mail to hard-bounced addresses in your User model
public function scopeMailable($query)
{
return $query->whereNull('email_bounced_at');
} Continuing to send to hard bounces damages domain reputation for every mail type—not just the one that bounced. Wire webhook handlers in the same sprint as mail integration, not six months later when Gmail starts spam-foldering password resets.
Structured logging helps. Log the provider message ID alongside your local correlation ID (order ID, booking ID). When a user claims they never received a reset link, you can trace the exact delivery event in the provider dashboard within seconds.
Align mail architecture with broader application patterns from modern Laravel architecture best practices—keep Mailables thin, put attachment generation in actions or services, and never embed raw HTML strings in controllers. For apps exposing outbound notifications via API, document mail triggers alongside endpoints in your Laravel API best practices guide internally so frontend teams know which actions send email.
Email also intersects with SEO for Laravel sites in one specific way: marketing pages should not share the transactional sending domain. A promotional blast that triggers spam complaints can drag password-reset mail into junk folders on the same domain. Use separate subdomains—tx.yourdomain.com for transactional, news.yourdomain.com for campaigns.
Hosting choice affects SES latency but rarely matters for Mailgun or Postmark. If you compare providers and infrastructure together, the AWS vs DigitalOcean vs Hetzner for Laravel hosting analysis helps frame total cost. For enterprise apps with strict audit trails, pair mail logging with database event records using patterns from database migrations best practices so you have a local copy of sent notifications.
On platforms that also push real-time updates—booking status, chat, admin alerts—mail complements rather than replaces broadcasting. See Laravel broadcasting with Reverb for the websocket side; email remains the durable fallback when the user is offline.
If you build custom admin panels with Filament or Livewire, wire a "resend notification" action that re-queues the same Mailable rather than duplicating template logic. The Laravel Livewire tutorial covers component patterns that map cleanly to admin resend buttons. For larger builds, enterprise application development engagements typically include mail as part of the notification matrix from day one—not as a post-launch patch.
Reference the official Mailgun documentation for inbound routing if your app parses replies, and the AWS SES developer guide for SNS bounce subscription setup. Both are authoritative when Laravel's abstraction layer hides the details you need during incident response.
Key Takeaways
- Laravel Mail with Mailgun Postmark and SES uses the same Mailable classes—switching providers is an env and DNS change, not a rewrite.
- Verify SPF, DKIM, and DMARC on your sending domain before production traffic; most deliverability failures are DNS problems, not PHP bugs.
- Queue every non-instant Mailable with
ShouldQueueand supervise workers across deploys. - Choose Postmark for transactional simplicity, SES for AWS-native cost at scale, Mailgun for EU routing and flexible domain rules.
- Handle bounces via webhooks immediately—suppress hard-bounced addresses to protect domain reputation.
- Keep transactional and marketing mail on separate subdomains so campaigns never poison password-reset deliverability.
People Also Ask
Can I use multiple email providers at the same time in Laravel?
Yes. Laravel's failover mailer tries transports in order until one succeeds. You can also route different Mailables to different mailers by calling Mail::mailer('postmark')->send(...) explicitly. Some teams send transactional mail through Postmark and marketing campaigns through Mailgun on a separate domain.
Does Laravel 13 require a separate package for SES?
You need symfony/amazon-mailer and aws/aws-sdk-php installed via Composer. Laravel 13 includes the configuration stubs, but the AWS SDK is not bundled by default. Provide credentials through IAM roles on EC2 or through standard AWS environment variables.
Why do my Laravel emails go to spam with Mailgun or SES?
Usually because DKIM or SPF is misconfigured, MAIL_FROM_ADDRESS does not match the verified domain, or you are sending from a fresh domain without warmed reputation. Check the provider's authentication dashboard, send a test to mail-tester.com, and confirm you queue mail rather than blasting thousands of messages synchronously from a loop.
Should I use SMTP instead of the Mailgun, Postmark, or SES API?
API transports are preferred in production. SMTP adds connection overhead, lacks rich error responses, and is harder to retry intelligently from queued jobs. SMTP remains acceptable for local development with Mailhog or Mailpit, but production Laravel apps should use the HTTP transports bundled with Symfony Mailer.
Ship reliable Laravel Mail in production
Laravel Mail with Mailgun Postmark and SES gives you production-grade transactional email without maintaining SMTP infrastructure. Pick the provider that matches your hosting and volume, verify DNS before the first real send, queue everything that can wait, and wire bounce webhooks in the same release as your first Mailable. The application code stays boring and portable—that is the point.
Need mail integrated into a booking flow, client portal, or eCommerce checkout? Contact us to scope Laravel notification architecture, provider setup, and production monitoring. For related reading, explore the Laravel blog archive or review shipped work in the portfolio.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

