Email Message-ID Implementation: A Production Guide
A Message-ID gives one version of one email message a globally unique identity. It helps mailbox software build reply threads and gives operators a stable value to follow across an application, an MTA, a relay, and a received message. In a multi-customer email platform, it can also connect an abuse report to the responsible client, campaign, template, and recipient delivery record. It is useful evidence, but it is not authentication, a delivery receipt, or a replacement for a queue ID.
Most Message-ID problems begin with ownership. The application assumes the MTA will add the field, the MTA assumes the application already did, and neither system records the final value. Other implementations put customer names, recipient addresses, or database keys into a header that every recipient can inspect. A sound design decides where the identifier is created, makes collisions impractical, preserves it for the same message, and records the separate identifiers added by each delivery system.
Start with the job Message-ID actually performs
RFC 5322 section 3.6.4 says that every message should have a Message-ID field and that the generator must guarantee a globally unique value. The identifier refers to a particular version of a particular message. A normal outbound message has exactly one such field.
That definition gives Message-ID four practical jobs:
- identify the message when an operator compares application records, relay evidence, raw source, and support tickets;
- resolve a trusted abuse sample to the responsible client, campaign, template, recipient delivery, and sending route;
- provide the parent identifier used by
In-Reply-ToandReferenceswhen a reply thread is built; - help systems recognize that another copy or transport attempt belongs to the same logical message.
It does not prove who sent the message. It does not make SPF, DKIM, or DMARC pass. It does not confirm inbox placement, an open, a click, or even SMTP acceptance. Treat it as an opaque identity and correlation value.
Use the RFC syntax without inventing a 255-character rule
The normal shape is one identifier between angle brackets:
Message-ID: <[email protected]>
The part before @ is id-left. The part after it is id-right. RFC 5322 recommends a domain identifier on the right so the generating system can guarantee that its left-side values are unique within that domain. Internal whitespace, comments, and quoted strings are not valid inside the modern msg-id form.
RFC 5322 does not define a special 255-character maximum for Message-ID. It applies the general message-line limits: a line must not exceed 998 characters and should not exceed 78 characters, excluding CRLF. Those limits are documented in section 2.1.1. In production, there is no benefit in approaching either limit. A short random value and a controlled domain normally keep the complete field below the 78-character recommendation.
| Check | Accept | Reject or correct |
|---|---|---|
| Field count | One Message-ID | Missing field or two competing fields |
| Shape | <id-left@id-right> | Missing angle bracket, missing @, or an empty side |
| Characters | A conservative ASCII token and domain | Spaces, line breaks, comments, display names, or copied user input |
| Length | Compact enough to stay on a normal header line | An invented 255-character target or a value close to the 998-character line limit |
Choose one authoritative generator
The best generator is usually the first trusted system that knows it is creating a new message and can store the chosen ID before submission. For an application or ESP, that is often the message assembly service. For local scripts that hand unstructured mail to an MTA, the MTA may be the only practical generator.
| Generator | When it works well | Operational tradeoff |
|---|---|---|
| Application | The application needs the final Message-ID in its database, API response, or support tools before SMTP submission. | Every sending path must use the same library and ownership rule. |
| Submission service | Several applications share one controlled message-assembly layer. | The service needs a durable mapping back to each application record. |
| MTA | Local jobs submit messages without a complete RFC header set. | The application may not know the generated value unless logs or a return channel capture it. |
| ESP API | The provider documents that it creates a standards-compliant Message-ID and exposes it in events or the delivered source. | The provider may replace a supplied value, use a separate provider message ID, or expose different identifiers in different APIs. |
Do not let two layers generate a value independently. If the application owns Message-ID, downstream systems should preserve it. If the MTA is the fallback owner, it should add the field only when it is missing on trusted submissions. Test the real route because defaults and provider behavior vary.
Generate an opaque value with enough collision resistance
A strong practical design uses at least 128 bits from a cryptographically secure random source and encodes the bytes with a conservative alphabet. Thirty-two lowercase hexadecimal characters are easy to log, compare, and place in the RFC syntax:
Message-ID: <[email protected]>
The domain above is reserved for documentation. Use a stable domain controlled by the organization that generates the identifier. A dedicated subdomain such as id.mail.example makes ownership clear, but it does not need to match the visible From domain, Return-Path domain, HELO name, or DKIM signing domain.
Time alone is not enough. Two workers can create mail in the same clock interval, clocks can move backward, and cloned systems can restart the same sequence. If a design uses a timestamp for operational sorting, combine it with random data or a properly coordinated unique sequence. Do not depend on a process ID, recipient ID, or auto-incrementing database row by itself.
Keep private and business data out of the identifier
Message-ID is visible in raw message source, replies, archives, support systems, and sometimes public mailing-list records. Anyone who receives a copy can read it. The left side should therefore be an opaque token, not a small database in a header.
Do not embed:
- an email address, customer name, account number, or recipient database key;
- a campaign subject, order number, invoice number, support case, or reset token;
- a sending IP, internal hostname, process ID, or topology detail that should remain private;
- a sequential value that lets an outside observer estimate traffic or enumerate records;
- a secret, signature key, authentication token, or value accepted as authorization.
Store the relationship between the opaque Message-ID and internal records in a controlled database or log. The public identifier should be useful only as a lookup key.
Use Message-ID to investigate abuse across thousands of customers
A multi-tenant ESP or marketing platform needs to answer a practical question quickly: which client, campaign, template, and recipient delivery produced the message in an abuse report? Message-ID is a strong starting key because a standard Abuse Reporting Format report includes the original message or its complete header block. A direct complaint forwarded with its original source can provide the same evidence.
The safe design is one opaque public Message-ID connected to a protected message record and its recipient-delivery records. A readable composite such as the following can be valid RFC syntax when every component uses permitted characters, but RFC validity alone does not make it safe to expose:
recipient-id-campaign-id-client-id-date-template-id-random@sending.example
That structure exposes internal relationships to recipients, forwarded-message readers, archives, and anyone who receives an abuse sample. Sequential client, recipient, campaign, or template values can reveal account and traffic patterns. Hashing a short numeric ID without a secret does not solve the problem because an observer can test likely values. Stable field-by-field hashes also make messages linkable across time.
Keep the public field compact and opaque:
Message-ID: <[email protected]>
The v1 prefix can identify the generator format without revealing business data. The random portion supplies the unique lookup key. Use a stable domain controlled by the generating platform. A customer sending domain is not required on the right side, and a customer must not be allowed to inject an arbitrary value there.
Create the message record before submission, connect its recipient-delivery records, and retain the operational dimensions needed for an investigation:
| Stored field | What it identifies | Handling rule |
|---|---|---|
message_id | The exact RFC message version | Unique indexed lookup key. Store the exact generated token in one documented form and do not change its case. |
tenant_id or client_id | The customer account responsible for the send | Internal value only |
campaign_id and stream | The campaign and transactional or marketing traffic class | Use for scoped complaint-rate and pattern analysis |
recipient_delivery_id | One recipient delivery attempt | Link one or more delivery rows to the message. Use an internal key, not an email address in Message-ID. |
template_id and template_version | The exact content build used for the message | Retain the version or a content fingerprint so later edits do not erase evidence |
created_at | The application creation time | Store a precise UTC value internally instead of depending on a timestamp embedded in Message-ID |
| sender identities | Visible From, envelope Mail From, DKIM d=, HELO or EHLO, and Message-ID domains | Keep the identities separate because they perform different jobs |
| destination snapshot | Envelope recipient domain, resolved MX, classified mailbox provider, and SMTP peer at send time | Store the observed values and classification time internally because hosting and MX records can change |
| report origin | Validated blocklist, trap-network, or feedback-loop source and the reporting mailbox provider | Append this evidence when a report arrives. Do not overwrite the original destination snapshot. |
| route identifiers | MTA queue ID, provider ID, IP pool, region, and delivery route | Append each value when that system creates it |
An investigation can then follow a controlled path:
- Validate the source and structure of the complaint or feedback report before taking automated action. RFC 5965 warns that an unvetted report can be incomplete, incorrect, or deliberately forged.
- Extract the exact Message-ID from the attached original headers and resolve it to the send ledger.
- Identify the client, campaign, template version, recipient delivery, sender identities, route, and submission time.
- Join the record to complaint, unsubscribe, bounce, authentication, content, link-domain, volume, and provider evidence for the same client and traffic stream.
- Compare rates and repeated patterns across the campaign, template, domain, route, and time window. One validated complaint is evidence about one message, not automatic proof that every message from the customer is abusive.
- Apply the narrowest justified control, such as pausing one campaign, template, stream, identity, or client account, while preserving the raw evidence and the decision trail.
Recipient identification depends on how the platform composes mail. If each personalized recipient copy is a separate RFC message, give each copy its own Message-ID and the lookup can resolve to one recipient-delivery record. If one identical MIME message with one Message-ID is submitted to several envelope recipients, Message-ID identifies only that shared message. An ARF report might include Original-Rcpt-To, but report generators can redact recipient information for privacy. In that multi-recipient case, Message-ID alone cannot reliably identify which recipient complained.
Gmail is a separate case. Its feedback loop reports aggregate complaint patterns by Feedback-ID, and Google explicitly says not to use a value that is unique to every message, such as Message-ID, as a Feedback-ID identifier. Use Message-ID for exact message correlation when exact-message evidence is available. Use campaign, customer, mail type, and sender identifiers in the purpose-built Feedback-ID scheme for Gmail aggregate abuse detection. The Gmail Feedback Loop setup guide covers that design in detail.
A stateless encrypted token can carry internal fields when a database lookup is impossible, but it needs authenticated encryption, key identifiers, rotation, expiry and retention rules, strict size controls, and a reviewed failure path. For most platforms, a random Message-ID plus a durable indexed send ledger is simpler, shorter, and safer.
Use spam-trap and complaint samples to reconstruct the path
Some blocklist, reputation, and spam-trap operators provide a message sample, complete headers, or a limited header fragment when they report an event. Others provide less evidence, and trap owners generally do not reveal the trap address. When the sample retains Message-ID, the platform can use it to find the exact message record without needing the trap address in the public header.
The lookup should identify the client, campaign, recipient-delivery record, template version, list or acquisition source, sending identity, IP pool, route, submission time, and neighboring events. That is enough to investigate which customer and data source produced the hit. It should not become a trap-hunting tool that suppresses one exposed address while leaving the collection problem in place. Spamhaus explains that trap owners do not reveal their trap addresses and recommends correcting the permission, acquisition, and list-hygiene failure instead.
A sample is not self-authenticating. Message-ID can be copied or forged, and a malicious report can contain invented headers. Verify the report source, match the ID to the internal send ledger, compare timestamps and content fingerprints, and inspect the sending route. When the full original message is available, reverify the relevant DKIM signature and confirm that its h= list covered message-id. When only headers are available, use Authentication-Results only from a trusted receiving boundary. A DKIM-Signature field by itself does not prove that the signature passed. A match across independent evidence is stronger than Message-ID alone.
Traditional complaint feedback loops can provide the same exact-message correlation. Yahoo documents that its Complaint Feedback Loop returns ARF reports for messages associated with enrolled DKIM domains. Its reports include the full original headers, original body, and machine-readable metadata. Message-ID can therefore resolve the complaint to the original customer and delivery record when the platform retained a unique mapping.
Store two mailbox-provider values instead of putting a provider name in Message-ID:
| Value | When it is recorded | What it proves |
|---|---|---|
destination_provider_at_send | When the platform resolves the recipient domain, selects a route, and submits the message | The platform classification and SMTP destination observed at send time |
reporting_provider | When a validated ARF, provider complaint, or trap report arrives | Which trusted reporting system produced that event |
received_path | When complete trusted sample headers are available | The hop sequence visible from the trusted receiving boundary |
Putting gmail, yahoo, or another provider name in the public Message-ID records only the platform guess made before delivery. It does not prove where the message finally arrived, and it becomes stale when a domain changes hosting. Keep the provider classification behind the opaque lookup key. If the lookup service is sharded, a short non-business shard or format code can be included, such as v1.s07.random, but that code should route the lookup rather than claim a mailbox provider.
Consider a message submitted to a gmail.com recipient that later produces a validated Yahoo ARF complaint. That mismatch is a useful forwarding or alias signal. If the ARF contains the same Message-ID, the enrolled DKIM signature remains valid, and the trusted part of the Received chain shows the message moving from Google infrastructure to Yahoo, the evidence supports a Gmail-to-Yahoo forwarding path. The unique Message-ID can still resolve the complaint to the original Gmail recipient-delivery record, even if Yahoo redacts its final recipient.
The provider mismatch alone is not proof of forwarding. Yahoo hosts mail for many domains, MX classifications can be wrong or stale, a user can resend a message, and an untrusted report can be forged. Record the mismatch as possible_forwarding, then confirm it from the trusted trace, DKIM result, internal SMTP peer, and timing. Gmail does not send a traditional per-message complaint through its aggregate Feedback-ID program, so a valid Yahoo ARF for the same message is Yahoo feedback about the final Yahoo-side mailbox, not a Gmail Feedback-ID event.
This distinction also prevents the wrong suppression decision. Suppress or investigate the original recipient-delivery record resolved from the unique Message-ID and trusted report evidence. Do not suppress every address at the reporting provider, and do not assume the reporting provider was the original SMTP destination.
Generate Message-ID safely in PHP
PHP’s official random_bytes() documentation describes a cryptographically secure random source. Encode 16 bytes as hexadecimal, use a fixed controlled domain, and let the mail library serialize the header:
<?php
function newMessageId(): string
{
$token = bin2hex(random_bytes(16));
return sprintf("<%[email protected]>", $token);
}
$messageId = newMessageId();
The example deliberately fixes the documentation domain inside trusted code instead of accepting the domain or token from an HTTP request, template variable, customer field, or campaign name. Replace the reserved example domain with a domain the platform controls. If several trusted Message-ID domains are required, select them by an internal route key from a server-side allowlist. Do not concatenate unvalidated text into a header. A CR or LF from user input can become a header-injection vulnerability.
Record the value in the same transaction or durable work unit that creates the outbound message. Apply a unique database constraint if the Message-ID is stored in a relational system. A collision should fail closed and generate a fresh value, not overwrite the earlier record.
Use the standard library in Python
Python’s official email.utils.make_msgid() function returns a value suitable for a Message-ID field. Its optional domain argument is particularly useful when several workers should use one consistent controlled domain:
from email.message import EmailMessage
from email.utils import make_msgid
message = EmailMessage()
message["Message-ID"] = make_msgid(domain="id.mail.example")
Use the language’s email package or another well-maintained RFC-aware library rather than hand-building the whole message. The library should manage header serialization, line endings, and rejection of invalid values. The application still owns the lifecycle rule: create the ID once for that message, store it, and reuse it when the same message is retried.
Preserve the same ID for a retry of the same message
A temporary SMTP failure does not create a new message. When an MTA retries the same queued content, it should retain the original Message-ID. The same principle applies when an application repeats an API or SMTP submission after a timeout and the operation is still the same intended message.
| Event | Message-ID decision | Reason |
|---|---|---|
| MTA retries after a 4xx response | Keep the same value | The queued message has not changed identity. |
| Application retries after an uncertain API timeout | Reuse the stored value and an idempotency key | The retry should not look like a newly composed message. |
| A recipient-specific copy is composed separately | Create a distinct value for that message instance | Each independently composed version needs its own identity. |
| Content, audience, language, or attachments are revised for a new send | Create a new value | RFC 5322 associates an ID with one particular version. |
| An operator intentionally sends the message again later | Create a new value | This is a new transmission decision, not an automatic retry. |
Keep the same application message key and Message-ID available during ambiguous failures. The key can prevent duplicate submission, while the Message-ID makes any copies that did escape easier to correlate. Message-ID by itself is not a deduplication guarantee because receivers are not required to suppress mail with a repeated value.
Do not reuse one campaign ID as every recipient’s Message-ID
A campaign identifier and a Message-ID solve different problems. A campaign groups many messages. A Message-ID identifies one message version. If a platform creates a separately personalized MIME message for every recipient, each instance should receive a distinct Message-ID even though all of them share the same internal campaign key.
Keep campaign, tenant, template, and stream data in internal fields. When Gmail complaint aggregation is required, use the purpose-built Feedback-ID design described in the Gmail Feedback Loop setup guide. Do not place a unique per-recipient Message-ID in Feedback-ID, and do not reuse one Feedback-ID campaign token as Message-ID for a whole broadcast.
Build replies with In-Reply-To and References
A reply is a new message, so it receives a new Message-ID. It points back to the parent with In-Reply-To, while References carries the existing thread chain followed by the parent’s Message-ID:
Message-ID: <[email protected]>
In-Reply-To: <[email protected]>
References: <[email protected]>
<[email protected]>
Mailbox clients often use these fields to construct conversations. A similar subject line is not a reliable threading mechanism. Do not force unrelated notifications into one thread by copying the same Message-ID, and do not replace a proper parent reference with an internal ticket or campaign value.
Automatic replies should follow the same parent-chain rule. They also need the separate loop-prevention and response controls applicable to automated mail. The broader header and MIME context is covered in the email format and structure guide.
Distinguish a formal resent message from a new send
RFC 5322 provides a set of Resent-* fields for a message reintroduced into the transport system by a user or an agent deliberately acting as a resender. In that case, the original Message-ID remains part of the original message and a new Resent-Message-ID can identify the resent handling block.
This is not the normal mechanism for resending a marketing campaign, regenerating a notification, or sending corrected content. Those actions normally create a new message version with a new Message-ID. Use Resent-* only when the application genuinely implements the RFC resent model and can create a complete, correctly ordered resent field block.
Understand what Postfix may add
Postfix cleanup can add a missing Message-ID, but administrators should not assume that it will repair every remote submission. The current Postfix documentation lists always_add_missing_headers = no as the default. With that default, missing headers are added for clients covered by local_header_rewrite_clients, including local submission paths under the default policy.
Inspect the effective configuration rather than copying a setting from another server:
postconf always_add_missing_headers
postconf local_header_rewrite_clients
Setting always_add_missing_headers = yes changes behavior for traffic outside the local rewrite class and deserves testing. Postfix also warns that adding a field can break a DKIM signature that deliberately covered the absence of that field. The safe assembly order is to create or complete Message-ID before the controlling outbound DKIM signature is applied.
The Postfix queue ID is not the Message-ID. A message can receive a new queue ID after reinjection through a content filter or another Postfix instance while retaining its RFC Message-ID. Capture both if a delivery path crosses multiple queues.
Then validate paid outbound MTAs
After the open-source Postfix example, apply the same ownership rule to each paid outbound MTA in the delivery estate. Commercial products do not share one configuration switch, and behavior can vary by version, injection method, source rule, or mail class. The application should normally create the opaque RFC Message-ID before submission. Any MTA feature that adds a missing field should be a tested fallback, not the primary correlation design.
| Paid MTA | Documented behavior to account for | Implementation rule |
|---|---|---|
| PowerMTA | Bird’s PowerMTA guidance confirms that source controls can add a missing Message-ID. PowerMTA also has its own delivery and accounting records. | Supply the RFC field upstream, verify the received copy, and map the final value to the PowerMTA accounting record. Use the licensed manual for the exact directive supported by the installed release. |
| MailerQ | MailerQ outgoing JSON can carry a complete MIME message, while SMTP intake creates a separate internal, per-recipient message-id JSON property. | Put the RFC Message-ID inside the MIME content. Store MailerQ’s internal identifier in a different ledger field and never substitute it into the public header without an explicit design. |
| GreenArrow Engine | The GreenArrow HTTP Submission API preserves a supplied Message-ID in structured submissions or generates one when absent. Raw mime submission does not generate one. A Mail Class can also add one when missing. | Test every injection mode in use. Prefer a supplied opaque value so the application, GreenArrow delivery event, and received source can resolve to the same ledger record. |
| Momentum | Momentum can add a missing field when rfc2822_messageid_header is set to ifneeded. Momentum separately assigns internal recipient message IDs and a batch ID. | Keep the RFC Message-ID, Momentum recipient ID, and batch ID in separate columns. The internal per-recipient ID is useful for queue work but is not the transmitted RFC field. |
| Halon | Halon HSL can add or set message headers before DKIM signing. Its queue also has separate transaction and queue identifiers, plus tenantid, jobid, and metadata fields. | Keep customer and campaign values in protected tenant, job, or metadata fields. Add the opaque RFC Message-ID before signing, then correlate it with the Halon transaction and queue records. |
Run the same acceptance test on every paid MTA and every active injection path. Submit one message with a known valid Message-ID and another without one. Record the application key and MTA queue identifiers, inspect the final raw message, verify DKIM, and confirm that delivery, bounce, and complaint events resolve to the intended ledger row. Repeat the test after upgrades, failover changes, or a move between SMTP and API injection.
Add Message-ID before DKIM signing
Message-ID is not an authentication identity and its domain is not part of DMARC alignment. DMARC evaluates the visible From domain against a passing DKIM d= domain or the SPF-authenticated Mail From domain. Matching Message-ID to any of those domains is neither a DMARC rule nor proof of better deliverability.
It is still good practice to protect the final Message-ID with the outbound DKIM signature. RFC 6376 section 5.4 requires the From field to be signed and permits the signer to include other present fields. Add message-id to the signer’s h= list after the final value exists:
DKIM-Signature: v=1; a=rsa-sha256; d=mail.example;
s=outbound2026;
h=from:to:subject:date:message-id:message-id:mime-version:content-type;
bh=...; b=...
The repeated message-id in this example is deliberate oversigning. The first occurrence covers the one existing field. The second covers the absence of another occurrence, so adding a duplicate Message-ID later causes verification to fail. A signer that lists the name only once protects the selected existing value but does not make the absence of an extra instance part of the signature. Use the signer’s supported oversigning mechanism rather than editing a generated DKIM field by hand.
The example uses reserved names and placeholder hashes. The operational check is the received message: confirm the expected Message-ID, a passing signature, and the intended message-id coverage in that signature’s signed header list. When dual DKIM is used, inspect each signature independently. One signature can cover or oversign Message-ID while another does not.
Correlate the identifiers instead of treating them as interchangeable
A production mail path creates several identifiers. Preserve their relationships at the point where each becomes known:
| Identifier | Scope | Can it change? | Best use |
|---|---|---|---|
| Application message key | The application’s work item or send decision | Controlled by the application | Idempotency, business lookup, and state transitions |
| Message-ID | One version of one RFC message | Retained for the same message, replaced for a new version | Cross-system correlation and threading |
| MTA queue ID | One queue record on one MTA | Usually changes on reinjection or another MTA | Queue, routing, retry, and SMTP log analysis |
| ESP or provider message ID | A provider API or event object | Provider-defined | Webhook, event, and support correlation |
| Envelope recipient or VERP key | A recipient delivery attempt | Often unique per recipient | Bounce and suppression processing |
| Feedback-ID | An aggregate Gmail campaign, customer, type, or sender group | Stable for the intended reporting population | Aggregate complaint analysis |
A useful event record can therefore contain application_message_key, message_id, queue_id, provider_message_id, recipient_event_key, destination_provider_at_send, reporting_provider, content fingerprint, route, timestamp, and event type. Sensitive recipient data can remain in the protected event store instead of appearing in Message-ID.
For bounces, use the recipient-level envelope and DSN evidence first. Message-ID is valuable supporting context, but one message can have several envelope recipients and several delivery outcomes. The bounce-handling guide explains how to keep message, recipient, and transport evidence separate.
Validate the delivered source, not only the pre-send object
A unit test can prove that a library produced a plausible value. It cannot prove that an ESP retained it, a relay did not add a second one, or the final DKIM signature covered it. Send through every important production route to controlled mailboxes and download the original received source.
| Validation | Expected evidence | Typical failure |
|---|---|---|
| Header count | Exactly one Message-ID | Application and relay both inject a field, or imported mail already contained one |
| Syntax | One compact <id-left@id-right> value | Missing brackets, spaces, line break, or malformed domain |
| Application mapping | The received value equals the stored value | Provider replaces the supplied ID or the wrong record is logged |
| Retry behavior | The same queued message retains its value | A retry handler rebuilds the message and silently creates a new ID |
| Personalized copies | Separately composed recipient versions have distinct values | A campaign-level ID is copied into every message |
| DKIM protection | Expected signature passes, covers message-id, and oversigns it when configured | The header is added after signing, modified by a relay, or a duplicate is not covered by policy |
| Correlation | Application, queue, provider, and recipient events resolve to the same message record | Only one identifier is retained, leaving a gap between systems |
Repeat the test for a normal send, a personalized send, an SMTP retry, an API retry after a simulated timeout, a reply, and any reinjection path. Include failover regions and secondary MTAs. The difficult bugs usually sit outside the primary route.
Troubleshoot missing, duplicate, and changing values
Message-ID is missing: determine whether the application was meant to set it and whether the actual submission client belongs to the MTA’s local fixup policy. Do not assume an internet-facing SMTP listener will repair remote mail.
Two fields are present: capture the raw source at each boundary. Find the first hop where the second field appears. Correct the ownership rule rather than deleting an arbitrary instance after DKIM signing.
The ID changes between the application and mailbox: check the ESP’s header policy and whether the value was valid when submitted. Some APIs expose a provider message ID that looks similar but is not the transmitted RFC field.
Every retry has a new ID: the retry path is recomposing instead of resubmitting the stored message. Persist the chosen ID before the first attempt and make the retry consume that stored value.
Many recipients share one ID: determine whether this was truly one RFC message with several envelope recipients or many separately composed copies. For individualized messages, move campaign correlation into an internal key and generate a Message-ID for each instance.
Replies appear in separate threads: inspect the child message’s new Message-ID, In-Reply-To, and References. Subject matching alone is not enough, and copying the parent Message-ID into the reply is invalid.
DKIM passes but Message-ID is not protected: inspect the passing signature’s own h= list. The presence of a passing DKIM signature does not mean every field was signed.
Monitor the implementation as infrastructure changes
Message-ID failures often return after a migration, new ESP route, regional failover, or library upgrade. A small set of controls catches most regressions:
- count outbound messages with no Message-ID, more than one field, or invalid syntax;
- sample the domain on the right side and alert on unexpected domain drift;
- enforce uniqueness in the application store and alert on any collision;
- measure how often the received value differs from the submitted value by route;
- verify that retry events retain the original Message-ID;
- confirm that intended DKIM signatures still cover
message-idand retain any configured oversigning; - retain the mapping between application, Message-ID, queue, provider, and recipient event keys for the required support and compliance period.
A domain change needs a planned transition. During a multi-platform migration, both old and new controlled Message-ID domains can be valid as long as each generating system maintains uniqueness. Do not rewrite old queued messages simply to make the domain look uniform.
Use this production checklist
- Assign one trusted application, submission service, ESP, or MTA as the Message-ID generator.
- Create exactly one compact RFC 5322 value for each independently composed message version.
- Use a controlled, stable domain and at least 128 bits of secure randomness.
- Keep client names, email addresses, database keys, campaign details, topology, and secrets out of the field.
- Persist the value before submission and reuse it for retries of the same message.
- Create a new value for a revised message, a separately personalized copy, or an intentional new send.
- Give replies a new Message-ID and construct
In-Reply-ToandReferencesfrom the parent. - Add the final field before DKIM signing, include
message-idin the intended signature’s header list, and oversign it when the signer supports that policy. - Record Message-ID alongside application, queue, provider, and recipient-event identifiers.
- Resolve trusted complaint samples through the private ledger and scope any action to the evidence-supported client, campaign, template, stream, or identity.
- Keep the original destination-provider snapshot separate from the reporting provider, and confirm suspected forwarding from trusted trace headers.
- Use
Feedback-ID, not a per-message Message-ID, for Gmail aggregate abuse analysis. - Validate raw received source across primary, failover, retry, reply, and reinjection paths.
- Monitor missing, duplicate, malformed, replaced, reused, and domain-drift cases.
A reliable Message-ID implementation is deliberately boring. The identifier is opaque, unique, stable for the life of the message, and easy to follow when something goes wrong. NitWings can help with email infrastructure architecture when Message-ID ownership, MTA behavior, DKIM order, spam-trap or feedback-loop correlation, or a multi-ESP migration crosses several systems.


