Back to News & Insights
JavaScript October 1, 2026 · 4 min read

Don't localize carrier scan timestamps. You are not storing an instant.

If you build order-tracking UI, you have almost certainly done this: new...

Don't localize carrier scan timestamps. You are not storing an instant.

It looks correct. It is the single most common way tracking pages end up lying to customers, and the bug report you get back will not look like a timezone bug. It will look like this, which is a real support message we got this week:

"This morning my package said delivered at 21:49. This was at 6:04 am. It was not delivered to my house."

Two different complaints are tangled together there, and only one of them is the carrier's fault.

A tracking event is produced by a device in a facility, or in a driver's hand, writing down the time where the scan happened. Carriers publish that wall-clock reading. Most of them do not publish an offset with it.

So the string 2026-09-30 21:49:00 arriving in your pipeline is not an instant in time. It is "the clock on the wall at whichever building scanned this said 21:49". Without the offset you cannot convert it, and if you treat it as UTC and then render it in the viewer's zone, you have invented a number nobody published.

The failure mode is nasty because it is plausible. Attach Z to a naive string, render in the browser's zone, and a 09:57 delivery in Germany becomes 16:57 for a viewer in Vietnam. Nothing throws. The page just shows a time that never existed, and the customer - who was standing in their hallway at 09:57 - concludes your tracking is broken.

Using UTC getters on a value you only ever tagged as UTC is not a hack, it is the inverse of the tag. You get back exactly what the carrier said. The rule that follows:

Never convert a timestamp whose offset you were never given. Render it as text and label whose clock it is.

If you do have a genuine offset - some carriers send one, and your own events like "we last checked this" are real instants - convert those freely. Just keep the two classes apart in your schema. One is an instant. The other is a string that looks like one.

Once you stop converting, you notice the second problem: not every scan has a time.

Measured on delivery scans flowing through our platform (sample of ~27,000 recent delivery events):

| | share | |---|---| | carries a time of day | ~96% | | date only, no clock | ~4% |

Four percent sounds small until it is the one parcel a customer is angry about. Those arrive as 09/18/2026 with nothing after it. new Date() will happily give you midnight, your formatter will print 00:00, and you have just told someone their parcel was delivered at midnight.

Render that as the date alone. A missing time is information: it tells the customer the carrier never published a minute, so there is no minute to argue about.

Watch the format spread while you are in there. In the same sample the clock-bearing values came in at least two shapes - YYYY-MM-DD HH:MM:SS and July 20, 2026 9:00 PM. If you regex for one, you will silently classify the other as "no time". I did exactly that in the first pass of this measurement and got 72% instead of 96%; the 12-hour strings were hiding in the remainder.

Delivery scans in that same sample, by hour of day (carrier local): Busiest window is late morning to mid-afternoon, roughly 8-9% of deliveries per hour Between 20:00 and 05:59: about 9%, or roughly 1 in 11

A 21:49 delivery scan is not an anomaly worth surfacing as one. Contract and gig drivers finish the route they were assigned, and in peak season that runs later. If your UI puts a warning badge on evening deliveries, you will generate support tickets for one parcel in eleven, every day, forever.

Having removed three ways to be wrong, here is the one field worth promoting in the UI: the location on the delivery scan, not the minute.

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation