LW IT Solutions
« Blog Overview /Digital Analytics/Tutorials / Tutorial: The Five Timestamp Formats in a...

Tutorial: The Five Timestamp Formats in a Tracking Stack and How to Convert Between Them

Tutorial: The Five Timestamp Formats in a Tracking Stack and How to Convert Between Them
Contents
  1. Five Formats That Meet in One Stack
  2. Telling Them Apart by Counting Digits
  3. The Day That Is Not the Same Day
  4. Converting in BigQuery
  5. The Windows That Reject a Timestamp
  6. The Two Ambiguities That Cannot Be Fixed Afterwards
  7. Questions and answers
  8. Sources

A single click produces a timestamp in the browser, another in the tag manager, a third in the analytics export and a fourth in the advertising platform it is forwarded to. All four describe the same moment. None of them is written the same way.

Most of the confusion resolves by counting digits. The part that does not is a day boundary, and it is worth knowing about before a report is compared against another one.

Five timestamp formats with an example value and a digit count each, above a time axis showing an event at half past one at night that falls on two different dates depending on the convention
The same moment in five notations. The axis below shows the case in which two of them disagree about the date.

Five Formats That Meet in One Stack

All of them count from the same zero point: midnight on 1 January 1970, in UTC. What differs is the unit and the packaging.

Format Example Where it appears
Seconds 1790465400 Meta CAPI event_time, most REST APIs
Milliseconds 1790465400000 Date.now(), gtm.start, most JavaScript
Microseconds 1790465400000000 GA4 BigQuery event_timestamp
ISO 8601 2026-09-27T01:30:00+02:00 Logs, Search Console API, most exports
Date only 20260927 GA4 event_date, BigQuery table suffix

The first three are unambiguous: they are a number of units since a fixed point, and they contain no time zone because they do not need one. The fourth is unambiguous as long as it carries an offset – the +02:00 at the end is what makes it a moment rather than a description.

The fifth is different in kind. A date without a time is not a moment but a range of twenty-four hours, and which twenty-four hours depends on a time zone that is not written anywhere in the value.

Telling Them Apart by Counting Digits

For a number found in a log, in an export or in a payload, the length settles it.

10 digits   seconds        1790465400          until the year 2286
13 digits   milliseconds   1790465400000
16 digits   microseconds   1790465400000000
 8 digits   date           20260927            not a moment at all

Two mistakes come out of ignoring this, and both produce a result that looks plausible. Reading milliseconds as seconds moves the date to the year 58 707 – which is obviously wrong and therefore harmless. Reading seconds as milliseconds moves it to 21 January 1970, which is not obviously wrong at all and appears in a report as a tiny bar at the far left of every chart.

A guard clause is three lines and worth having wherever a value arrives from outside.

def zu_sekunden(wert):
    z = len(str(int(wert)))
    if z == 16: return int(wert) / 1_000_000
    if z == 13: return int(wert) / 1_000
    if z == 10: return int(wert)
    raise ValueError(f"unbekanntes Zeitformat mit {z} Stellen: {wert}")

The Day That Is Not the Same Day

The GA4 BigQuery export carries both a moment and a date, and they use different conventions.

event_timestamp is microseconds since the epoch, and therefore UTC. event_date is a string of the form YYYYMMDD in the reporting time zone of the property. For a property set to Berlin, every event between midnight and two in the morning local time therefore carries a date one day ahead of what its own timestamp says in UTC.

An event on 27.09.2026 at 01:30 Berlin time

event_date                                          20260927
DATE(TIMESTAMP_MICROS(event_timestamp))           2026-09-26
DATE(TIMESTAMP_MICROS(event_timestamp),
     "Europe/Berlin")                             2026-09-27

This is not a bug and it cannot be switched off. It is the reason two queries over the same table can return different daily totals, and the difference is always the same size: the events of the first one or two hours of each day.

The rule that follows is short. Grouping by day uses either event_date throughout or DATE(..., "Europe/Berlin") throughout, never a mixture – and a query that joins two tables has to use the same convention on both sides. The table suffix _TABLE_SUFFIX follows event_date, which means a date filter on the suffix and one on the converted timestamp select different rows.

Converting in BigQuery

Three functions cover every case, and each takes the unit in its name.

SELECT
  event_timestamp,
  TIMESTAMP_MICROS(event_timestamp)                       AS moment_utc,
  DATETIME(TIMESTAMP_MICROS(event_timestamp),
           "Europe/Berlin")                               AS ortszeit,
  DATE(TIMESTAMP_MICROS(event_timestamp), "Europe/Berlin") AS tag_lokal,
  FORMAT_TIMESTAMP("%FT%T%Ez",
                   TIMESTAMP_MICROS(event_timestamp),
                   "Europe/Berlin")                       AS iso_mit_versatz,
  UNIX_SECONDS(TIMESTAMP_MICROS(event_timestamp))         AS sekunden
FROM `projekt.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX = "20260927"
LIMIT 10;

The distinction between TIMESTAMP and DATETIME is the one worth internalising. A TIMESTAMP is a moment and knows what it is; a DATETIME is a wall-clock reading without a zone, and converting a timestamp into one is where the zone information is deliberately dropped. Storing a DATETIME and reading it back somewhere else is how an hour disappears.

In the other direction, a string becomes a timestamp only with a stated format, and the format string is where an offset is either read or invented.

PARSE_TIMESTAMP("%FT%T%Ez", "2026-09-27T01:30:00+02:00")   -- korrekt
PARSE_TIMESTAMP("%FT%T",     "2026-09-27T01:30:00")        -- als UTC gelesen

The Windows That Reject a Timestamp

Two receiving systems check the value rather than just storing it, and both fail in a way that is easy to miss.

The Meta Conversions API accepts an event_time within the last seven days. An event older than that is rejected, and a batch upload of historical conversions therefore silently loses everything beyond the window – the response reports how many events were received, not how many were kept. The check is the number of events in the response against the number sent.

A timestamp in the future is the other half of the same rule, and it happens more often than the past one: a server whose clock runs a few minutes fast produces events that the platform declines. Which means a failing conversion upload is worth investigating as a clock problem before it is investigated as a data problem.

Offline conversion uploads to advertising platforms usually want a local time with an explicit offset rather than an epoch value.

2026-09-27 01:30:00+02:00

The offset is not decoration. Without it the platform applies the account time zone, and an account configured in a different zone from the shop shifts every conversion by the difference – which shows up as conversions attributed to the wrong day and, at a month boundary, to the wrong month.

The Two Ambiguities That Cannot Be Fixed Afterwards

Everything above is a conversion. These two are losses, and the only remedy is not to create them.

The first is a local time without an offset. 2026-09-27 01:30:00 is not a moment – it is a moment in some zone, and if the zone was not written down, no later processing can recover it. A guess is possible and is a guess: the same string can be two moments an hour apart.

The second is the changeover night. When the clocks go back, the hour between two and three in the morning happens twice, and a local time inside it is genuinely ambiguous even with the zone known. An offset resolves it, because the two passes have different offsets. A zone name alone does not.

Both disappear if values are stored as UTC and converted only for display. That is a one-line rule that sounds obvious and is broken constantly, because a local time is what a person wants to read – and the fix is to convert at the point of reading rather than at the point of writing.

Questions and answers

A value has 19 digits. What is it?

Most likely nanoseconds since the epoch. This format is used by OpenTelemetry, for example, and by the UnixNano function in Go; in a tracking stack it mainly turns up in server and log data. The guard clause rightly rejects it instead of guessing, and it can be extended by one line for 19 digits with the divisor 1_000_000_000.

Can JavaScript handle 16-digit microseconds without loss?

Today, yes, but only just. JavaScript stores numbers as double-precision floating-point values, which represent integers exactly only up to 9,007,199,254,740,991 (Number.MAX_SAFE_INTEGER). A microsecond value from 2026, at 1,790,465,400,000,000, is about five times smaller; counted in microseconds, the limit reaches into the year 2255.

Nanoseconds, by contrast, lie far beyond it. A 19-digit value loses its last digits when read with JSON.parse, without any error being raised. Where such values are needed in JavaScript, they are best read as strings and converted with BigInt, or divided down to milliseconds on the server beforehand.

Why not simply set the property’s time zone to UTC?

That removes the difference between event_date and the timestamp but moves the problem somewhere else. Every daily report in GA4 then starts at midnight UTC, which for a shop in Berlin means one in the morning in winter and two in the morning in summer. A purchase at half past midnight counts towards the previous day, and daily figures no longer match the till, the inventory system and the ad accounts that work in local time.

Then there is the change itself. A new reporting time zone applies only to data from the moment of the change; earlier days stay as they were. The day of the change is therefore shorter or longer than twenty-four hours, and a time series spanning the change puts Berlin days next to UTC days.

The rule from the article holds up better: the property keeps the zone the business works in, and every query commits to one convention. Grouping by Berlin days in BigQuery then has the same day boundaries as the interface, and grouping by UTC is a deliberate choice of a different day.

How can a server clock be confirmed as the cause of a rejected upload?

With two checks. On the server itself, timedatectl on Linux shows whether the clock is synchronised with a time server; if the line System clock synchronized reads no, a drifting clock is likely. The second check needs no access to the operating system: every HTTP response from the platform carries its server’s time in the Date header, and comparing it with the local time at the same moment shows the offset to within about a second.

If the offset is in the range of minutes, it disappears as soon as synchronisation with a time server is running. Events already sent with the wrong time and rejected then have to be sent again with a corrected timestamp, as long as they are still inside the seven-day window.

Lukas Wojcik

Lukas Wojcik

Systems architect and technology enthusiast specializing in scalable tracking solutions, GMP Stack (GA4 & GTM), and robust backend architectures. Advocate for clean code and privacy-first design.

Get in Touch

Briefly describe your project or inquiry for a tailored response. This site is protected by reCAPTCHA.

Write a comment

Differing figures from other accounts and questions about the setup are welcome here.

The email address is not published. Required fields are marked with an asterisk.

ALL ARTICLES & CATEGORIES

CCTV

Follow this category by RSS

Cloud & AI

Follow this category by RSS

Data Privacy

All 15 articles in this category Follow this category by RSS

Digital Analytics

All 54 articles in this category Follow this category by RSS

Digital Marketing

All 33 articles in this category Follow this category by RSS

IT & Networks

All 18 articles in this category Follow this category by RSS

Music Production

Follow this category by RSS

Raspberry PI

Follow this category by RSS

Smart Home

All 19 articles in this category Follow this category by RSS

Web Development

Follow this category by RSS

WordPress Plugins & Tricks

Follow this category by RSS