A clinical event happens at 2:30 PM in a hospital in Chicago. Storing that as a FHIR instant sounds trivial and is not. Between converting to UTC, deciding whether to preserve the offset, handling milliseconds, and coping with DST edges, there are three or four places to drop precision or introduce ambiguity. The site's FHIR timestamp inspector verifies the round-trip. For the wider FHIR framing, additional FHIR architecture notes have more.
The Source Cases
- Local time with known timezone (2026-07-11 14:30 America/Chicago)
- Local time with only an offset (2026-07-11 14:30 -05:00)
- Local time with no zone information (bad input)
- Naive datetime from a system that never recorded a zone
Each requires different handling. The last two need policy decisions before conversion.
For the timezone discussion, timezones in clinical timestamps and where they hide is the entry.
The Conversion Path
- Parse the source local time
- Apply the source timezone (from context if not on the string)
- Compute the UTC equivalent
- Emit as
YYYY-MM-DDTHH:MM:SS.sssZorYYYY-MM-DDTHH:MM:SS-05:00
Each step is a place a bug hides. The library you use should handle each explicitly.
The DST Edge
Chicago in March: 2:30 AM does not exist (spring forward). Chicago in November: 1:30 AM exists twice (fall back).
A conversion library that silently picks one interpretation produces silently wrong results. Well-designed libraries surface the ambiguity — they raise or return both interpretations. Handle it explicitly in the calling code.
Precision Preservation
- Second precision:
2026-07-11T14:30:00Z - Millisecond precision:
2026-07-11T14:30:00.123Z - Sub-millisecond precision: not standard FHIR, do not use
If the source has milliseconds, preserve them. Dropping them is a small precision loss that shows up in event ordering.
The Offset vs UTC Choice
Both are legal FHIR:
2026-07-11T19:30:00Z— UTC2026-07-11T14:30:00-05:00— local time with offset
UTC is easier for arithmetic and sorting. Offset preserves the local clock context.
For the storage-side question, storing timestamps as UTC vs local: FHIR-flavored guidance is the entry.
The instant Datatype Specifically
FHIR instant requires second precision or better and a timezone. That is stricter than dateTime.
Every emit into an instant field has to:
- Include seconds (not just YYYY-MM-DDTHH:MM)
- Include a timezone (
Zor+HH:MM/-HH:MM) - Optionally include milliseconds
For the type framing, FHIR date, dateTime, instant: three types, one confusion is the entry.
Round-Trip Verification
Convert local → FHIR instant → back to local. The result should equal the original.
Roundtrip failures indicate:
- DST handling bug
- Precision loss on milliseconds
- Wrong timezone applied
- Silent naive interpretation
Test the conversion library on your specific timezone before shipping.
Library Choices
- Node —
luxonhandles DST and named zones well - Python —
zoneinfo(Python 3.9+) orpytz - Java —
java.time(ZonedDateTime + Instant) - Go —
time.Timewithtime.LoadLocation
All handle FHIR instant emission if you use them correctly. Naive datetime libraries (Python's naive datetime, JavaScript's raw Date) produce bugs.
The Short Version
Parse with an explicit zone. Handle DST edges deliberately. Preserve millisecond precision. Choose UTC or offset per workload. Round-trip test the conversion. Use timezone-aware datetime libraries; naive ones ship bugs.

Sources
- HL7 canonical R4 datatypes chapter covering instant - HL7 canonical R4 datatypes chapter covering instant precision
