Skip to main content

Debugging preview-vs-send mismatch

The email rendered correctly in Email Studio's preview, went out, and the actual sent version is different. Six checkpoints that reproduce the divergence deterministically before the next send fires — context variables, _messagecontext gates, locale formatting, time-sensitive logic, Lookup data drift, and the only diagnostic that's truly conclusive: a real test send.

How-to·Last updated 2026-05-19·Drafted by Lira · Edited by German Medina

The email rendered correctly in Email Studio's preview. QA approved. The Send went out. The actual sent emails look different — a content block appeared (or didn't), a timestamp shifted, a price formatted differently, a personalization fell back to a default when it shouldn't have. The Send Activity reports Completed. There's no error. The next person on the team asks how preview could possibly disagree with send when "it's the same template" — and you have to explain that preview and send use different code paths, and the only way to know which path your script took is to walk the differences explicitly.

This page is the diagnostic playbook for that exact shape. Six checkpoints that reproduce the divergence before the next send fires, plus the closing note that the only truly conclusive verification is a real test send to a staging Business Unit. See gotchas — #9.

What preview and send don't share

[ Email Studio Preview ]               [ Production Send ]
        ↓                                       ↓
_messagecontext = "Preview"           _messagecontext = "Send"
_jobid          = NULL                _jobid          = (real JobID)
_emailaddress   = chosen test sub     _emailaddress   = real recipient
Now()           = preview render time Now()           = send moment
locale          = MC user's tenant    locale          = subscriber's profile
Lookup data     = preview-time DE     Lookup data     = send-time DE
Cloud-write     = sometimes blocked   Cloud-write     = always fires (live)

Each row above is a place the rendered output can diverge. The checkpoints below walk each one in order from most-to-least common as a real production failure.

Step 1 — Find every _messagecontext gate in the email body

The most common preview-vs-send mismatch: a content block (or AMPscript-set variable) was gated on _messagecontext == "Send". In preview, the gate evaluated false, the block didn't render, and the QA reviewer thought the gate-controlled content was supposed to be hidden. In send, the gate evaluated true, the block rendered, and the rendered email surprised everyone.

%%[
  IF Lowercase(_messagecontext) == "send" THEN
    /* This block is INVISIBLE in preview but VISIBLE in send.
       If the QA reviewer didn't know it existed, they signed off on a
       "what you see is what you get" preview that wasn't. */
    SET @giftCode = ClaimRow("de_gift_codes", ...)
  ENDIF
]%%

Grep the email body and any included Code Resources for _messagecontext and review every match. The Cleon convention: document gated blocks in the email's frontmatter comment (a top-of-body comment listing every _messagecontext-gated block) so the next reviewer knows what to look for.

Step 2 — Find every _jobid reference

_jobid is NULL in preview, a real ID in send. Any AMPscript that branches on _jobid or uses it as part of a lookup key changes shape between the two contexts.

%%[
  /* This Lookup keyed on _jobid returns nothing in preview (_jobid is
     NULL) and the real send-time row in send. The block that depends
     on the lookup result renders differently. */
  SET @sendConfig = Lookup("de_send_configs", "Subject", "JobID", _jobid)
]%%

Search the script for _jobid and verify every use either:

  • Has an Empty() fallback that handles the NULL-in-preview case, or
  • Sits inside a _messagecontext == "Send" gate, or
  • Is read-only and its NULL value won't change rendered output.

Bare _jobid references without one of those three protections cause preview-vs-send divergence.

Step 3 — Find every time-sensitive computation

Now() returns the moment of render. Preview at 11am and send at 9pm give very different values. Anything that branches on the current time, or interpolates a relative time string ("you have 3 hours left"), or uses date comparison against Now(), diverges if preview and send happen at different moments.

%%[
  /* AT RISK — "Today is the last day of the sale!" might render true
     in 11am preview and false in 9pm send (or vice versa, around
     midnight boundaries). */
  SET @daysLeft = DateDiff(Now(), @saleEnds, "Day")
  IF @daysLeft == 0 THEN
    SET @subject = "Last day!"
  ENDIF
]%%

Audit Now(), SystemDate(), LocalDate(), DateAdd(Now(), ...), DateDiff(..., Now(), ...), and any string output that includes a relative time. For every match:

  • Confirm the value would be the same in preview and at the actual send time
  • If it wouldn't be, either pre-compute the time-sensitive value upstream in a SQL Activity (so it's fixed at audience-build time), or accept that preview is not authoritative for time-sensitive content and require a real test send near the production send time.

Step 4 — Find every locale-aware format function

FormatDate(d, fmt, locale) and FormatNumber(n, fmt, locale) use the locale argument. If the locale comes from the recipient's profile and the recipient (preview test subscriber vs real recipient) has different locale values, the rendered output diverges silently.

-- Compare locales between the test subscriber used for preview and a sample
-- of real recipients. If they differ, the rendered formatting differs.
SELECT
  Locale,
  COUNT(*) AS Subscribers
FROM de_send_audience_<send_name>
GROUP BY Locale
ORDER BY Subscribers DESC;

If the audience has multiple locales but the QA preview only tested one, the recipients in the other locales see different formatting. The fix is upstream: the audience-build SQL should align the locale per recipient, and the QA process should explicitly preview at least one recipient per distinct locale in the audience.

Step 5 — Compare Lookup data between preview-time and send-time

The DEs your AMPscript reads via Lookup / LookupRows change between preview-time and send-time. A row that existed in the segments DE when QA previewed at 3pm may have been updated or deleted by a 5pm SQL Activity rebuild before the 9pm send.

-- Verify the row exists for the affected recipient at the moment you're
-- looking. Run this twice — once now (before send), once after send —
-- and compare.
SELECT
  SubscriberKey,
  Tier,
  UpdatedDate
FROM master_segments
WHERE SubscriberKey = 'abc-12345';

The hand-off failure: a nightly SQL Activity rebuilds master_segments between QA preview and send. The Tier value changes; the Lookup returns a different result at send time. The fix isn't AMPscript-side — it's coordinating the audience-build and the send-time so they read from the same snapshot. Audit the Automation's step ordering: SQL Activity that rebuilds the lookup DE should run before the audience-build that the Send Activity uses, not after.

Step 6 — The only conclusive verification: a real test send

Preview lets you iterate fast. It does not verify send-time behavior end-to-end. The only conclusive verification is a real test send to a real subscriber in a staging Business Unit, run at a time close to the planned production send time.

Test send procedure (Cleon convention):

1. Create a staging Business Unit (or reuse an existing one).
2. Create a test subscriber there with realistic data — not a synthetic
   placeholder. The test subscriber's locale, AttributeValue profile, and
   sendable DE row should mirror a real production recipient.
3. Send the email to the staging BU's audience (size 1: the test subscriber).
4. Open the actual delivered email in the test subscriber's inbox.
5. Compare against preview. Any divergence is a bug to investigate before
   the production send fires.
6. If the production send is scheduled for a specific time, run the test
   send within an hour of that time (especially if the script has any
   time-sensitive logic).

The test send runs the same renderer the production send does. _messagecontext == "Send", _jobid is real, all gates fire, locale resolves per the test subscriber, time-sensitive logic uses the actual send moment. Anything that diverges in the test send will also diverge in production — and you catch it in the staging audience of one, not in 50,000 production inboxes.

Common divergences ranked by frequency

| Divergence | Where it shows up | Fix | |---|---|---| | _messagecontext gated block hidden in preview, visible in send | A content block appears in inbox that QA didn't expect | Step 1: document gated blocks; preview-validate the send-context state | | _jobid Lookup returns NULL in preview, real value in send | Conditional renders differently based on send config | Step 2: Empty() fallback or _messagecontext gate around _jobid use | | Now()-based logic shifts between preview and send time | Time-sensitive subject lines, countdowns, "today is X" branches | Step 3: pre-compute upstream in SQL, or accept preview is not authoritative | | Locale formatting differs between test subscriber and real recipients | Numbers/dates render in unexpected format for some segments | Step 4: audit audience locale distribution; preview multiple locales | | Lookup DE changed between preview and send moment | Tier/segment value renders differently than QA expected | Step 5: audit Automation step ordering; lookup DE rebuild before audience-build | | Cloud-write fires in send but not preview | CRM updates land that QA didn't see preview for | Step 1 (gates) + test send + audit de_log_sf_writes after the first send | | Subscriber Attribute populated for test subscriber but missing for real recipients | A profile-attribute personalization works for QA, fails for some recipients | Step 4 (audit profile coverage); add Empty() default |

Related