pixellint

Time · engineers · marketers

Unix time has no timezone. Your shop does.

A purchase at 21:00 America/New_York is 02:00 UTC the next calendar date. If you send 21:00 as UTC seconds, Meta files it five hours late, and Ads Manager then applies the account timezone on top of that.

Unix and ISO

Meta event_time and GA4 timestamp_micros are Unix instants. They do not carry America/Chicago. You convert the real instant to seconds or microseconds in UTC, which is what Unix already is. The bug is treating a local wall clock as if it were UTC. Date.parse of a naive string is implementation-defined. Do not rely on it in a worker in us-east-1 that thinks 21:00 means 21:00Z.

ISO 8601 timestamps on PostHog, Segment, TikTok, and Braze can carry an offset (2026-07-26T21:00:00-04:00) or a Z. Send the offset you mean. A naive local string with no offset will be interpreted as UTC by someone in the chain. AppsFlyer eventTime is documented as UTC yyyy-mm-dd hh:mm:ss.sss. If you pass local wall time in that field without converting, AppsFlyer files the wrong instant.

Account timezone is display

Meta Ads Manager and Google Ads each have an account timezone. That setting decides which calendar day a Unix instant falls on in the UI. It does not change the payload. Two accounts that share a pixel and disagree on timezone will split the same event_time across two dates.

Shop timezone, warehouse timezone, and ads timezone should be written down. If finance closes the day at midnight local and ads reports midnight Pacific, the daily ROAS meeting is a timezone meeting. Changing the ads account timezone restates which calendar bucket Unix instants fall into. It does not rewrite event_time on events you already sent.

DST and stores

Offline uploads of POS tickets labeled 17:00 without a zone will shift twice a year. Stamp the instant in UTC when the sale is recorded, or stamp ISO 8601 with an offset. Do not reconstruct the instant later from a date column and a guess. America/New_York 17:00 in March is not the same UTC offset as 17:00 in July.

Currency is a separate field. Timezone mistakes do not show up in value. They show up as conversions on the wrong campaign day. That is why they survive QA that only checks Purchase fired. A 13-digit Meta event_time is a different class of bug (unit). A correct 10-digit stamp of the wrong instant is this class.

Workers in UTC, shops in local

Lambda, Cloud Functions, and Kubernetes nodes often run UTC. new Date() is UTC. That is what you want for Unix. The bug is taking order.local_time_hour from the shop DB (a wall clock) and feeding it to Date.UTC as if the numbers were UTC components. Use a timezone-aware library on the shop zone, then export epoch.

Google Ads conversionDateTime is not Unix and not ISO 8601. It is yyyy-mm-dd hh:mm:ss+|-hh:mm with a space. Pixellint: vendor.google-ads-click-conversions.body.conversionDateTime.invalid. That field carries an offset. Meta event_time does not. Convert the same instant into both shapes. Do not copy the Ads string into event_time.

What to log

Log the shop timezone name, the stored instant as ISO with offset, the Meta seconds, the GA4 microseconds, and the Ads Manager account timezone. When the daily totals disagree, those five lines tell you whether you shifted hours or whether you used Date.now() milliseconds on a seconds field.

Pixellint is not affiliated with Meta or Google. It counts digits and ISO shape. It does not know America/Chicago. A 10-digit event_time of the wrong instant validates clean and still wrecks the day.

// WRONG: treat local wall clock as UTC
event_time: Date.UTC(2026, 6, 26, 21, 0, 0) / 1000
// 21:00 UTC, not 21:00 America/New_York

// RIGHT: instant -> Unix seconds / microseconds / ISO
const ms = purchasedAt.toMillis(); // already UTC instant
meta.event_time = Math.floor(ms / 1000);
ga4.timestamp_micros = ms * 1000;
posthog.timestamp = new Date(ms).toISOString();

Check the artifact

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