pixellint

Time · engineers · PMs

Ingestion time is not event time

A worker drains Friday's orders on Monday and stamps now(), so dashboards spike on Monday and Friday's campaigns get nothing. The events are not missing; they are mis-clocked.

PostHog

Send ISO 8601 for PostHog event time. Do not assume a Unix epoch in a generic timestamp field means event time. Capture documents timestamp as ISO 8601. A numeric epoch is a different type. In some paths it is treated as ingestion-related, and the historical value is not applied. Pixellint flags a non-ISO timestamp as vendor.posthog.body.timestamp.invalid.

Backfills must send 2026-01-15T18:04:00Z, not 1736964240, not Date.now(). Insights that show a spike today after a historical import are this bug until proven otherwise. The capture endpoint still returns 200. Distinct_id missing is a different 200-and-drop. You can get both: no person, and the wrong day.

TikTok and AppsFlyer treat epoch as arrival

TikTok Events API timestamp is ISO 8601. An epoch number is not that format, and TikTok then stamps the event with the time it arrived. A delayed queue that sends 1770000000 looks 'on time' in Events Manager for the drain minute, not the purchase minute. Pixellint: vendor.tiktok-events-api.body.timestamp.invalid.

AppsFlyer eventTime, when it is the wrong type, is also stamped as arrival. Send UTC yyyy-mm-dd hh:mm:ss.sss. Adjust created_at is ISO 8601 when present; created_at_unix is seconds. Mixing those MMP fields with Date.now() is the same ingestion-time collapse with a different logo.

CAPI and queues

Meta event_time is the time of the conversion, in seconds, not the time your queue posted it. A delayed worker that sends event_time=now attributes to whatever campaign is running at drain time. Store event_time on the order when it happens, and replay that value. Floor the stored millisecond instant. Do not re-read Date.now() in the worker.

If the worker is more than about 7 days late, Meta will not take the event as a conversion inside that window no matter how honest the stamp. That is a late-events problem. Stamping now() to sneak past the age check attributes Friday's store sales to Monday's campaigns. Do not do that.

Segment timestamp versus received time

Segment timestamp is ISO 8601 event time. If you omit it, Segment uses received time. That is fine for live track calls. It is fatal for a warehouse sync that replays a week. Pixellint flags a non-ISO timestamp as vendor.segment.body.timestamp.invalid. Omitting the field is legal and means received time. An epoch number is invalid, not a convenient omit.

Braze events[].time is required ISO 8601. There is no 'omit and we stamp now' excuse on that field in the pack contract. Send the action time.

received_at is a different column

CDPs and warehouses often keep received_at alongside timestamp. Reports that group by received_at will not match ads UIs that group by event_time. Pick one clock for the meeting, and label it. A Monday spike in the warehouse with a Friday spike in Ads Manager is this split until proven otherwise.

Retries must not refresh the event time. Refreshing it to now() on the third attempt is how a 500 turns into a late conversion that looks on-time and double-counts against a pixel that already fired. Reuse event_id and the original event_time. Pixellint is not affiliated with PostHog, Meta, or Segment. It checks format, not whether your queue lied about when the sale happened.

// WRONG: drain time on a Monday replay of Friday orders
event_time: Math.floor(Date.now() / 1000)
timestamp: Date.now()

// RIGHT: time of the action
event_time: Math.floor(order.purchasedAtMs / 1000)
timestamp: new Date(order.purchasedAtMs).toISOString()

Check the artifact

Paste the pixel URL or JSON body into the playground. Same engine as pixellint validate. Nothing leaves the tab.