A customer unsubscribes, gets the confirmation, and finds the newsletter back in her inbox the following week. Nobody touched the settings, the unsubscribe link works in testing, and yet the complaint lands on the service desk with the letters GDPR in it.
In Marketing Cloud Engagement, that scenario is almost never a bug. It follows from the data model, and from one value in particular: the Subscriber Key.
The Subscriber Key identifies a subscriber. Two rows carrying the same email address under two different keys are, as far as the platform is concerned, two separate people. Everything else follows from that.
| Topic | Behaviour | Consequence with two keys |
|---|---|---|
| Sending | Duplicates are dropped from a send on the basis of the key | The same email twice in the same inbox |
| Unsubscribe | The link acts on the subscriber identified by their key | The person stays active under the other key |
| Status | Each subscriber carries its own status | An address in error stays active elsewhere |
| Contacts | Each key creates a contact, and contacts count towards your contract | The same person is billed twice |
The cause is nearly always the same: a sending data extension related to subscribers on the email address, because the source file carried no customer identifier. A loyalty import, a list from a supplier, an export from an events site, and the database splits in two without a single error message.
⚠️ The signal to watch: a complaint about an unsubscribe that was not honoured is rarely an isolated case. If one exists, there are often thousands, and each one is a GDPR breach.
The system data view _Subscribers exposes All Subscribers in SQL. First query, the volume: how many addresses exist under more than one key.
SELECT
s.EmailAddress,
COUNT(*) AS NbCles,
MIN(s.DateJoined) AS PremiereInscription
FROM _Subscribers AS s
WHERE s.EmailAddress IS NOT NULL
GROUP BY s.EmailAddress
HAVING COUNT(*) > 1
A few dozen rows is a clean-up job. Several thousand points to a feed that is wired badly and keeps producing duplicates on every run.
Second query, the cases at risk: subscribers still active while the same address is unsubscribed under another key. Those are the ones that trigger complaints.
SELECT
actif.SubscriberKey,
actif.EmailAddress,
MAX(desab.DateUnsubscribed) AS DateDesabonnement
FROM _Subscribers AS actif
INNER JOIN _Subscribers AS desab
ON desab.EmailAddress = actif.EmailAddress
AND desab.SubscriberKey <> actif.SubscriberKey
WHERE actif.Status = 'active'
AND desab.Status = 'unsubscribed'
GROUP BY actif.SubscriberKey, actif.EmailAddress
The GROUP BY stops the same active key being written twice when the address was unsubscribed under several other keys, which would make the write fail in a data extension whose SubscriberKey is the primary key.
💡 Business units: _Subscribers holds enterprise level data. From a child business unit, query ENT._Subscribers to get the full list.
The underlying fix takes weeks. In the meantime, an exclusion script stops sends to those addresses. It is an AMPscript expression evaluated for every recipient at send time: if it returns true, the recipient is dropped.
RowCount(LookupRows("Desabonnes_Autre_Cle", "EmailAddress", emailaddr)) > 0
The test runs on the address rather than the key, so it covers every duplicate at once. LookupRows ignores case, which stops an address typed in capitals from slipping through. Salesforce does warn that a complex exclusion script, or one applied to a large table, slows the send down: on high volumes, filter the audience before it enters the journey.
As long as the feed relates on the email address, the duplicates come back. Three steps, in this order:
⚠️ Do not clean up by deleting rows in All Subscribers: deleting a subscriber also erases its status. If the person comes back through an import, they are active again, and the contact keeps counting until it is deleted in Contact Builder.
Salesforce advises against the email address as a Subscriber Key. A good key is unique, stable over time, independent of the channel and shared with your other systems. In practice that is the customer identifier from your master system, or the Contact or Lead identifier when Marketing Cloud Connect is in place. The Contact Key must carry the same value.
Changing the key later is not a maintenance task: Salesforce states that a migration is planned with your account executive and can freeze sends and imports for several days. Better to choose correctly before the first load.
In Adobe Campaign, the technical key of a recipient is generated by the database, and reconciliation uses the keys you pick at each import. In Marketing Cloud, the Subscriber Key plays both parts at once: you supply it, and it is not easily corrected.
The unsubscribe mechanism will feel familiar. A blacklist flag set on one recipient record does not protect a second record carrying the same address. The difference lies in deduplication, which Campaign can apply on the address while preparing a delivery, whereas Marketing Cloud relies on the key.
Before hunting for a bug in a journey or in an unsubscribe link, run the first query. If NbCles goes above 1 across thousands of addresses, the problem is not in the send: it is in the data model, and no interface setting will fix it.
Two rows carrying the same email address under two different keys are, as far as the platform is concerned, two separate people. The unsubscribe link acts on the subscriber identified by their key, so the person stays active under the other key. That scenario is almost never a bug, it follows from the data model.
The system data view _Subscribers exposes All Subscribers in SQL: a GROUP BY on EmailAddress with HAVING COUNT(*) > 1 gives the number of addresses existing under more than one key. A few dozen rows is a clean-up job, several thousand points to a badly wired feed that keeps producing duplicates on every run. From a child business unit, query ENT._Subscribers to get the full list.
An exclusion script, an AMPscript expression evaluated for every recipient at send time, drops the recipient when it returns true. The test runs on the address rather than the key, so it covers every duplicate at once, and LookupRows ignores case. Salesforce does warn that a complex exclusion script, or one applied to a large table, slows the send down: on high volumes, filter the audience before it enters the journey.
No. Deleting a subscriber also erases its status: if the person comes back through an import, they are active again. The contact itself keeps counting until it is deleted in Contact Builder, through the contact delete process.
Salesforce advises against the email address. A good key is unique, stable over time, independent of the channel and shared with your other systems: in practice the customer identifier from your master system, or the Contact or Lead identifier when Marketing Cloud Connect is in place, with the Contact Key carrying the same value. Changing the key later is not a maintenance task: the migration is planned with your account executive and can freeze sends and imports for several days.
I can review your duplicates, your exclusions and your sends, then hand you a written diagnosis with the queries to run on your own data.
Tell me about your situation →