A customer emails once and somehow gets two ticket numbers. The cause is almost never the customer — it's a threading strategy that guesses instead of tracking a real conversation identity, plus a mailbox poller with no protection against running twice. Here's the actual mechanics, and the fix that holds even when everything else goes wrong.
If you run a support mailbox behind WordPress, you've probably seen this: a customer replies to ticket #482, and instead of that reply landing on #482, it spawns a brand-new #511. Now two agents might work the same problem, the customer gets two different threads to keep track of, and whoever closes #482 first leaves #511 sitting open looking unresolved. It reads like a minor annoyance. It's actually a symptom of a threading strategy that was never going to hold up, and it's worth understanding exactly why so you can tell whether a given helpdesk plugin has actually solved it or just made it less frequent.
There are three broad approaches to matching an incoming email to an existing conversation, and they differ enormously in how reliable they are. Most of the plugins in the WordPress ecosystem use the weakest of the three, because it's the easiest to implement and looks fine in a five-minute demo.
Subject-line threading, and why it's a guess
The simplest approach matches incoming mail to a ticket by parsing the subject line, usually looking for a pattern like Re: [Ticket #482] Refund request and pulling the number back out with a regular expression. This works right up until something touches the subject line, which happens constantly and mostly outside your control.
- Outlook and some corporate mail gateways rewrite or truncate subjects — stripping bracketed tags, collapsing repeated
Re: Re: Re:prefixes, or applying a security banner that pushes your ticket tag out of the parsed range. - Customers forward the original email to a colleague, who replies from a different address with the subject intact but no relationship to your system's parsing beyond string matching.
- Mailing-list and helpdesk software both try to own the subject tag, and when a customer's own ticketing system (their IT department, an agency, a marketplace) is doing the same trick, the tags collide or get double-wrapped.
- A customer simply edits the subject before hitting send, which normal mail clients allow without any warning.
Any one of these breaks the regex, and a broken match means your system falls back to creating a new ticket, because it has nothing better to go on. Subject-line threading isn't unreliable because it's badly coded — it's unreliable because a human-readable string that any mail client is free to mangle is fundamentally the wrong data to build an identity match on.
RFC 5322 headers: threading on structure, not prose
Email has had a proper answer to this since long before helpdesk software existed. RFC 5322, the standard that defines the format of an email message, specifies three headers built specifically to represent conversation structure without touching anything a human ever reads or edits.
- Message-ID — a globally unique identifier the sending mail server attaches to every outgoing message, in the form
<random-string@domain>. No two legitimate messages should ever share one. - In-Reply-To — present on a reply, this header holds the Message-ID of the message being replied to. It's how a mail client draws a thread even when the subject has been edited.
- References — a space-separated list of every Message-ID in the thread so far, going back to the original message, giving you the full ancestry rather than just the immediate parent.
These headers are generated and propagated by mail infrastructure, not by anything a customer can casually change by editing a text box. A helpdesk that reads incoming mail and matches on In-Reply-To (falling back to scanning References for any known Message-ID) is matching on data the standard exists specifically to make reliable. It's the correct primary strategy, and it's what a competently built system should default to.
Signed reply tokens: the fallback that doesn't depend on the client
The fallback that closes the remaining gap is to stop relying on the incoming message to self-identify at all, and instead embed identity in the outgoing message that the helpdesk itself controls. When an agent replies to a ticket, the system generates a unique, cryptographically signed token — something like a reply-to address of support+t482-a91f3c@yourdomain.com, where the token after the plus sign encodes the ticket ID and a signature that proves the address was actually issued by your system and not guessed or forged.
When the customer replies, the token comes back in the recipient address itself, which almost nothing in the mail path rewrites, because doing so would break delivery entirely. The signature matters because without it, anyone who worked out your ticket-numbering scheme could inject replies into arbitrary tickets simply by emailing support+t1@yourdomain.com — the signature makes a forged token computationally infeasible to produce, rather than merely unlikely.
Put together, a correct threading cascade looks like this: try the In-Reply-To and References headers first, since they're the standard and cover the overwhelming majority of replies correctly; fall back to a signed reply token if the message came through one of your own tokenised addresses; and only as a last resort — and even then, treated as a hint rather than a certainty — fall back to subject parsing. A system that only implements the last one is the one you'll see duplicate tickets from regularly.
The other source: cron overlaps and webhook retries
Threading logic explains duplicates that come from the customer's side of the exchange. There's a second, entirely separate source that has nothing to do with how the email is parsed and everything to do with how the mailbox is polled.
Most WordPress-based helpdesks fetch new mail on a schedule using WP-Cron, which runs on page load rather than as a true system-level scheduled job. On a busy site, or one behind a CDN and caching layer that generates traffic even without real visitors, it's entirely possible for two cron runs to overlap — the second one starts fetching the mailbox before the first has finished processing what it already pulled. If both runs see the same unread message before either has marked it processed, both create a ticket for it, and you get an exact duplicate with no threading ambiguity involved at all — same subject, same body, same sender, two ticket numbers.
The same failure mode shows up with webhook-based mailbox connections (Gmail API push notifications, Microsoft Graph subscriptions). Webhook delivery is explicitly not exactly-once by design — providers retry delivery if they don't get a fast, clean acknowledgement, which means your endpoint should expect to receive the same notification more than once and behave correctly when it does. A helpdesk that processes each webhook call as if it can only ever arrive once will duplicate a ticket every time a retry fires, and retries are the normal case under load, not an edge case.
Why deduplication belongs in a database constraint
The instinctive fix for both problems is an application-level check: before creating a ticket, query the database for a message with the same Message-ID (or the same token, or the same sender-plus-subject-plus-timestamp fingerprint), and skip creation if you find one. This is better than nothing, but it has a structural race condition baked in. Between the moment your check runs and the moment your insert runs, another process — a second overlapping cron run, a duplicate webhook delivery arriving a few milliseconds apart — can perform exactly the same check, also find nothing, and also proceed to insert. The check-then-act pattern is not atomic, and under concurrent execution it fails exactly when you need it most: precisely during the overlaps and retries that caused the problem in the first place.
The fix that actually holds is to stop treating deduplication as application logic and make it a property of the schema instead: a unique constraint on the column storing the Message-ID (or token) for each ticket's originating email. With that constraint in place, if two processes both attempt to insert a row with the same Message-ID, the database itself — not your PHP code, not a timing-dependent check — rejects the second insert outright, and your application simply catches that rejection and treats it as "already processed" rather than creating a second ticket. This works regardless of how many overlapping cron runs or retried webhooks hit the table simultaneously, because the guarantee is enforced by the database engine's own transaction handling, not by hoping two requests never land close enough together to race.
None of this is exotic engineering. RFC 5322 headers are decades old, signed tokens are a standard mitigation against forgery, and unique constraints are a first-year database concept. What's missing in a lot of WordPress helpdesk plugins isn't sophistication — it's that subject-line parsing plus an application-level duplicate check is quick to build and demos perfectly, and the failure modes only show up under real mail traffic and real concurrency, well after launch. If you're evaluating a service desk and want to know whether duplicate tickets will be a recurring support burden, ask specifically how it threads replies and whether ticket creation is protected by a database constraint — the answer tells you almost everything.
See how this fits into the rest of the mailbox pipeline — connection options, mailbox health monitoring, and the full threading cascade — on the Email to Ticket page, or the wider ticket workflow on Service Desk. If you're weighing this against SupportCandy specifically, our SupportCandy comparison covers what's included by default versus behind an add-on, and Pricing has the full plan breakdown.