Time · engineers
event_time in the future is a bug
A conversion that happens after now, in Unix time, did not happen. Either the unit is wrong, the device clock is wrong, or you added a buffer because you were afraid of being late.
The millisecond future
Meta CAPI event_time with Date.now() is 13 digits. That instant is millennia ahead. It is outside the 7-day window. It never attributes. The status code does not say future. Count digits before you debug the access token. Pinterest CAPI is the same 10-digit seconds contract. Date.now() there is the same future.
GA4 timestamp_micros with Date.now() is the inverse: 13 digits where 16 are required, so the value is too small rather than in the future. Same class of bug, opposite direction. LinkedIn, Amplitude, and Reddit v3 want the 13-digit value Meta rejects. Copying the Meta floor(Date.now()/1000) onto those APIs is how you land in 1970.
Real skew
Phones, IoT checkouts, and VMs with stalled clocks send event times a few minutes ahead. Ads APIs are not generous about the future. Clamp to now if the local clock is ahead, or better, stamp the event on a server you NTP. Browser Date.now() on a device with a wrong clock will skew the pixel too. CAPI from your NTP'd worker is the correction, not a second wrong clock.
Do not add five minutes to event_time to beat a perceived race with the pixel. Dedup is event_id, not a later timestamp. A padded clock just looks like skew. If the pixel fired at T and CAPI at T+5 minutes with a different event_id, you double-counted and looked late. If they share event_id, the extra five minutes did not help.
ISO fields and arrival time
PostHog and TikTok do not put you in the year 56000 when you send an epoch number. They treat it as arrival time. That is a different skew: every delayed event looks perfectly on-time for the drain. Insights spike now. Campaigns that ran then get nothing. The fix is ISO 8601 of the action, not NTP on the worker (though you still want NTP).
AppsFlyer eventTime has the same arrival-time behavior for the wrong type. Braze events[].time rejects epoch in the pack (invalid), which is louder. Segment timestamp invalid is the same loud failure. Prefer loud. Silent arrival-time is how backfills pass QA.
What to log
Log the raw integer, the digit count, and the ISO rendering in UTC. A 10-digit Meta value should look like today's date. A 13-digit value will render as a date you will not mistake for a campaign day. A 16-digit GA4 value should render as now, not as 1970 plus a few weeks of microseconds-as-milliseconds confusion.
On retry, keep the original event_time even if the worker's clock has moved. Replacing it with now() on a delayed retry turns clock skew into a late-event problem and can double-count if event_id also changed.
Check digits in CI
Assert String(event_time).length === 10 for Meta and Pinterest. Assert String(conversionHappenedAt).length === 13 for LinkedIn. Assert String(timestamp_micros).length === 16 for GA4. Assert String(time).length === 13 for Amplitude. Assert typeof timestamp === 'string' && timestamp.includes('T') for PostHog, TikTok, Segment, and Braze. Then run pixellint validate json.
Pixellint is not affiliated with those vendors. The digit rules are the pack contracts: event_time.invalid, timestamp_micros.invalid, conversionHappenedAt.invalid, time.invalid, event_at.invalid, timestamp.invalid.
// WRONG: Meta (13-digit future)
event_time: Date.now()
// WRONG: GA4 (13-digit ms, not 16-digit us)
timestamp_micros: Date.now()
// WRONG: LinkedIn / Amplitude (10-digit 1970)
conversionHappenedAt: Math.floor(Date.now() / 1000)
// RIGHT
meta.event_time = Math.floor(Date.now() / 1000); // 10
ga4.timestamp_micros = Date.now() * 1000; // 16
linkedin.conversionHappenedAt = Date.now(); // 13
Check the artifact
Paste the pixel URL or JSON body into the
playground. Same engine as
pixellint validate. Nothing leaves the tab.