Why Your Magento 2 Transactional Emails Are Not Sending
Most Magento 2 email problems are not SMTP problems. Since 2.3, order and invoice emails are queued and only sent when cron runs, so a dead cron means silence. Here is the fix-first path: queue and cron, then SMTP, then SPF, DKIM, DMARC and templates.
“Orders come in but no confirmation email goes out” is one of the most common Magento support tickets, and most of the time it is not an SMTP problem at all. It is a queue that never gets processed because cron is dead. This guide fixes transactional email end to end on Magento 2.4.4 to 2.4.9, fix-first: start with the queue and cron, then SMTP, then deliverability, then the templates and per-type toggles.
Start here: the 60-second triage
Before you touch a single SMTP field, answer four questions in order. The large majority of “Magento not sending email” cases are solved at step one or step two, and jumping straight to SMTP wastes hours.
- Is asynchronous sending on, and is cron actually running? (The usual culprit.)
- Can the server send mail at all, or is Magento handing messages to an unconfigured
sendmail? - Is the sending domain authenticated with SPF, DKIM, and DMARC so mail is not silently dropped or spam-filed?
- Is the specific email type enabled, and does its template still compile?
Cause number one: async sending plus a dead cron
Since Magento 2.3, order, invoice, shipment, and credit memo emails are not sent during the web request. When sales_email/general/async_sending is enabled (the default), Magento marks the sales row with send_email = 1 and email_sent = NULL, then moves on so checkout stays fast. A background cron job, sales_send_all_emails in Magento_Sales, is what actually queries those rows and sends the mail. If cron is not running, that job never fires and no email ever leaves the box, no matter how perfect your SMTP settings are.
Check the setting and the backlog first:
# 1 = async (queued), 0 = send during the request
bin/magento config:show sales_email/general/async_sending
-- how many order emails are waiting to be sent?
SELECT entity_id, increment_id, email_sent
FROM sales_order
WHERE send_email = 1 AND email_sent IS NULL;
If that query returns rows, the pipeline is fine but the flush never ran. Confirm cron is installed and alive for the web user, then force the due jobs:
crontab -l | grep magento
tail -n 50 var/log/cron.log
# run it twice: the first pass schedules jobs, the second executes them
bin/magento cron:run
bin/magento cron:run
After cron runs, re-check the query above. The email_sent column should flip to 1 and the mail should be on its way. If cron is not installed at all, that is its own problem, walked through in fixing Magento cron when it is not running.
Sales emails are driven by the cron job above, not by the message queue. But Magento also runs a broader queue system, and several background tasks depend on consumers being alive. List them with bin/magento queue:consumers:list. On a default install cron starts the consumers for you; if you disabled that runner (cron_consumers_runner), you must start them yourself, for example bin/magento queue:consumers:start sales.rule.update.coupon.usage --max-messages=1000. Either cron or a running consumer process must exist for any queued work to finish.
Force a send to prove the pipeline
To separate a queue problem from a mail-transport problem, take cron out of the equation and send synchronously. Turn async off, flush config cache, and place a test order:
bin/magento config:set sales_email/general/async_sending 0
bin/magento cache:flush
# now place a real test order in the storefront
If the confirmation email arrives with async off but not with async on, your problem is cron or the consumer, so go back to the previous section. If it still does not arrive with async off, the message is failing at the transport layer, which means SMTP. Turn async back on once you finish testing; synchronous sending slows checkout under real load.
Magento has no built-in SMTP
This surprises people coming from other platforms. Out of the box, Magento’s Magento\Framework\Mail layer hands every message to the local mail transport, PHP’s mail() function, which in turn calls the server’s sendmail binary. On a typical container or VPS that binary is either missing, unconfigured, or sending from an IP with no reputation, so the mail is refused or dropped straight into spam. There is no host, port, or password field in core Magento because core does not speak SMTP.
The fix is an SMTP extension that routes mail through a real transactional provider (a dedicated email service, not a personal mailbox). This store runs Mageplaza SMTP. After installing it, configure it under Stores > Configuration > Mageplaza Extensions > SMTP:
- Host and Port: your provider’s relay, commonly port
587with STARTTLS or465with SSL. - Authentication:
LOGIN, with the API user and key or password from the provider. - Protocol:
TLSorSSLto match the port you chose.
Then use the extension’s Send Test Email button. A green result means the credentials and TLS handshake are correct; a failure here reports the exact SMTP error (auth rejected, connection refused, TLS mismatch) before you waste time elsewhere. You can confirm the module took over transport from the CLI:
bin/magento config:show smtp/general/enabled
bin/magento config:show smtp/configuration/host
Deliverability: SPF, DKIM, DMARC, and the From address
Once SMTP works, mail can still vanish if the sending domain is not authenticated. Mailbox providers now treat unauthenticated mail as spam by default. Three DNS records on your sending domain do the heavy lifting:
- SPF: a TXT record listing the servers and providers allowed to send for the domain.
- DKIM: a public key your provider gives you, published as a TXT record, so recipients can verify the signature the provider adds to each message.
- DMARC: a policy record that tells receivers what to do when SPF or DKIM fail, and where to send reports.
Inside Magento, the sender identities must match the authenticated domain. Set them under Stores > Configuration > General > Store Email Addresses, or read them on the CLI:
bin/magento config:show trans_email/ident_general/email
bin/magento config:show trans_email/ident_sales/email
Use an address on your own domain, for example orders@yourstore.com. Never send from a free mailbox like @gmail.com or @yahoo.com: their DMARC policies reject mail sent by a third party on their behalf, so those order emails bounce outright. Finally, open your provider dashboard. Bounces, blocks, and complaints logged there tell you whether the provider accepted the message and exactly what the recipient server said back.
Per-type toggles and broken templates
Every transactional email type has its own on/off switch, and a disabled type sends nothing with no error at all. Check them under Stores > Configuration > Sales > Sales Emails (order, invoice, shipment, credit memo) and under Customers > Customer Configuration for the welcome email. The config paths are worth knowing:
bin/magento config:show sales_email/order/enabled
bin/magento config:show sales_email/invoice/enabled
bin/magento config:show sales_email/shipment/enabled
The second silent killer is a custom email template with a bad directive. Magento email templates use a restricted directive syntax, and an unbalanced or non-whitelisted directive throws an exception during rendering, so the send aborts before the message is even built. Common offenders are a stray or nested {{depend}}, a {{var}} pointing at data the email context does not expose, or a leftover comment that is not a real directive. If email broke right after someone edited a template in the admin, that template is the first suspect: revert it to the default and test again.
Adjacent or nested {{depend}} blocks and {{var}} references to variables the sender does not pass will silently kill an otherwise valid email. Pre-compute values in the sender or helper and keep the template logic flat. If you are wiring up your own notification emails from a module, the pattern in building a custom Magento module from scratch shows where the sender class and template registration belong.
Where to look: logs and the queue tables
Turn on email logging so failures are recorded instead of disappearing. Mageplaza SMTP has an Email Log that captures every attempt with its status and error message; enable it and read that grid first. At the framework level, watch the standard logs while you place a test order:
tail -f var/log/system.log var/log/exception.log var/log/debug.log
A template directive error shows up in exception.log, and an SMTP auth or connection failure shows up wherever your extension logs. For queued mail that looks stuck, inspect the message queue tables directly:
-- generic message queue backlog
SELECT COUNT(*) AS queued FROM queue_message;
SELECT status, COUNT(*) FROM queue_message_status GROUP BY status;
-- bulk and async operation records; read the recent result_message values
SELECT id, status, result_message FROM magento_operation
ORDER BY id DESC LIMIT 20;
Rows piling up in queue and queue_message with nothing draining them, or failing magento_operation records, both point straight back to a consumer or cron that is not running, the same root cause as the first section.
The quick decision list
Run down this list top to bottom and stop at the first “no.”
- Is cron alive?
crontab -l | grep magentoand a freshvar/log/cron.log. No cron means no queued mail, ever. - Are consumers running?
bin/magento queue:consumers:listplus a running process (or cron’s consumer runner) for background work. - What is the async setting?
config:show sales_email/general/async_sending, and test with it off. - Is an SMTP extension configured? Core has none, so send a test email from the extension.
- Sender identity plus SPF, DKIM, DMARC? An own-domain From address, with all three DNS records passing.
- Does the template compile? No broken
{{depend}}or{{var}}; revert custom templates to test. - Is the email type enabled?
sales_email/*/enabledand the welcome-email toggle.
Frequently asked questions
Why are my Magento order confirmation emails not sending?
On Magento 2.3 and later the usual cause is asynchronous sending plus a dead cron. With sales_email/general/async_sending enabled, order emails are queued and only sent when the sales_send_all_emails cron job runs. If cron is not running, nothing sends. Confirm cron is alive, run bin/magento cron:run, and only then look at SMTP.
How do I test whether Magento can send email at all?
Turn async off so the send happens during the request: bin/magento config:set sales_email/general/async_sending 0, then bin/magento cache:flush, then place a test order. If the mail arrives with async off but not on, the problem is cron or the consumer. If it still fails, the problem is your mail transport (SMTP).
Does Magento 2 have built-in SMTP?
No. Core Magento hands mail to PHP’s mail() and the server sendmail binary, with no host, port, or password fields. To send reliably you install an SMTP extension such as Mageplaza SMTP and point it at a transactional email provider, then use its Send Test Email button to verify.
Why do my Magento emails go to spam?
Almost always missing domain authentication. Publish SPF, DKIM, and DMARC records for your sending domain, and set trans_email/ident_* to an address on that same domain. Sending from a free @gmail.com or @yahoo.com address triggers DMARC rejection, so use your own domain and check your provider dashboard for bounces and blocks.
How do I turn off asynchronous email sending in Magento 2?
Run bin/magento config:set sales_email/general/async_sending 0 then bin/magento cache:flush. This is useful for testing because emails then send during checkout instead of waiting for cron. Turn it back on with the same command and a value of 1 for production, since synchronous sending slows checkout under load.
Which cron job sends Magento sales emails?
The sales_send_all_emails cron job in the Magento_Sales module. It queries sales_order, invoice, shipment, and credit memo grids for rows flagged send_email = 1 with email_sent still empty, and sends each one. If cron is not running, that job never executes and queued mail sits forever.
Why did my welcome email stop working after I edited the template?
A custom email template with a broken directive throws during rendering, so the send aborts silently. The usual causes are a stray or nested {{depend}}, or a {{var}} that references data the email does not pass. Check var/log/exception.log, revert the template to default to confirm, then rebuild it with flat, whitelisted directives.
Where are stuck Magento emails stored in the database?
Queued sales emails live as flags on the order and related grids (send_email = 1, email_sent = NULL). Broader background work sits in the message queue tables queue, queue_message, and queue_message_status, with bulk operations tracked in magento_operation. Backlog in those tables usually means a consumer or cron is not running.
Emails still not arriving? I trace Magento 2 transactional email end to end, from cron and consumers to SMTP and DNS authentication, and get order, invoice, and welcome mail delivering again, starting with a fixed-fee $499 audit. See services or hire me.
Get a Magento developer on it