Your Laravel app sends mail perfectly in local development. You push to production, and the emails stop arriving. No exception is thrown, no error appears in the logs, and Mail::send() returns without complaint. Users report that password resets never arrive and order confirmations never show up.
In most cases the application code is correct. What changed is the mail configuration, the queue, or the SMTP host your app is now talking to. This guide covers the full production setup: the .env values that actually matter, mailable classes, queued sending, and the specific Laravel failures that let mail disappear without a trace.
Quick Answer: How Do You Configure Mail in Laravel?
Set the SMTP mailer in your .env file, then send through a mailable class:
# .env
MAIL_MAILER=smtp
MAIL_HOST=smtp.photonconsole.com
MAIL_PORT=587
MAIL_USERNAME=your_project_api_user
MAIL_PASSWORD=your_secret_api_key
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="noreply@yourdomain.com"
MAIL_FROM_NAME="${APP_NAME}"
use Illuminate\Support\Facades\Mail;
use App\Mail\OrderShipped;
Mail::to($user->email)->send(new OrderShipped($order));
Laravel reads these values through config/mail.php. The application code rarely causes delivery failures. The host you authenticate against, and whether your domain is authorised to send, is what decides if the message reaches an inbox. An authenticated relay such as PhotonConsole on port 587 removes most of the failure modes described below.
How Laravel Actually Sends Mail
Since Laravel 9, the framework uses Symfony Mailer underneath (earlier versions used SwiftMailer). Your app builds a message through a mailable class, Laravel hands it to the configured transport, and the transport opens an SMTP connection to whatever host you named in .env.
Laravel does not deliver the email itself. It is a client, not a mail server. That distinction explains most production failures: the Laravel side works exactly as written, but the transport it was pointed at cannot deliver.
Note
Configuration key names have shifted between Laravel versions. Laravel 11 and later accept MAIL_SCHEME alongside MAIL_ENCRYPTION, and Laravel 9 changed mailable classes from the older build() method to envelope() and content(). Check the official Laravel mail documentation for your specific version before copying configuration from an older tutorial.
Why Laravel Mail Fails in Production

Most SMTP errors occur due to misconfiguration or authentication issues rather than faults in application code. These are the causes that account for the majority of Laravel delivery failures.
1. MAIL_MAILER Is Still Set to log
Recent Laravel releases ship with MAIL_MAILER=log as the default in .env. This writes the entire email into storage/logs/laravel.log instead of sending it. Everything appears to work — no error, no exception, and the mailable is rendered correctly. The message simply never leaves the server.
This is the single most common reason a Laravel app “sends” mail that nobody receives.
2. Configuration Is Cached
If php artisan config:cache has been run, Laravel reads from the cached config file and ignores .env entirely. You can correct every mail value in .env, deploy, and see no change at all, because the cached configuration still holds the old credentials.
3. The Queue Worker Is Not Running
If your mailable implements ShouldQueue, Laravel pushes the message onto a queue instead of sending it. With no worker process running, jobs accumulate in the jobs table and nothing is ever delivered. The application reports success because queueing the job succeeded.
4. MAIL_FROM_ADDRESS Does Not Match an Authorised Domain
If your from address is noreply@yourdomain.com but your domain has no SPF record covering the sending server, mailbox providers cannot verify the message and filter or reject it. This is covered fully in our guide to SPF, DKIM and DMARC.
5. Your Host Blocks Outbound SMTP
AWS, Google Cloud, DigitalOcean and Azure restrict outbound port 25 by default — AWS documents its port 25 throttle removal process — and many shared hosts throttle SMTP entirely. The connection stalls and eventually times out with no useful message. Our breakdown of SMTP connection timeouts covers the diagnosis.
Quick Fix
Laravel Sends Locally but Not in Production
- Confirm
MAIL_MAILER=smtp, notlogorarray - Run
php artisan config:clearafter any.envchange - Check whether your mailable implements
ShouldQueue, and whether a worker is running - Confirm
MAIL_PORT=587withMAIL_ENCRYPTION=tls, or 465 withssl - Look in
storage/logs/laravel.log— if the full email body is there, the log driver is still active
Step-by-Step: Production Mail Setup
Step 1: Set the Correct Environment Values
Every mail value belongs in .env, never hardcoded in config/mail.php. Keep .env out of version control.
MAIL_MAILER=smtp
MAIL_HOST=smtp.photonconsole.com
MAIL_PORT=587
MAIL_USERNAME=your_project_api_user
MAIL_PASSWORD=your_secret_api_key
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="noreply@yourdomain.com"
MAIL_FROM_NAME="${APP_NAME}"
The from address matters more than it appears. It must be on a domain you have authenticated, not a Gmail or Yahoo address, or mailbox providers will treat the message as spoofed.
Step 2: Clear the Config Cache
Do this after every change to mail settings, on every environment.
php artisan config:clear
# In production, rebuild the cache afterwards
php artisan config:cache
Common Mistake
Editing .env on a production server without clearing the config cache. Laravel will keep using the cached values, so the new SMTP credentials are silently ignored and mail continues to fail in exactly the same way. If a configuration change appears to have no effect at all, this is almost always the reason.
Step 3: Create a Mailable Class
php artisan make:mail OrderShipped --markdown=emails.orders.shipped
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class OrderShipped extends Mailable
{
use Queueable, SerializesModels;
public function __construct(public Order $order) {}
public function envelope(): Envelope
{
return new Envelope(
subject: 'Your order has shipped',
);
}
public function content(): Content
{
return new Content(
markdown: 'emails.orders.shipped',
);
}
}
Step 4: Verify the Connection Before Trusting It
Use Tinker to send a real message before wiring mail into your application flow.
php artisan tinker
Mail::raw('Test message from Laravel', function ($m) {
$m->to('you@yourdomain.com')->subject('SMTP connection test');
});
If this throws an exception, the problem is configuration or connectivity. If it returns cleanly but nothing arrives, the problem is delivery — authentication records or filtering, not Laravel.
Step 5: Queue Mail So Requests Stay Fast

Never make a user wait for an SMTP handshake during a web request. Implement ShouldQueue on the mailable and Laravel will push it to a background job automatically.
use Illuminate\Contracts\Queue\ShouldQueue;
class OrderShipped extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
// ...
}
Set a real queue driver in .env — the default sync driver runs the job immediately and defeats the purpose:
QUEUE_CONNECTION=database
# then:
php artisan queue:table
php artisan migrate
php artisan queue:work
In production, run the worker under Supervisor or a systemd service so it restarts automatically. A queue worker that dies silently produces exactly the same symptom as broken SMTP: no errors, no email. Our guide to transactional email queue architecture covers the wider pattern.
Step 6: Handle Failures Explicitly
Laravel raises a TransportException when the SMTP conversation fails. Catch it and log the response rather than letting it disappear into a failed job.
use Illuminate\Support\Facades\Log;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
try {
Mail::to($user->email)->send(new OrderShipped($order));
} catch (TransportExceptionInterface $e) {
Log::error('Mail transport failed', [
'to' => $user->email,
'message' => $e->getMessage(),
]);
}
For queued mail, use the failed() method on the mailable, and monitor the failed_jobs table. For interpreting SMTP response codes inside these exceptions, see our reference on SMTP response codes.
Quick Fix
Queued Mail Never Sends
- Confirm
QUEUE_CONNECTIONis not set tosyncin production - Check that
php artisan queue:workis running under Supervisor - Restart workers after every deploy with
php artisan queue:restart - Inspect the
jobstable — a growing row count means no worker is consuming - Inspect
failed_jobsfor the actual exception message
Port and Encryption Reference
Mismatched port and encryption values cause connections that hang until they time out. Use one of these pairings — PhotonConsole accepts all three of the working combinations below.
| MAIL_PORT | MAIL_ENCRYPTION | When to use |
|---|---|---|
| 587 | tls | Recommended default for most applications |
| 465 | ssl | When the provider requires implicit SSL |
| 2525 | tls | When the host blocks 587 and 465 |
| 25 | null | Avoid — blocked by most hosting providers |
Configuring Laravel With PhotonConsole
Moving Laravel onto a dedicated relay is a change to .env and DNS. No application code changes, and mailable classes stay exactly as they are.
DNS Records
TXT @ v=spf1 include:relay.photonconsole.com ~all
CNAME photon._domainkey dkim.photonconsole.com
Note
DNS changes are not instant. SPF and DKIM records can take from a few minutes to 24-48 hours to propagate depending on your provider and TTL settings. Confirm propagation with a DNS lookup tool before assuming a record is misconfigured.
Environment Configuration
MAIL_MAILER=smtp
MAIL_HOST=smtp.photonconsole.com
MAIL_PORT=587 # 465 for implicit SSL, 2525 if 587 is blocked
MAIL_USERNAME=your_project_api_user
MAIL_PASSWORD=your_secret_api_key
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="noreply@yourdomain.com"
MAIL_FROM_NAME="${APP_NAME}"
php artisan config:clear
php artisan queue:restart
Port 2525 exists specifically for hosting environments that block the standard SMTP ports, which resolves the AWS and DigitalOcean problem described earlier. Every account includes 5,000 free emails per month, enough to validate the full setup — DNS records, queued sending and failure handling — before any spend. Configuration details are on the PhotonRelay page, and pricing has no monthly minimum.
If you are still comparing options, our analysis of free SMTP servers covers where each one breaks down under production load.
Environment-Specific Notes
Local Development
Use a mail catcher such as Mailpit or Mailtrap rather than sending real messages from a development machine. Set MAIL_MAILER=smtp with the catcher’s host and port so the code path matches production, rather than using the log driver, which hides transport problems until deploy day.
Laravel Forge and Envoyer
Environment variables are managed in the panel, not by editing .env over SSH. Changes made directly on the server can be overwritten on the next deploy. Update the values in Forge and redeploy so the change persists.
Shared Hosting and cPanel
Many shared hosts cap outbound mail per hour and route everything through their own server, which usually carries a poor sending reputation. Application email should go through an external relay rather than the host’s mail server.
Docker and Kubernetes
Containers have no local mail transfer agent, so an external relay is mandatory. Inject credentials through secrets rather than baking them into the image, and remember that config:cache run at image build time will freeze whatever values were present then.
Pro Tips for Reliable Laravel Mail
- Always run
queue:restartafter deploying. Long-running workers keep old code in memory, including old mail configuration. - Set a monitored Reply-To address. Use the
replyToparameter in the envelope rather than leaving a noreply address as the only route back to you. - Use markdown mailables for consistency. They produce a plain-text alternative automatically, which improves spam filter scores.
- Verify DNS after any change. MXToolbox confirms SPF, DKIM and blocklist status in a single lookup.
- Score a real send before launch. Mail Tester flags authentication problems before customers encounter them.
- Never log the mail password. Log the exception message and SMTP code, never the full config array.
- Separate transactional and bulk queues. A marketing batch that gets throttled should never delay a password reset.
Related Issues You May Hit Next
- SMTP authentication errors when credentials are rejected despite being correct
- SMTP not working across the ten most common failure modes
- Emails landing in Gmail spam despite a successful send
- Emails sent but not delivered when the SMTP response says success
- SMTP configuration reference for host, port and encryption settings
Frequently Asked Questions
Why does Laravel say mail was sent when nothing arrives?
Three usual causes: MAIL_MAILER is set to log, the config cache is stale, or the mailable is queued with no worker running. Check storage/logs/laravel.log first — if the full email body is written there, the log driver is still active.
Do I need to run config:clear every time?
After any change to .env, yes. Laravel reads cached configuration in preference to .env, so an uncleared cache means your new values are ignored entirely.
Should mail be queued or sent synchronously?
Queue it in any web application. Synchronous sending makes the user wait for the full SMTP conversation, which adds seconds to the response and fails the whole request if the mail server is slow.
What is the difference between MAIL_ENCRYPTION and MAIL_SCHEME?
Older Laravel versions use MAIL_ENCRYPTION with values tls or ssl. Laravel 11 and later also accept MAIL_SCHEME with smtp or smtps. Check the documentation for your version rather than mixing conventions.
Can I use Gmail SMTP for a Laravel application?
For local testing, yes. For production, no. Google publishes strict Workspace sending limits, throttles automated traffic, and can lock the account, which takes your authentication emails down with it. A dedicated relay such as PhotonConsole avoids that risk entirely.
Why do my emails go to spam even though Laravel sends them?
Delivery and inbox placement are separate. Laravel handing the message to a relay successfully says nothing about whether the receiving provider trusts your domain. Missing SPF or DKIM records are the usual cause.
How do I send email from a queued job in a different Laravel app?
The mail configuration is per application. Each app needs its own .env mail values, and each should authenticate with its own credentials so sending can be attributed and revoked independently.
Conclusion
Laravel’s mail layer is reliable. Almost every production failure traces back to one of four things: the log driver left active, a cached config that ignores your changes, a queue with no worker consuming it, or a sending domain with no authentication records.
Fix those four and Laravel mail works predictably at any volume an application is likely to need. What remains is infrastructure rather than framework: whether your host permits outbound SMTP, and whether the receiving provider trusts the domain you send from.
If your messages are being blocked by port restrictions, throttled by a mailbox provider, or filtered because your server has no sending reputation, no change to config/mail.php will fix it. A dedicated transactional email solution handles authentication, routing and reputation, using the same .env configuration shown above. Developers working across stacks may also want our guides to sending email in Node.js and sending email in Python.
Read More
- SMTP Configuration: Complete Setup Reference
- Transactional Email Queue Architecture Explained
- SMTP Retry Logic for Transactional Email Systems
- SPF, DKIM and DMARC Explained Simply
- Sending Email in Node.js with Nodemailer
- Sending Email in Python: smtplib vs an Email API
- PhotonRelay: SMTP Relay Service
- PhotonConsole Pricing

