Image: New York container terminal (Staten Island) north gate by Jim.henderson, licensed under CC0 1.0.
Your audit says the container had one more free day. The carrier's invoice says it did not. Both sides are reading the same gate-out event, and the answers sit exactly one day apart.
That gap is rarely a disagreement about the tariff. Usually it is a truncation: an instant in time became a date in the wrong zone, and every count downstream moved with it. The line responsible is one expression, and it has never thrown an error in its life.
One line, and the day moves
A container event arrives as an instant, normalised to UTC by whoever supplied it. A tariff does not count instants. It counts days on the terminal's local calendar, starting and stopping at local midnight.
Terminal49's documentation states that "Event timestamps are stored and returned in UTC," and that each one should carry a matching IANA timezone for conversion. Their worked example uses a pod_arrived_at of 2022-12-22T07:00:00Z with a pod_timezone of America/Los_Angeles. Rendered locally, that is 2022-12-21T23:00:00 Pacific Standard Time.
Two dates, one event. Call new Date(ts).toISOString().slice(0, 10) and you get the twenty-second. Resolve it in the port's zone and you get the twenty-first. The same defect appears in SQL when a timestamptz column meets DATE() under a UTC session, and in Python when a tz-aware datetime meets .date() with no astimezone call before it.
Move the availability date by one and the whole free time window slides with it. Move the gate-out date by one and the billable count changes directly. On ACL's published tariff, import demurrage on a dry van at Norfolk opens at $135 a day and reaches $245 beyond day twenty, and charges "will commence at 00:01 hours on the first day after expiration of free time." That is the clock your arithmetic has to match, on every box.
Why it survives every test you write
The bug reaches production because it usually cancels itself. When both ends of the window are truncated in UTC under the same offset, the difference between them is right even though neither date is. A fixture built from two clean afternoon timestamps at one port passes, and goes on passing.
The cancellation breaks under four ordinary conditions:
- One end falls within the offset of local midnight. At a US West Coast terminal in winter, every event between 00:00 and 08:00 UTC belongs to the previous local day.
- The two ends come from different feeds. Carrier EDI, a terminal portal and a visibility provider do not agree on whether an offset is included, and a timestamp string without one is a guess.
- The event has no resolvable location. Terminal49 documents this case plainly: when the timezone is null, "Terminal49 takes the timestamp as given from the source and parses it in UTC." If that source was emitting local time, the instant is now wrong by the full offset and nothing downstream can detect it.
- The window crosses a daylight saving transition, which the next section covers.
None of these raise an exception. They return a count that is well formed, plausible, and one lower than the carrier's.
A day is not 86,400 seconds
The second version is arithmetic rather than formatting. Subtract two instants, divide by 86,400,000, take the floor, and you have measured elapsed time instead of counting calendar days.
Those quantities diverge twice a year. United States clocks sprang forward on March 8, 2026 and fall back on November 1, 2026. A five day window containing the March transition runs 119 hours rather than 120. Floor that into days and it returns four. Your audit now believes a free day went unused, and the dispute you raise on it is wrong before the carrier opens the email. November runs the other way and is almost always harmless, which is why the March defect survives the eight months between them.
Calendar days have to be counted on a calendar: render both instants in the terminal's zone, take their civil dates, and count the boundaries between. Hapag-Lloyd's Detention and Demurrage Guide for the USA, dated May 2026, specifies that detention and demurrage apply "on a calendar / continuous day basis after free time has expired." Free time itself often does not. ACL grants Norfolk five business days of import free time before switching to a calendar count once charges begin, so one window carries two counting rules, and the business-day rule needs a holiday list keyed to local dates.
Your timezone database expires
The third version catches teams who got the first two right. Zone rules are data, that data changes, and a build pins it.
Morocco is the live example. The IANA time zone database recorded in its 2026c release that "Morocco plans to move back to permanent UTC, without daylight saving time transitions, on 2026-09-20 at 02:00," a change the tz mailing list traced to decree 2.26.530. Africa/Casablanca governs Tanger Med. An image built before that release resolves terminal local time an hour off for every container handled since September 20, enough to flip the date on any event near midnight.
Canada contributed three more inside the same year: 2026b carried British Columbia to permanent UTC-07, 2026c carried Alberta to permanent UTC-06 after a final clock change on March 8, and 2026d added the Northwest Territories. A box discharged at a coastal terminal and billed at a rail ramp runs on two clocks at once, and ACL's tariff starts rail ramp counting on the first business day after grounding rather than after vessel discharge.
Pinning tzdata is correct engineering. Pinning it and never advancing it is slow data corruption with no error message attached.
Where the wrong day lands
None of this would matter if the number stayed inside your system. Under 46 CFR 541.6, a demurrage or detention invoice has to state the allowed free time in days, the start and end dates of that free time, and the specific dates charged. Dispute one and you file a competing version of those exact fields, under your company's name.
Section 541.7 adds a second date that has to be right. The billing party has 30 calendar days from the date the charge was last incurred to issue its invoice, and missing that window removes the obligation to pay. Get the zone wrong on a charge incurred late in the terminal's evening and a late invoice reads as timely, or a timely one reads as late and you short pay something you owe. We have written about why the day count never comes from a language model; this is the same principle one layer down.
Counting the day correctly means replacing six defaults:
| What the code usually does | What the tariff requires |
|---|---|
| Truncates the UTC instant to a date | Renders the instant in the terminal's IANA zone, then takes the civil date |
| Applies one zone across the whole shipment | Resolves a zone per event, because the discharge terminal and the rail ramp differ |
| Divides elapsed milliseconds by 86,400,000 | Counts the midnight boundaries crossed between two civil dates |
| Accepts an offset-free timestamp as UTC | Treats an offset-free timestamp as missing data |
| Freezes zone rules at image build | Tracks tzdata as a dependency with a release cadence |
| Takes one feed as the source of the gate-out | Corroborates the event across two independent sources before counting it |
Penny is never handed a date. She is handed an instant, the zone it resolved in, and the source that supplied it, and the count comes back from an engine applying the governing tariff's own calendar. Chase contributes the discharge, availability and gate events, which is why the zone travels attached to the event rather than being inferred later from a port code.
What to do next
Three checks, none of which need a vendor in the room:
- Pull ten containers whose last free day fell within two hours of local midnight at the terminal. Compare your count against the carrier's, line by line.
- Re-run a week of counts from early March and early November. Any window spanning the transition that returns a day short is the floor-divide defect.
- Ask which timestamps in your warehouse carry an offset. The ones that do not are being guessed at.
The first check usually finds something. The third usually explains it.
Book a walkthrough to see Penny recompute a month of your demurrage invoices against the terminal's own clock, event by event.
