Email Threading Headers: Message-ID, In-Reply-To, and References
A practical guide to the headers, data model, sending rules, and tests that keep transactional and support replies in the right email conversation.
Overview
Email threading headers tell mail clients and receiving systems how messages relate. Give every outbound message a unique Message-ID. When sending a reply, set In-Reply-To to the direct parent's Message-ID and build References from the parent's existing reference chain plus that parent ID. Subject prefixes such as Re: may help people scan an inbox, but they are not a reliable relationship key. A production design stores the exact wire identifiers and also keeps its own immutable conversation ID, because external senders and mailbox clients do not always preserve a perfect chain.
Understand the three-header contract
RFC 5322 defines Message-ID as a single unique message identifier. It also says replies should include In-Reply-To and References where appropriate. The fields solve different jobs: Message-ID identifies this message, In-Reply-To names the message being answered directly, and References carries the ancestry that lets a client reconstruct a conversation.
Treat the values as opaque identifiers. Do not parse business meaning from the text around the @ sign, lowercase them, trim their angle brackets before storage, or regenerate a provider-returned value into your preferred format. Normalize only enough to compare safely in your own system, while retaining the original header value for later replies and diagnostics.
- Generate one Message-ID per logical message, not per rendering attempt
- Keep provider message identifiers separate from RFC Message-ID values
- Use angle-bracketed message identifiers on the wire
- Never use the subject as the database key for a conversation
| Header | What to write | What to persist |
|---|---|---|
| Message-ID | One globally unique ID for the new message | Exact outbound ID and provider ID separately |
| In-Reply-To | The direct parent message's ID | Exact parent ID used for the send |
| References | Prior reference chain followed by the parent ID | Parsed ordered list and original raw header |
Build reply headers from the direct parent
Suppose the original outbound message has Message-ID <welcome-42@mailer.example>. A customer reply should identify itself with a new Message-ID, set In-Reply-To to <welcome-42@mailer.example>, and include that ID in References. If your app then answers the customer, the new message points In-Reply-To at the customer's Message-ID and carries forward the customer's References chain before appending the customer ID.
The parent must be the actual message being answered, not merely the first message your application sent. That distinction matters in a shared support inbox: always pointing at the campaign root can flatten branches, confuse clients, and make operator replies appear out of order.
- Load the direct parent message selected by the user or automation.
- Read the parent's exact Message-ID and parsed References list.
- Create a new unique Message-ID for the reply.
- Set In-Reply-To to the parent's Message-ID.
- Set References to the parent's reference chain followed by the parent's Message-ID.
- Persist the relationship before enqueueing the send, then record the provider attempt separately.
| Message | In-Reply-To | References |
|---|---|---|
| A: original outbound | None | None |
| B: recipient replies to A | <A@example> | <A@example> |
| C: operator replies to B | <B@example> | <A@example> <B@example> |
Store enough data to survive imperfect mail
A robust data model separates a conversation from its transport headers. Store an internal conversation_id and message_id primary key, parent_message_id, direction, RFC Message-ID, raw and parsed References, provider message ID, sender, recipients, timestamps, and delivery state. Put a uniqueness constraint on normalized RFC Message-ID when one exists, but allow a quarantine path for malformed or missing identifiers instead of attaching them to a guessed thread.
Inbound matching should use a confidence ladder. First match In-Reply-To against a known RFC Message-ID. Then inspect References from newest to oldest for a known ancestor. A signed reply alias or tokenized reply-to address can provide a separate application correlation signal. Subject, sender, and time proximity are useful for a manual-review suggestion, not an automatic high-confidence join.
- Keep raw headers for incident debugging and parser improvements
- Deduplicate inbound delivery before creating a second message row
- Do not merge conversations across workspaces or tenants
- Record which signal produced the threading decision
| Signal | Confidence | Recommended action |
|---|---|---|
| Known In-Reply-To value | High | Attach as child of that message |
| Known ID in References | High to medium | Attach to the conversation; record inferred parent |
| Valid signed reply token | High for conversation | Attach to token's conversation and retain header evidence |
| Matching normalized subject only | Low | Queue for review; do not auto-merge |
Keep message identity stable across retries
A transport retry is not a new logical email. If a queue retries the same send after a timeout or temporary failure, reuse the logical message record and its Message-ID. Creating a fresh Message-ID on every attempt can produce duplicate visible messages if an earlier request succeeded but its acknowledgement was lost. Pair the logical record with an idempotency key and retain each provider attempt as a child event.
A deliberate resend is different. If a human chooses to send an updated message, create a new logical message and Message-ID. Decide explicitly whether it is a reply in the existing conversation or a new thread. That keeps audit history honest and prevents retry mechanics from leaking into the recipient experience.
- Generate identity before the network call
- Do not confuse provider acceptance IDs with internet message identifiers
- Make retry workers idempotent
- Audit every attempt without showing each attempt as a separate email
| Situation | Message-ID rule | Conversation rule |
|---|---|---|
| Temporary provider retry | Reuse the logical Message-ID | Same message and conversation |
| Failover attempt for the same send intent | Reuse unless the provider contract prevents it | Same logical message; log a new attempt |
| Human edits and resends | Create a new Message-ID | Choose reply or new conversation deliberately |
| Automated follow-up | Create a new Message-ID | Reference the intended parent if continuing the thread |
Test threading as an interoperability feature
A unit test can prove header construction, but only mailbox tests reveal how clients display the result. Build fixtures for a new conversation, one reply, a three-message chain, an inbound reply with folded headers, missing In-Reply-To with usable References, duplicate webhook delivery, and a malformed identifier. Then send controlled conversations through the mailbox providers and clients your users actually use.
Your acceptance check should inspect both raw source and UI behavior. Confirm that each message has one expected Message-ID, reply headers point to known IDs, the visible order is correct, and the inbound parser attaches the reply to the right tenant and conversation. Include forwarding and support-system handoffs because intermediaries can alter the chain.
- Test long chains and folded References headers
- Include messages composed by humans outside your application
- Log parser and matching reasons without logging sensitive bodies unnecessarily
- Monitor orphaned replies and manual re-thread rates after release
- Assert exact header output from deterministic message fixtures.
- Round-trip replies through at least two external mailbox systems.
- Inspect raw message source instead of trusting only the threaded UI.
- Replay the same inbound event and verify that no duplicate message is created.
- Remove or corrupt each threading signal and confirm the fallback goes to review rather than a guessed conversation.
- Verify tenant boundaries with identical subjects and participants in separate workspaces.
Where Mailbase fits in a reply workflow
Mailbase's relevant role is the workflow around replies: a shared inbox, durable outbound work, event history, assignment, and AI-assisted drafts. Those features are useful only when transport identity is preserved accurately. Threading headers belong in the sending and inbound-ingestion contract; team workflow belongs above that contract.
If you build or integrate a similar system, keep the boundary explicit. The provider transports messages, RFC identifiers connect message ancestry, and the application controls tenant-safe conversation records, operators, status, and follow-up work. When the evidence is ambiguous, surface an orphaned reply for review rather than quietly merging customer conversations.
- Show operators the whole conversation, not only the latest webhook event
- Keep AI drafts attached to an internal conversation before they are sent
- Preserve transport identifiers in the audit trail
- Fail closed when a reply cannot be resolved to one workspace
Common Mistakes
- Choosing a tool before deciding who owns deliverability.
- Treating DNS authentication as a one-time checkbox instead of an operating baseline.
- Mixing product-critical transactional email with experimental marketing sends, with no clear boundary.
- Trusting headline metrics (like open rate) that privacy proxies now inflate.
Sources & Further Reading
Official docs for current setup details, pricing, and API behavior — verify specifics there, since they change.
Related guides
More on email threading headers and the surrounding email infrastructure workflow:
FAQ
Which headers are used for email threading?
The core headers are Message-ID, In-Reply-To, and References. Message-ID identifies the current message, In-Reply-To identifies its direct parent, and References carries the prior message ancestry used to reconstruct the conversation.
Is Re: in the subject enough to thread an email?
No. Subject prefixes are presentation conventions and can collide across unrelated conversations. Use RFC message identifiers for transport relationships and an internal conversation ID for application state.
Should an email retry get a new Message-ID?
A transport retry of the same logical send should normally keep the same logical message identity and Message-ID, while each provider attempt is logged separately. A deliberate new or edited send should receive a new Message-ID.
How should inbound email be matched to a thread?
Match a known In-Reply-To first, then known identifiers in References, then a secure application correlation signal such as a signed reply token. Do not auto-merge on subject alone; quarantine ambiguous messages for review.