Your password-reset emails worked in the demo. You flipped to production, pointed at a real customer base, and now the support queue fills with “I never got the code.” Some users find the mail in Junk; others never see it; a handful bounce back with a cryptic 550 5.7.1. Nothing in your code changed. Azure Communication Services (ACS) Email — the managed transactional-email service that sends mail from your own domain over a REST API or SDK without an SMTP server — reports every send as Succeeded, yet the mail isn’t arriving. This is the most maddening ACS Email incident, because “the API returned 200” and “the human got the email” are completely different claims, separated by a gauntlet of receiver-side spam filters, authentication checks and reputation gates that ACS cannot see and your logs do not mention.
This is the diagnostic playbook for that gauntlet. Deliverability is not one bug; it is a chain — sender domain → SPF → DKIM → DMARC alignment → reputation → recipient filter → bounce/suppression — and a failure at any link drops your mail into spam while the send still “succeeds.” You will learn to read that chain hop by hop, using the artefacts that tell the truth: the domain verification records in your DNS zone, the Authentication-Results header in a received message, the delivery status from the SDK, the suppression list, and the Email Service diagnostic logs in Azure Monitor. Every diagnosis comes with the exact place to confirm it — a dig query, an az communication command, a header, a KQL query — and the precise fix, with az CLI and Bicep where it applies.
By the end you will stop guessing. When the “I didn’t get the email” tickets arrive you will know within minutes whether you face an unverified domain, a missing DKIM selector, an SPF record that doesn’t authorise ACS, a DMARC policy quarantining you on an alignment failure, a suppressed recipient from an earlier bounce, a managed-domain reputation problem, or simply a body that trips a spam filter. Knowing which is the difference between a one-line DNS fix and a week of blaming “Outlook being weird.”
What problem this solves
ACS Email gives you a transactional-mail pipeline you call with one SDK method — no SMTP relay to babysit. That abstraction is a gift until a message goes missing, then an opaque wall. The send API is deliberately optimistic: it tells you ACS accepted the message, not that a mailbox accepted it into the inbox. Those are wildly different events, and everything interesting — authentication, reputation, content scoring, the recipient’s filter — happens after the API returns, outside anything ACS reports in real time.
What breaks without this knowledge: an engineer re-sends three times (it “succeeds” three times), tells the user to “check spam,” or blames the recipient’s provider and ships anyway. Meanwhile the actual cause sits there, perfectly diagnosable: a DKIM CNAME never added to DNS, an SPF record listing every vendor except ACS, a forgotten p=reject DMARC policy, or a recipient who hard-bounced last week and is now on the suppression list so ACS silently drops every further send.
Who hits this: every team that migrates from a sandbox managed domain to a custom domain and skips a DNS record; every team with an existing DMARC policy that doesn’t know ACS has to align; every product sending to consumer mailboxes (Gmail and Yahoo’s 2024 rules made SPF, DKIM and DMARC effectively mandatory); and anyone who reads “the API succeeded” as “delivered.” The fix is almost never “re-send” — it’s “find the failing link and make it pass.” Every failure class, the question it forces, and the first place to look:
| Failure class | What’s actually happening | First question to ask | First place to look | Most common single cause |
|---|---|---|---|---|
| Domain not verified | ACS rejects the send or won’t let you connect the domain | Is the domain Verified in ACS? |
az communication email domain show → verificationStates |
Verification TXT/CNAME records never added |
| Lands in spam (not bounced) | Delivered, but filtered to Junk | Does the received header show DKIM/DMARC pass? | Authentication-Results header in a test message |
Missing/broken DKIM or SPF |
| DMARC quarantine/reject | Receiver drops or junks on policy | Is your DMARC policy enforcing, and does ACS align? | dig TXT _dmarc.<domain> + the header |
p=reject/quarantine with no aligned DKIM |
| Hard bounce | Recipient address rejected by their server | What’s the SMTP enhanced status code? | Send-status / Azure Monitor deliveryStatusDetails |
Bad address (550 5.1.1) |
| Recipient suppressed | ACS silently drops the send | Is this address on the domain suppression list? | Suppression-list API / Monitor logs | Earlier hard bounce or complaint |
| Throttled / over quota | 429 or queued, mail delayed | Did you exceed the send-rate or size limit? | API 429 / quota in the portal |
Burst above the per-domain rate limit |
Learning objectives
By the end of this article you can:
- Map any “email didn’t arrive” report to a specific link in the deliverability chain — domain, SPF, DKIM, DMARC alignment, reputation, recipient filter, or suppression — and name the likely root cause.
- Connect a custom domain to ACS Email end to end: the domain-verification TXT, the SPF TXT, the two DKIM selector CNAMEs and a DMARC record, confirmed from both DNS and ACS.
- Read the Authentication-Results header of a received message to tell at a glance whether SPF, DKIM and DMARC each passed, and which one is dropping you into spam.
- Distinguish an Azure-managed domain (instant, shared-reputation, no custom From) from a custom domain (your brand and reputation) and pick the right one per environment.
- Diagnose a hard bounce versus a soft bounce versus a suppression from the SMTP enhanced status code and ACS delivery status, and confirm with exact commands.
- Drive the core tools fluently:
az communication email,digfor DNS, the message header, the SDK delivery status, the suppression list, and ACS Email Service diagnostic logs in Log Analytics (KQL). - Apply the Gmail/Yahoo bulk-sender requirements (authenticated mail, DMARC, one-click unsubscribe, low complaint rate) so consumer mailboxes accept your mail.
Prerequisites & where this fits
You should already understand the ACS basics: an Azure Communication Services resource is the parent that exposes channels (Email, SMS, Chat, Calling), and Email Communication Services is the companion resource that owns email domains; you connect a domain to the parent, then send with a connection string or Entra ID. You should know how to run az in Cloud Shell, read JSON output, and edit DNS records in whatever zone hosts your domain (Azure DNS or an external registrar). A high-level grasp of email authentication — receivers check who claims to send against who is allowed to send — helps, but this article builds it from the ground up.
This sits in the Observability & Troubleshooting track, on the messaging side. It assumes you know your way around DNS records (the How to Host Public DNS on Azure DNS: Zone Delegation, Alias Records and Apex Domains deep-dive is the upstream skill — every fix here is a DNS record). For the observability half it pairs with Azure Monitor and Application Insights: Full-Stack Observability, because the ACS Email Service diagnostic logs land in Log Analytics and KQL is how you read delivery status at scale; if those logs are empty, No Logs Showing Up? Troubleshooting Empty Log Analytics Tables and Ingestion Gaps is the companion. If you secure the connection string in Key Vault (you should), Azure Key Vault: Secrets, Keys and Certificates Done Right covers that, and Managed Identities Demystified: System vs User-Assigned and When to Use Each covers sending without a connection string at all.
Responsibility splits across teams: the app/platform team owns the ACS resource and suppression list; the DNS/network team owns the SPF/DKIM/DMARC records; the app/dev team owns the From address and content; and the recipient’s provider — outside Azure entirely — owns the spam filter, reputation gates and bounces.
Core concepts
Five mental models make every later diagnosis obvious.
“Accepted” is not “delivered to the inbox.” The send API returns a status meaning ACS queued your message — it passed validation and the domain is connected. It says nothing about whether the receiver authenticated it, scored its content, checked your reputation, and chose Inbox vs Junk vs reject. Deliverability lives entirely in that gap: Succeeded is the start of delivery, and the truth arrives later as a delivery status (Delivered / Bounced / Suppressed / Failed) in the operation result and diagnostic logs.
Three authentication mechanisms answer one question: “is this sender allowed?” On mail claiming to be from @yourbrand.com, a receiver runs three checks. SPF asks “is the sending IP authorised in yourbrand.com’s SPF record?”; DKIM asks “is the message signed by a key published in yourbrand.com’s DNS, and is the signature intact?”; DMARC asks “did SPF or DKIM pass and align with the From, and if not, what should I do?” SPF authorises an IP, DKIM authorises a message, DMARC ties them to the From. A clean pass on all three is most of the difference between Inbox and Junk.
Alignment is the trap inside DMARC. SPF and DKIM can pass while DMARC still fails, because DMARC additionally requires the passing domain to align with the visible From: domain. SPF aligns on the MailFrom / Return-Path (the envelope sender ACS sets); DKIM aligns on the d= tag in the signature. If ACS signs with a d= of your custom domain and you publish its DKIM keys, DKIM aligns and DMARC passes — which is why DKIM is the load-bearing record for ACS. “SPF passed but DMARC failed” almost always means SPF authorised a server on a domain that doesn’t match your From — alignment, not authorisation, is the failing half.
The domain comes in two flavours with opposite trade-offs. An Azure-managed domain (<guid>.azurecomm.net) is instant with pre-configured SPF/DKIM but shares reputation and can’t brand the From; a custom domain (mail.yourbrand.com) is yours — you publish SPF/DKIM/DMARC, send from your brand, and build your own reputation. Managed = fast and throwaway; custom = production and yours.
Reputation is earned, shared or borrowed — and it decays. Mailbox providers score the sending identity (IP and domain) on history: volume consistency, complaint rate, bounce rate, spam-trap hits. A brand-new custom domain has no reputation and must be warmed up or a sudden blast looks suspicious; a managed domain borrows shared reputation you don’t control. A hard bounce or complaint damages reputation and lands the recipient on your suppression list. Reputation is the invisible gate behind every “it’s in spam for some users but not others.”
Those terms — the two domain types, SPF/DKIM/DMARC, the MailFrom, the suppression list, the delivery status, and reputation/warm-up — are the whole vocabulary; the glossary defines each, and the sections below take them one at a time.
Email authentication, end to end (SPF, DKIM, DMARC)
The three records aren’t interchangeable — each answers a different question and fails its own way:
| Mechanism | The question it answers | Record type & location | What aligns with the From | Failure symptom |
|---|---|---|---|---|
| SPF | Is the sending IP authorised by the domain? | TXT at the domain (or MailFrom subdomain) |
MailFrom / Return-Path | spf=fail/softfail; lowers trust, rarely junks alone |
| DKIM | Is the message signed by a key the domain published? | Two CNAME selectors → ACS-hosted keys |
The d= tag in the signature |
dkim=fail/none; major Junk driver; breaks DMARC |
| DMARC | Did SPF or DKIM pass and align? What to do if not? | TXT at _dmarc.<domain> |
The visible From: domain |
dmarc=fail + p=quarantine/reject → Junk or drop |
SPF — authorising the sending server
SPF is a single TXT record listing the servers allowed to send for a domain; the receiver checks the connecting IP of the MailFrom domain against it and records spf=pass/softfail/fail. SPF alone rarely junks you, but a fail lowers trust and it is one of the two ways DMARC can pass. For ACS you publish a record that includes the ACS infrastructure via the include: value ACS gives you. The cardinal rules: exactly one SPF record per domain (two is a permerror), stay within the 10-lookup limit, and end with ~all or -all.
Confirm what’s published with dig +short TXT mail.yourbrand.com | grep spf1 (expect one v=spf1 include:... ~all line). In Bicep this is a Microsoft.Network/dnsZones/TXT resource on the MailFrom host with that single value (the DKIM Bicep below shows the record pattern).
The SPF values and the traps that quietly break them:
| SPF element | Meaning | Use it when | Trap / limit |
|---|---|---|---|
v=spf1 |
Marks the record as SPF v1 | Always, first token | Two SPF TXT records = permerror, SPF ignored |
include:<host> |
Authorise another domain’s senders | Add the ACS include | Each include counts toward the 10-lookup cap |
ip4: / a / mx |
Authorise literal IPs / your A/MX hosts | Static or self-hosted senders | Each costs a DNS lookup; brittle |
~all |
Softfail everything else | Most production (safer rollout) | Receivers may still accept softfail |
-all |
Hardfail everything else | Locked-down senders | Risky if any legit sender is unlisted |
+all |
Pass-all | Never | Authorises the world — never use |
DKIM — signing the message (the record that matters most)
DKIM is the workhorse for ACS because it is what makes DMARC align. ACS signs each message with a DKIM-Signature header carrying a selector and the signing domain (d=); the receiver fetches the public key from <selector>._domainkey.<domain> and verifies it. For a custom domain ACS hosts the keys and gives you two CNAME records — selector1-azurecomm-prod-net._domainkey and selector2-azurecomm-prod-net._domainkey (two selectors for key rotation). If either CNAME is missing, signing fails (dkim=none), DMARC can’t align via DKIM, and your mail gets junked — the number-one cause of ACS Email landing in spam after a custom-domain migration.
Confirm both resolve with dig +short CNAME selector1-azurecomm-prod-net._domainkey.yourbrand.com (and selector2-...) — each must return an azurecomm-prod-net target; empty means the record is missing. In Bicep:
// The two DKIM selector CNAMEs (targets come from ACS domain config — do not invent them)
resource dkim1 'Microsoft.Network/dnsZones/CNAME@2023-07-01-preview' = {
parent: zone
name: 'selector1-azurecomm-prod-net._domainkey'
properties: { TTL: 3600, CNAMERecord: { cname: '<acs-selector1-target>' } }
}
resource dkim2 'Microsoft.Network/dnsZones/CNAME@2023-07-01-preview' = {
parent: zone
name: 'selector2-azurecomm-prod-net._domainkey'
properties: { TTL: 3600, CNAMERecord: { cname: '<acs-selector2-target>' } }
}
The DKIM moving parts and where each one breaks:
| DKIM element | What it is | Where it lives | Failure if wrong |
|---|---|---|---|
| Selector + public key | The CNAME naming which ACS-hosted key signs (e.g. selector1-azurecomm-prod-net) |
<selector>._domainkey.<domain> CNAME |
Missing CNAME → no key → dkim=none |
d= tag |
The signing domain in the signature header | The DKIM-Signature header |
Doesn’t match From → no align → DMARC fail |
| Two selectors | selector1 + selector2 for key rotation | Two CNAME records | Only one added → rotation breaks signing later |
b= / bh= |
The signature and body hash | In the header (set by ACS) | Body modified in transit → dkim=fail |
DMARC — the policy that decides your fate
DMARC is a TXT record at _dmarc.<domain> that requires alignment (a passing SPF or DKIM whose domain matches the visible From) and states a policy for failures — none (monitor), quarantine (Junk), or reject (refuse). It is why a signed message from the wrong domain still fails, and why an enforcing policy you forgot can quarantine your new ACS mail on day one. Gmail and Yahoo now effectively require it for bulk senders (p=none minimum), and an aligned DMARC pass is your strongest trust signal — but turning it to quarantine/reject before DKIM aligns buries your own mail.
Read the policy in effect with dig +short TXT _dmarc.yourbrand.com. In Bicep, start at p=none:
// Start at p=none to monitor, then tighten once DKIM aligns
resource dmarc 'Microsoft.Network/dnsZones/TXT@2023-07-01-preview' = {
parent: zone
name: '_dmarc'
properties: {
TTL: 3600
TXTRecords: [
{ value: [ 'v=DMARC1; p=none; rua=mailto:dmarc-reports@yourbrand.com; adkim=r; aspf=r' ] }
]
}
}
The DMARC tags you set, with the recommended rollout value:
| DMARC tag | Meaning | Values | Recommended (ACS rollout) |
|---|---|---|---|
v |
Version | DMARC1 |
DMARC1 (required, first) |
p |
Policy for the domain | none / quarantine / reject |
none first → quarantine → reject |
rua |
Where to send aggregate reports | mailto: |
Set it — reports show alignment results |
pct |
% of mail the policy applies to | 1–100 | Ramp 25→100 when tightening |
adkim / aspf |
DKIM / SPF alignment strictness | r (relaxed) / s (strict) |
r unless you control MailFrom exactly |
The cardinal rule: roll DMARC out in stages — p=none with rua reporting first, confirm aligned-DKIM passes from the reports and a test send, then quarantine, finally reject. Jumping straight to reject before DKIM is verified is a self-inflicted outage.
Reading the Authentication-Results header
The fastest deliverability diagnosis costs nothing: send a test to an address you control, open the raw source, and read the Authentication-Results header. In one line it tells you whether SPF, DKIM and DMARC each passed — turning “is it an auth problem?” from a guess into a fact.
Authentication-Results: mx.google.com;
dkim=pass header.i=@yourbrand.com header.s=selector1-azurecomm-prod-net;
spf=pass (google.com: domain of bounces@mail.yourbrand.com ...) smtp.mailfrom=mail.yourbrand.com;
dmarc=pass (p=NONE sp=NONE dis=NONE) header.from=yourbrand.com
How to read every outcome you’ll see in that header:
| Header token | Meaning | What it tells you | If it’s not pass |
|---|---|---|---|
dkim=pass |
Signature verified | The DKIM CNAMEs are correct and signing works | none = CNAME missing; fail = key/body mismatch |
spf=pass |
Sending IP authorised | SPF TXT includes ACS and MailFrom resolves | softfail/fail = SPF missing the ACS include |
dmarc=pass |
SPF or DKIM passed and aligned | Alignment is working; trust is high | fail = nothing aligned with the From domain |
So dkim=pass with dmarc=fail is an alignment problem (DKIM’s d= doesn’t match the From); spf=pass with dmarc=fail means the MailFrom is an ACS host (SPF aligns to that, not your brand) and DKIM isn’t carrying alignment — exactly why DKIM must be configured for custom domains.
Choosing and verifying your domain
Every send starts from a domain, and that choice fixes your reputation, your From address, and how much DNS work you do.
Azure-managed vs custom domain
The decision is about whose reputation and which environment: managed gets a test send in minutes from a *.azurecomm.net address; custom gets your brand and your own reputation at the cost of DNS work:
| Dimension | Azure-managed domain | Custom domain |
|---|---|---|
| Setup time | Minutes (no DNS work) | Hours–days (DNS + propagation + verify) |
| From address | Fixed *.azurecomm.net subdomain |
Your brand (no-reply@mail.yourbrand.com) |
| SPF / DKIM | Pre-configured by ACS | You publish them in your zone |
| Reputation | Shared across tenants | Yours alone (must warm up) |
| Send limits | Low, test-grade per-day caps | Higher, raisable via support |
| Best for | Dev/test, demos, quick spikes | Production, anything customer-facing |
| Brand trust | Low (generic domain) | High (your domain) |
| Risk | Another tenant can taint shared reputation | You own every reputation signal |
The rule: managed for non-production, custom for anything a customer sees — the From address alone screams “test”, and the shared reputation is outside your control.
Verifying a custom domain
Connecting a custom domain is two gates: prove ownership, then prove you authorised ACS to send (SPF + DKIM). ACS gives you the exact records (... domain show --query verificationRecords); you add them, then ask ACS to check each:
# Create the domain, read the records to add, then verify each type
az communication email domain create --domain-name mail.yourbrand.com \
--email-service-name acs-email-prod -g rg-mail-prod \
--location global --domain-management CustomerManaged
az communication email domain show --domain-name mail.yourbrand.com \
--email-service-name acs-email-prod -g rg-mail-prod --query "verificationRecords" -o json
# After adding the records in DNS, verify each type (Domain, SPF, DKIM, DKIM2):
az communication email domain initiate-verification --domain-name mail.yourbrand.com \
--email-service-name acs-email-prod -g rg-mail-prod --verification-type Domain
ACS issues four records: the Domain TXT (proves ownership — without it you can’t connect the domain at all), the SPF TXT (authorises ACS to send), and the two DKIM CNAMEs (selector1-... and selector2-..., publishing the signing keys — without them dkim=none and DMARC can’t align). After all four show Verified, connect the domain to the parent Communication Services resource so sends from it are authorised. Verification waits on DNS propagation — add the records, wait out the TTL, then initiate verification; initiating before propagation is the most common “it won’t verify” self-own.
Sender addresses and MailFrom
Once a domain is verified you add one or more sender usernames (the part before the @, e.g. no-reply), making the From no-reply@mail.yourbrand.com. The MailFrom (Return-Path) is separate — the envelope sender ACS uses for bounces and SPF, an ACS-managed address on your domain by default. Keep them distinct: the From is what the human sees and what DMARC evaluates; the MailFrom is where bounces go and what SPF is checked against.
# Add a sender username to a verified domain
az communication email domain sender-username create \
--domain-name mail.yourbrand.com --email-service-name acs-email-prod \
--resource-group rg-mail-prod --sender-username no-reply \
--username no-reply --display-name "YourBrand (no-reply)"
Sending mail and reading the real result
The send call is where engineers form the wrong belief that “it worked.” Here is how to get the delivery truth.
The send operation and its statuses
You send with the Email SDK (or REST). The call is long-running — you poll an operation through statuses — and the status that matters is the message’s delivery status, not “the call returned”.
// .NET: send and WAIT for the result, don't fire-and-forget
var client = new EmailClient(connectionString);
var message = new EmailMessage(
senderAddress: "no-reply@mail.yourbrand.com",
recipientAddress: "user@example.com",
content: new EmailContent("Your code") { PlainText = "Your code is 123456" });
EmailSendOperation op = await client.SendAsync(WaitUntil.Completed, message);
Console.WriteLine($"Status: {op.Value.Status}, OperationId: {op.Id}"); // correlate Id in Monitor
The send-status values and what they do not mean:
EmailSendStatus |
Meaning | What it does NOT mean | Your next move |
|---|---|---|---|
NotStarted |
Queued, not yet processing | Anything about delivery | Keep polling |
Running |
ACS is processing the send | That a mailbox accepted it | Keep polling |
Succeeded |
ACS accepted & dispatched the message | That it reached the inbox | Check delivery status / Monitor logs |
Failed |
ACS could not send it | That the address is bad (could be config) | Read the error code below |
Canceled |
The operation was canceled | A recipient action | Re-issue if intended |
The single most important sentence in this article: Succeeded means accepted, not delivered. Delivered vs Bounced vs Suppressed lives in the delivery-status detail — per-recipient in the operation result, and at scale in the Azure Monitor logs (below).
Send-time error codes (the call itself fails)
When the call fails, the error code tells you whether it’s your config, your input, or a limit:
| HTTP / code | Meaning | Likely cause | Fix |
|---|---|---|---|
401 Unauthorized |
Auth to ACS failed | Bad/rotated connection string or key | Refresh the connection string / Entra token |
403 Forbidden (DomainNotLinked) |
The From domain isn’t connected | Domain not linked to the ACS resource, or not verified | Connect & verify the domain |
400 InvalidSenderDomain |
Sender address not allowed | From username/domain not provisioned | Add the sender username; use a verified domain |
400 (Invalid recipient/content) |
Malformed request | Bad email address, missing content | Validate inputs before send |
429 TooManyRequests |
Rate/size limit hit | Burst over the per-domain send rate | Back off + retry with the Retry-After header |
413 / 500 / 503 |
Payload too large / transient error | Oversized attachments, or an ACS-side blip | Shrink/host attachments; retry with backoff |
Bounces, soft bounces and the delivery status
A message ACS accepted can still come back as a delivery status with an SMTP enhanced status code: Delivered (2.x.y); Bounced hard (5.x.y, permanent — adds the recipient to your suppression list); Suppressed (dropped because already suppressed); FilteredSpam/Quarantined (the receiver’s filter or DMARC held it — a reputation/auth fix, not a retry); and Failed soft (4.x.y, temporary like a full mailbox — retriable). The SMTP codes you’ll meet most:
| Enhanced code | Class | Typical meaning | What to do |
|---|---|---|---|
5.1.1 / 5.1.10 |
Permanent | Recipient address does not exist (5.1.10 = M365 variant) |
Drop the address; fix data quality |
5.7.1 |
Permanent | Rejected by policy (auth/reputation/blocklist) | Fix SPF/DKIM/DMARC; check blocklists |
5.7.26 |
Permanent | Failed auth (SPF/DKIM/DMARC) at receiver | Fix authentication — the classic deliverability bug |
5.4.1 |
Permanent | Recipient server not accepting (no answer) | Verify the recipient domain/MX |
4.2.2 / 5.2.2 |
Temp / Perm | Mailbox full / over quota | Retry later; if persistent, stop |
4.7.x |
Temporary | Greylisting / temporary policy defer | Retry after a delay (greylisting clears) |
Diagnosing deliverability with Azure Monitor
At scale you don’t poll individual operations — you read the Email Service diagnostic logs ACS emits to Log Analytics, where every send, delivery and engagement event is a queryable row. This is how you answer “what’s our bounce rate?” and “which recipients are suppressed?” across millions of messages. The logs are off by default; route them to a workspace via a diagnostic setting on the Communication Services resource:
# Send ACS Email diagnostic logs to Log Analytics
az monitor diagnostic-settings create \
--name acs-email-diag \
--resource $(az communication show -n acs-comms-prod -g rg-mail-prod --query id -o tsv) \
--workspace $(az monitor log-analytics workspace show -g rg-mail-prod -n law-mail --query id -o tsv) \
--logs '[{"category":"EmailSendMailOperational","enabled":true},
{"category":"EmailStatusUpdateOperational","enabled":true},
{"category":"EmailUserEngagementOperational","enabled":true}]'
The ACS Email log categories and what each answers:
| Log category | What it records | Question it answers |
|---|---|---|
EmailSendMailOperational |
Each send request (sender, message id, size) | “Did we attempt the send, and with what From?” |
EmailStatusUpdateOperational |
Delivery status updates (Delivered/Bounced/Suppressed) | “What actually happened to the message?” |
EmailUserEngagementOperational |
Opens/clicks (when tracking is on) | “Did the recipient engage?” |
The KQL you’ll actually run
// Delivery-status breakdown for the last 24h — your bounce/suppression dashboard
ACSEmailStatusUpdateOperational
| where TimeGenerated > ago(24h)
| summarize count() by DeliveryStatus
| order by count_ desc
// Which recipients/domains are hard-bouncing? (data-quality + reputation signal)
ACSEmailStatusUpdateOperational
| where TimeGenerated > ago(7d) and DeliveryStatus == "Bounced"
| extend recipientDomain = tostring(split(RecipientId, "@")[1])
| summarize bounces = count() by recipientDomain
| order by bounces desc
// Correlate a single message from send to final status by MessageId
ACSEmailSendMailOperational
| where TimeGenerated > ago(1d)
| join kind=leftouter (ACSEmailStatusUpdateOperational) on MessageId
| project TimeGenerated, Sender, MessageId, Size, DeliveryStatus, DeliveryStatusDetails
Those three patterns — group by DeliveryStatus, break bounces down by recipient domain, and join send-to-status by MessageId — answer almost every operational question (delivery mix, suppression rate, failure spikes via bin(TimeGenerated, 5m), complaint signal from the engagement table). If these tables are empty, the diagnostic setting isn’t routing or the workspace is wrong — that ingestion gap is covered in No Logs Showing Up? Troubleshooting Empty Log Analytics Tables and Ingestion Gaps.
The suppression list
ACS keeps a suppression list per email domain: addresses that hard-bounced or complained, which ACS silently refuses to send to (the send Succeeds, the status is Suppressed). This protects your reputation — hammering a dead address is what gets a sender blocklisted — but it surprises engineers who “re-sent and it still didn’t arrive.” Review it, and only remove an address you’ve confirmed is corrected.
# List suppressed addresses for a domain (via the Domains suppression-list management)
az communication email domain suppression-list list \
--domain-name mail.yourbrand.com --email-service-name acs-email-prod \
--resource-group rg-mail-prod -o table
Architecture at a glance
Hold the whole system as a left-to-right pipeline and every diagnosis becomes “which stage is failing?” Your application calls the ACS Email send API; ACS validates that the domain is connected and verified, signs the message with your DKIM key (a d=yourbrand.com signature), sets the MailFrom/Return-Path for bounces, and dispatches it — at which point the call returns Succeeded. That is the last event ACS fully controls.
From there the message crosses to the recipient’s mail provider, which runs the gauntlet your logs can’t see: it looks up your SPF record, verifies your DKIM signature (and whether d= aligns with the From), evaluates DMARC, scores your reputation, and runs content filtering. The verdict — Inbox, Junk, quarantine, or reject — is decided here, entirely outside Azure. The outcome then flows back as a delivery status into your Azure Monitor logs and, on a hard bounce or complaint, onto your suppression list.
The mental model: ACS owns the left half (domain, signing, dispatch), which you fix with DNS and config; the right half (auth checks, reputation, filtering) lives at the receiver, influenced only through what you publish (SPF/DKIM/DMARC) and how you behave (volume, complaints, list hygiene). Every “it’s in spam” question reduces to: did the message pass the three auth checks with alignment, and is your reputation good enough? The diagnostic move is always the same — read the Authentication-Results header on a test send to localise the failing check, then fix the corresponding DNS record.
Real-world scenario
Finlyte, a fintech startup in Bengaluru, runs onboarding and OTP delivery through ACS Email. In the sandbox they used an Azure-managed domain — instant, OTPs in seconds — and shipped. Two weeks after launch on their custom domain mail.finlyte.in, the support inbox filled with “I never received my OTP”: roughly 40% of sign-ups stalled at verification.
The first reflex was to re-send (it “succeeded” every time); the second was to blame Gmail. The breakthrough cost nothing: an engineer sent a test OTP to her own Gmail, opened Show original, and read spf=pass, dkim=none, dmarc=fail. SPF passed because the MailFrom resolved to an ACS host — but DKIM was none, so DMARC had nothing aligned with finlyte.in, and Finlyte had set p=quarantine months earlier for a different sender. Gmail was doing exactly what the policy told it to: quarantine unaligned mail to Junk.
The root cause was a single missing DNS record. During migration the team had added the domain-verification TXT and SPF TXT, ACS flipped those to Verified, and they declared victory — but they had never added the two DKIM selector CNAMEs. az communication email domain show --query verificationStates confirmed it: DKIM: NotVerified, DKIM2: NotVerified. No DKIM → no signature → DMARC couldn’t align on finlyte.in → with p=quarantine, Gmail junked it. The 40% had it sitting in Junk; a stricter slice was outright quarantined.
The fix had two parts. Immediately: add both DKIM CNAMEs, wait the TTL, run initiate-verification --verification-type DKIM / DKIM2. Within an hour both showed Verified, a fresh test showed dkim=pass and dmarc=pass, and OTPs reached the inbox. The next week they pulled ACSEmailStatusUpdateOperational, found 6% Bounced with 5.1.1 (typos from a broken form) and 3% Suppressed, fixed signup validation, cleaned the list, and only then walked DMARC toward p=reject. Delivered rate went from ~60% to 98.5%. The lesson on the wall: “A ‘Succeeded’ send that doesn’t arrive is a DKIM question. Read the header before you blame the receiver.”
The incident as a timeline — the order of moves is the lesson:
| Time | Symptom | Action taken | Effect | What it should have been |
|---|---|---|---|---|
| Day 0 | OTPs missing for ~40% | Re-send the OTP | “Succeeds”, still missing | Read the received header first |
| +1 day | Still missing | Blame Gmail’s filter | No progress | Don’t blame the receiver blind |
| +2 days | Breakthrough | Read Authentication-Results on a test |
dkim=none; dmarc=fail |
This was the diagnosis |
| +2 days | Root cause | ... --query verificationStates |
DKIM/DKIM2 NotVerified |
One missing record set |
| +2 days | Fixed (auth) | Add DKIM CNAMEs, verify | dkim=pass; dmarc=pass; inbox |
The actual fix is DNS |
| +1 week | Hardening | KQL bounce audit; fix signup; tighten DMARC | 60% → 98.5% delivered | List hygiene + staged DMARC |
Advantages and disadvantages
The managed-pipeline model both causes this class of problem and gives you the tools to diagnose it. Weigh it honestly:
| Advantages (why this model helps you) | Disadvantages (why it bites) |
|---|---|
| No SMTP server to run — send with one SDK call, scale handled for you | “Succeeded” only means accepted; delivery truth arrives later and elsewhere |
| Azure-managed domains let you send a test mail in minutes | Managed domains share reputation and can’t carry your brand — useless for prod |
| DKIM keys are hosted and rotated by ACS — you just publish two CNAMEs | If you skip those CNAMEs, mail silently junks with no error on send |
| Delivery status + Azure Monitor logs give per-message and fleet-wide truth | Logs are off by default; without them you’re diagnosing blind |
| Suppression list protects your reputation automatically | It silently drops sends, surprising anyone who “just re-sends” |
| Standard SPF/DKIM/DMARC + SMTP bounce codes — portable knowledge | DMARC alignment is subtle (an old p=reject buries day-one mail), and the real cause lives at the receiver, which ACS can’t fully see |
The model is right for transactional mail — OTPs, receipts, resets, notifications — where you want to ship features, not run a mail server. It bites hardest on teams that miss a DNS record on migration, products with an unreconciled DMARC policy, and anyone who reads “Succeeded” as “delivered.” The disadvantages are all manageable — but only if you know the chain exists.
Hands-on lab
Verify a custom domain, then prove the deliverability chain by sending a test message and reading its authentication header — the single most useful skill here. You need a domain you control and an ACS resource. Run in Cloud Shell (Bash).
Step 1 — Variables and resource group.
RG=rg-acs-email-lab
LOC=global
ES=acs-email-lab
COMMS=acs-comms-lab
DOMAIN=mail.yourbrand.com # a domain whose DNS you can edit
az group create -n $RG -l southindia -o table
Step 2 — Create the Email + Communication Services resources.
az communication email create -n $ES -g $RG -l $LOC --data-location unitedstates -o table
az communication create -n $COMMS -g $RG -l $LOC --data-location unitedstates -o table
Expected: two resources provisioned; note the id of each.
Step 3 — Add a custom domain and read the DNS records ACS wants.
az communication email domain create --domain-name $DOMAIN --email-service-name $ES \
-g $RG -l $LOC --domain-management CustomerManaged -o table
az communication email domain show --domain-name $DOMAIN --email-service-name $ES \
-g $RG --query "verificationRecords" -o json
Expected: a JSON block listing the Domain TXT, SPF TXT and two DKIM CNAMEs — the exact values to add to DNS.
Step 4 — Add the records in your DNS zone, then verify. Add each record from Step 3, wait out the TTL, then:
for T in Domain SPF DKIM DKIM2; do
az communication email domain initiate-verification --domain-name $DOMAIN \
--email-service-name $ES -g $RG --verification-type $T
done
az communication email domain show --domain-name $DOMAIN --email-service-name $ES \
-g $RG --query "verificationStates" -o json
Expected: all four states read Verified. If DKIM is NotVerified, your CNAME hasn’t propagated — wait and re-run (this is the exact bug from the scenario).
Step 5 — Confirm the records resolve from public DNS (independent of ACS):
dig +short TXT $DOMAIN | grep spf1
dig +short CNAME selector1-azurecomm-prod-net._domainkey.$DOMAIN
dig +short CNAME selector2-azurecomm-prod-net._domainkey.$DOMAIN
dig +short TXT _dmarc.$DOMAIN # add a p=none DMARC record first
Expected: an SPF line with the ACS include, two CNAMEs resolving to azurecomm-prod-net targets, and your DMARC policy.
Step 6 — Connect the domain, add a sender, and send a test.
az communication email domain sender-username create --domain-name $DOMAIN \
--email-service-name $ES -g $RG --sender-username no-reply \
--username no-reply --display-name "Lab no-reply"
# Connect $DOMAIN to $COMMS (portal: Communication Services → Email → Connect domain),
# grab the connection string, then send to an address you own (Gmail is ideal):
az communication email send-status ... # via SDK/REST using the connection string
Step 7 — Read the Authentication-Results header (the payoff). In the received Gmail message: ⋮ → Show original. Read the line:
Authentication-Results: ... dkim=pass ... spf=pass ... dmarc=pass ...
Expected: all three pass. If dkim=none, your CNAMEs aren’t right; if dmarc=fail with dkim=pass, it’s an alignment problem.
Validation checklist. You verified a custom domain, confirmed each DNS record from both ACS (Step 4) and public DNS (Step 5), sent a real message, and read the receiver’s verdict in the header (Step 7) — the whole chain end to end. That header read is the 5-minute diagnosis you’ll reach for in every real incident.
Cleanup (avoid lingering charges).
az group delete -n $RG --yes --no-wait
ACS Email has no per-resource hourly charge (you pay per message and per MB), so the main thing to remove is the domain records from your DNS zone if you don’t keep the domain.
Common mistakes & troubleshooting
This is the playbook — the part you bookmark. First as a scannable table you read when the “I didn’t get the email” tickets land, then the same entries with full confirm-command detail underneath. It spans the basic failures (a missing DNS record) and the advanced ones (DMARC alignment, reputation, suppression).
| # | Symptom | Root cause | Confirm (exact cmd / portal path) | Fix |
|---|---|---|---|---|
| 1 | Mail lands in Junk; send “Succeeded” | DKIM not configured (CNAMEs missing) | dig CNAME selector1-azurecomm-prod-net._domainkey.<domain> empty; header dkim=none |
Add both DKIM CNAMEs; initiate-verification DKIM/DKIM2 |
| 2 | Send fails 403 DomainNotLinked |
Domain not verified or not connected to the ACS resource | az communication email domain show --query verificationStates; check the linked domain |
Verify all records; connect the domain to the Communication Services resource |
| 3 | Mail quarantined/rejected at receiver | DMARC p=quarantine/reject with nothing aligned |
dig TXT _dmarc.<domain>; header dmarc=fail |
Fix DKIM alignment first, then keep/raise policy |
| 4 | spf=pass but dmarc=fail |
MailFrom is an ACS host; SPF aligns to that, not your From; DKIM not aligning | Read smtp.mailfrom= vs header.from= in the header |
Configure DKIM so DMARC aligns via the signature |
| 5 | Some recipients never get it; no bounce visible | Recipients on the suppression list (earlier hard bounce) | az communication email domain suppression-list list; Monitor DeliveryStatus == Suppressed |
Confirm address is valid; remove from suppression list deliberately |
| 6 | Mail bounces 550 5.1.1 |
Recipient address doesn’t exist | Monitor ACSEmailStatusUpdateOperational DeliveryStatus == Bounced, code 5.1.1 |
Remove the address; fix data-entry validation |
| 7 | Bounces 5.7.26 / 5.7.1 |
Receiver rejected on authentication/reputation | Header / deliveryStatusDetails; check SPF/DKIM/DMARC all pass |
Fix the failing auth record; check blocklists |
| 8 | Domain won’t verify in ACS | Records added but DNS hasn’t propagated (or wrong host/typo) | dig the exact record; compare to verificationRecords value |
Match the host/value exactly; wait the TTL; re-initiate |
| 9 | SPF ignored / permerror; intermittent spam |
A second v=spf1 record, or more than 10 DNS lookups in the chain |
dig TXT <domain> shows two v=spf1 lines; or count the lookups |
Merge into ONE record; flatten includes under the 10-lookup cap |
| 10 | Production mail from *.azurecomm.net looks like spam |
Shipped on an Azure-managed domain | The From address is *.azurecomm.net |
Migrate to a verified custom domain |
| 11 | New custom domain mail junked despite passing auth | No sender reputation yet (cold domain) | Low/blank reputation; junked at high volume from day 1 | Warm up: ramp volume gradually; keep complaints low |
| 12 | Sends start failing 429 under load |
Exceeded the per-domain send-rate limit | 429 TooManyRequests with Retry-After |
Back off + retry; request a higher quota via support |
| 13 | DKIM was working, now dkim=fail intermittently |
Body modified in transit, or one selector removed | Header dkim=fail; confirm both selector CNAMEs still resolve |
Keep both selectors published; don’t let a relay rewrite the body |
| 14 | Gmail/Yahoo bulk mail bounces with policy errors | Missing DMARC / unsubscribe / high complaint rate | Header dmarc= absent; complaint rate high |
Publish p=none DMARC min; add one-click unsubscribe; cut complaints |
| 15 | Logs empty in Log Analytics; can’t audit delivery | Diagnostic setting not routing ACS Email categories | az monitor diagnostic-settings list on the ACS resource |
Add the three Email*Operational categories to the workspace |
The expanded form, with the full reasoning for the entries that bite hardest:
1. Mail lands in Junk while the send reports Succeeded.
Root cause: DKIM is not configured — the two selector CNAMEs were never added (the classic post-migration miss). No signature → DMARC can’t align → receivers junk it.
Confirm: dig +short CNAME selector1-azurecomm-prod-net._domainkey.<domain> returns nothing; a received message’s Authentication-Results shows dkim=none.
Fix: Add both DKIM CNAMEs from verificationRecords, wait out the TTL, and run az communication email domain initiate-verification --verification-type DKIM and DKIM2.
2. Mail is quarantined or rejected at the receiver.
Root cause: A DMARC policy of quarantine/reject is in effect while nothing aligns with the From — often an old policy the team set for a different sender.
Confirm: dig +short TXT _dmarc.<domain> shows p=quarantine/reject; the header shows dmarc=fail.
Fix: Make DKIM align first (Mistake 1), confirm dmarc=pass on a test, then keep or raise the policy. Never enforce before alignment works.
3. Some recipients never receive mail and there’s no obvious bounce.
Root cause: Those addresses are on the suppression list from an earlier hard bounce or complaint; ACS silently drops the send (status Suppressed).
Confirm: az communication email domain suppression-list list ...; or KQL ACSEmailStatusUpdateOperational | where DeliveryStatus == "Suppressed".
Fix: Confirm the address is genuinely valid now, then remove it from the suppression list deliberately — don’t blanket-clear, or you’ll re-hammer dead addresses and hurt reputation.
The remaining rows cover the rest of the field — 403/DomainNotLinked, spf=pass yet dmarc=fail, the 5.1.1/5.7.x bounces, a domain that won’t verify, the SPF traps, cold-domain junking, 429 throttling, intermittent DKIM fail, the Gmail/Yahoo bulk bar, and empty Log Analytics tables. Across all of them the method never changes: read the header to localise the failing check, confirm with the one command in the table, fix the corresponding record or setting.
Best practices
- Custom domain for anything a customer sees; managed only for tests. A managed domain’s generic From and shared reputation make it unfit for production — migrate before launch.
- Configure DKIM the moment you add the domain — both selector CNAMEs, verified, before your first production send. It is the record that makes DMARC align; skipping it is the single biggest deliverability mistake.
- Publish all three: SPF, DKIM, DMARC (Gmail and Yahoo require the trio for bulk senders), and roll DMARC out in stages —
p=none, thenquarantine, thenreject. Never enforce before DKIM aligns. - Read the
Authentication-Resultsheader on a test send before debugging anything else. It turns “is it auth?” into a fact in 30 seconds. - Turn on the Email diagnostic logs from day one. Without
ACSEmailStatusUpdateOperationalyou can’t measure bounce rate, suppression or delivery. - Treat the suppression list as a feature. Only remove addresses you’ve confirmed corrected; re-sending to hard-bounced addresses is what gets a sender blocklisted.
- Keep your list clean at the source — validate addresses and use double-opt-in so typos and fake addresses never enter it; most
5.1.1bounces are preventable at signup. - Warm up a new custom domain — ramp volume gradually so providers build trust; a cold domain blasting thousands of messages looks like a spammer.
- Keep one SPF record under ten lookups (merge vendor includes into a single
v=spf1line), add a one-clickList-Unsubscribeto bulk mail, and alert on the leading indicators — bounce rate, suppression growth anddmarc=fail— not just “users complained.”
The alerts worth wiring before the next incident — the leading indicators, not the lagging “tickets arrived”:
| Alert on | Signal (KQL source) | Threshold (starting point) | Why it’s leading |
|---|---|---|---|
| Bounce rate | DeliveryStatus == "Bounced" ÷ total |
> 2% over 1h | High bounces tank reputation fast |
| Suppression growth | new Suppressed per hour |
sustained increase | Signals a bad list or a broken form |
| Auth failures | bounces with 5.7.26/5.7.1 |
any sustained count | Direct sign SPF/DKIM/DMARC broke |
| Delivered-rate drop | Delivered ÷ total |
< your baseline (e.g. 95%) | Catches a reputation/auth regression early |
429 rate |
send responses 429 |
any sustained | You’re hitting the send-rate cap |
| DKIM resolution | external dig of both selectors |
either returns empty | Catches an accidental DNS deletion |
Security notes
- Protect the connection string like a password. The ACS connection string can send mail as your brand — store it in Key Vault, reference it from there, and rotate it on a schedule. A leaked string lets an attacker send phishing from your verified domain. See Azure Key Vault: Secrets, Keys and Certificates Done Right.
- Prefer Entra ID / managed identity over the connection string. Authenticate the SDK with a managed identity so there’s no shared secret to leak at all; grant least-privilege RBAC on the ACS resource. See Managed Identities Demystified: System vs User-Assigned and When to Use Each.
- DMARC is a security control, not just deliverability. A
p=rejectpolicy stops attackers spoofing your domain to your customers — phishing protection and inbox placement come from the same record, and theruareports surface spoofing attempts. - Guard the DNS zone and sender provisioning. Whoever can edit your DNS can change SPF/DKIM/DMARC and hijack your sending identity, and a sender username can send as your brand — treat both as high-privilege grants.
- Don’t leak internal data in mail or headers. Keep internal hostnames, stack traces and PII out of bodies and headers; transactional mail carries only what the recipient needs.
- Honour unsubscribe and consent. Ignoring unsubscribes raises complaints (a reputation hit) and breaches anti-spam law in many jurisdictions; wire
List-Unsubscribeand respect it. The theme: secure and trustworthy pull the same direction — DKIM/DMARC and least-privilege RBAC stop attackers and lift inbox placement.
Cost & sizing
ACS Email’s billing is simple: no hourly resource charge, just per email sent plus per MB transferred, so cost scales with how much you send — predictable and usually small for transactional workloads:
- Per-message + per-MB pricing dominates — volume is the driver, not infrastructure; attachments push the per-MB component, so link to large files. The domain and its SPF/DKIM/DMARC records cost nothing.
- Log Analytics ingestion is billed per GB — the diagnostic logs are worth it, but on high volume use sensible retention and sample engagement events.
- The hidden cost is bad list hygiene and rate limits, not the bill. Bounces and complaints damage reputation (lost conversions, not rupees), and throughput is gated by the per-domain send-rate caps (raisable via support) — plan bursts against the limit ahead of time.
A rough monthly picture for a small product: a few thousand to tens of thousands of messages costs tens to low hundreds of rupees, plus modest Log Analytics ingestion (~₹500–2,000). The expensive failure mode isn’t the bill — it’s a deliverability regression that halves your sign-up conversion. The cost drivers:
| Cost driver | What you pay for | Rough INR / month (small product) | What it affects | Watch-out |
|---|---|---|---|---|
| Messages sent | Per-email charge | Tens–low hundreds | Direct send volume | Scales linearly with volume |
| Data transferred | Per-MB of message size | Small unless attachment-heavy | Body + attachment size | Attachments inflate this; link instead |
| Custom domain | Nothing (DNS only) | ₹0 (registrar/zone aside) | Brand + reputation | Time cost: setup + warm-up |
| Azure-managed domain | Free to provision | ₹0 | Test sending | Shared reputation; not for prod |
| Log Analytics ingestion | Per-GB of diagnostic logs | ~₹500–2,000 | Deliverability visibility | Retention + engagement-event volume |
| Reputation (intangible) | List hygiene effort | “Free” but real | Inbox vs Junk placement | Bounces/complaints cost conversions |
Interview & exam questions
1. A send returns Succeeded but the email never arrived. What does Succeeded mean, and what do you check first? It means ACS accepted and dispatched the message, not that a mailbox received it — delivery, authentication and filtering all happen afterward at the receiver. Check first by reading the Authentication-Results header of a test send for SPF/DKIM/DMARC, then the delivery status in the ACSEmailStatusUpdateOperational logs.
2. Why is DKIM the most important record for ACS deliverability on a custom domain? Because DKIM is what makes DMARC align with your From: ACS signs with a d= equal to your domain (keys behind the two selector CNAMEs), so DKIM passes and aligns, whereas SPF often aligns only to the ACS MailFrom. Without DKIM, DMARC has nothing aligned and an enforcing policy junks or rejects your mail.
3. Explain SPF, DKIM and DMARC in one sentence each, and what each aligns on. SPF lists authorised sending IPs and aligns on the MailFrom; DKIM signs the message and aligns on the signature’s d= tag; DMARC requires SPF or DKIM to pass and align with the visible From and sets policy for failures. SPF authorises an IP, DKIM authorises a message, DMARC ties them to the From.
4. Difference between an Azure-managed domain and a custom domain, and when do you use each? Managed is an instant *.azurecomm.net subdomain with pre-set SPF/DKIM, shared reputation and low send limits — good for dev/test. Custom is your own, with auth records you publish, your brand in the From, and reputation you build — required for production. Prototype on managed; ship on custom.
5. A header shows spf=pass but dmarc=fail. What’s wrong and how do you fix it? SPF passed against the MailFrom, an ACS-owned host, so it aligns to that domain — not your From — and DKIM isn’t carrying alignment, so DMARC fails. Fix it by configuring DKIM so the signature’s d= matches your domain.
6. A recipient never gets mail and there’s no bounce. What’s likely, and how do you confirm? The address is likely on your suppression list from an earlier hard bounce or complaint, so ACS silently drops the send (Suppressed). Confirm via the suppression-list API or KQL (DeliveryStatus == "Suppressed"), and only remove it after confirming it’s valid.
7. What does a 5.7.26 bounce mean? A permanent rejection meaning the receiver failed your email authentication (SPF/DKIM/DMARC) — usually DKIM not aligning, or SPF not authorising the sender. Fix the failing record and re-test the header.
8. How do you roll out a DMARC policy without breaking your own mail? In stages: p=none with rua reporting first, confirm from the reports and a test header that ACS mail passes DMARC with aligned DKIM, then quarantine, finally reject. Enforcing before DKIM aligns quarantines your own legitimate mail.
9. Your bulk mail to Gmail and Yahoo started bouncing in 2024. What changed and what must you have? They introduced bulk-sender requirements: authenticated mail (SPF and DKIM), a DMARC record (at least p=none), a one-click List-Unsubscribe, and a complaint rate under their threshold. Missing any of these — or a high complaint rate — gets bulk mail rejected.
10. Where do you find delivery truth at scale, and how do you turn it on? In Log Analytics via the ACS Email diagnostic logs — EmailSendMailOperational, EmailStatusUpdateOperational (Delivered/Bounced/Suppressed) and EmailUserEngagementOperational. Turn them on with a diagnostic setting on the Communication Services resource routing those categories to a workspace, then query with KQL.
These map primarily to AZ-204 (Developer Associate) — develop solutions that use Azure Communication Services, plus the messaging/monitoring and diagnostic-logs objectives. The deliverability/DNS knowledge (SPF/DKIM/DMARC records) is portable email-architecture skill that also touches AZ-700 (name resolution) and AZ-500 (DMARC as anti-spoofing and secure secret handling).
Quick check
- A send reports
Succeededbut the email never reaches the inbox. What doesSucceededactually guarantee, and what’s the first free diagnostic you run? - You added the domain-verification TXT and the SPF TXT, ACS shows them
Verified, but mail still lands in Junk. What did you most likely forget, and how do you confirm it? - A received message’s header shows
dkim=passbutdmarc=fail. What kind of problem is this, in one word, and what’s the fix? - A recipient never gets your mail and you see no bounce. Name the mechanism that’s silently dropping it and the command to confirm.
- Why can publishing a second
v=spf1TXT record break all your email?
Answers
Succeededonly guarantees that ACS accepted and dispatched the message — not that any mailbox received it; delivery, authentication and filtering happen afterward at the receiver. The first free diagnostic is to send a test to an address you control and read theAuthentication-Resultsheader for SPF/DKIM/DMARC results.- You most likely forgot the two DKIM selector CNAMEs (
selector1-azurecomm-prod-net._domainkeyandselector2-...) — domain and SPF can verify without them. Confirm withdig +short CNAME selector1-azurecomm-prod-net._domainkey.<domain>(empty = missing) and a header showingdkim=none. - Alignment. DKIM is passing but its
d=domain doesn’t align with the visible From, so DMARC fails — configure DKIM so it signs with your domain (or correct the From) so DMARC aligns via the signature. - The suppression list — ACS silently drops sends to addresses that previously hard-bounced or complained (delivery status
Suppressed). Confirm withaz communication email domain suppression-list list ...or KQLwhere DeliveryStatus == "Suppressed". - SPF permits exactly one
v=spf1record per domain; a second one is apermerrorthat makes receivers ignore SPF entirely. Merge all senders into a single SPF record with multipleinclude:mechanisms.
Glossary
- Azure Communication Services (ACS) / Email Communication Services — the managed multichannel service (Email, SMS, Chat, Calling); the parent resource holds the connection string, the Email companion resource owns the domains, verification, SPF/DKIM and suppression list.
- Azure-managed domain — an instant
*.azurecomm.netsubdomain with ACS-owned DNS and pre-set authentication; fast and throwaway, shared reputation, not for production. - Custom domain — your own domain connected to ACS; you publish SPF/DKIM/DMARC and own the reputation; required for production, customer-facing mail.
- Domain verification — the TXT/CNAME records proving you own a domain; until
Verifiedyou can’t connect or send from it. - SPF (Sender Policy Framework) — a TXT record listing the servers authorised to send for a domain; receivers check the sending IP against it; aligns on the MailFrom.
- DKIM (DomainKeys Identified Mail) — a cryptographic signature on each message; the public key is published behind two selector CNAMEs (
<selector>._domainkey.<domain>); aligns on thed=tag — the workhorse for DMARC alignment. - DMARC (Domain-based Message Authentication, Reporting & Conformance) — a
_dmarcTXT policy requiring SPF or DKIM to pass and align (the passing domain matches the visible From), with ap=action (none/quarantine/reject) for failures. - MailFrom / Return-Path — the envelope sender used for bounces and SPF checks; on ACS it’s a managed address on your domain by default.
- Authentication-Results — the header a receiver stamps recording
spf=,dkim=anddmarc=outcomes; the fastest deliverability diagnosis. - Delivery status — the message’s final fate (Delivered / Bounced / Suppressed / Failed), available from the send operation result and the Azure Monitor logs — the truth the send API doesn’t give up front.
- Hard vs soft bounce — hard is a permanent rejection (
5.x.y, e.g. address doesn’t exist) that adds the recipient to the suppression list; soft is a temporary failure (4.x.y, e.g. mailbox full or greylisting) that is retriable later. - Suppression list — the per-domain list of addresses ACS will silently refuse to send to (after a hard bounce or complaint), protecting your sending reputation.
- Reputation / warm-up — the trust mailbox providers assign your sending IP and domain based on history; a new custom domain has none and must ramp volume gradually.
- Enhanced status code — the SMTP
x.y.zcode in a bounce (e.g.5.1.1no such user,5.7.26auth failure) that pinpoints why a message was rejected.
Next steps
You can now localise any “email didn’t arrive” report to a link in the chain and fix it. Build outward:
- Next: How to Host Public DNS on Azure DNS: Zone Delegation, Alias Records and Apex Domains — every deliverability fix here is a DNS record; master the zone that holds your SPF/DKIM/DMARC.
- Related: Azure Monitor and Application Insights: Full-Stack Observability — go deep on the Log Analytics + KQL that turn delivery status into a fleet-wide dashboard.
- Related: No Logs Showing Up? Troubleshooting Empty Log Analytics Tables and Ingestion Gaps — when your ACS Email diagnostic tables are empty, fix the ingestion path.
- Related: Azure Key Vault: Secrets, Keys and Certificates Done Right — keep the ACS connection string out of code so nobody can send phishing as your brand.
- Related: Managed Identities Demystified: System vs User-Assigned and When to Use Each — send from ACS with no shared secret at all.
- Related: How to Build Your First Logic App Workflow: Triggers, Actions, Connectors and Expressions — wire ACS Email into event-driven flows for notifications and alerts.