Modern .NET Development
Date, Time and Time Zones in .NET Without the Usual Bugs
“Store UTC” is useful but incomplete. Reliable temporal software distinguishes instants, local calendar values, elapsed durations, offsets, time-zone rules and recurring schedules before choosing a .NET type.

Time bugs begin by modelling different things as DateTime
An audit event happened at one instant. A birthday is a calendar date with no timezone. A shop opens at a local time. A meeting at 09:00 Europe/London maps to an instant using time-zone rules. A timeout is an elapsed duration and should not depend on the wall clock.
These values are not interchangeable. Before choosing DateTime, DateTimeOffset, DateOnly, TimeOnly or TimeSpan, name the business meaning. Conversion bugs often indicate that meaning was never decided.
Choose a representation by meaning
| Meaning | Typical .NET representation | Store |
|---|---|---|
| Unique instant | DateTimeOffset or UTC instant | UTC timestamp, optionally source offset |
| Calendar date | DateOnly | Date column |
| Local time of day | TimeOnly | Time column |
| Elapsed duration | TimeSpan / monotonic measurement | Duration units |
| Future zoned event | Local date/time + zone ID | Both, plus calculated instant where useful |
| Recurring schedule | Rule + zone ID | Rule, zone and next occurrence |
An offset is not a time zone. +00:00 does not say whether the user is in London, Reykjavik or another region, and it cannot calculate future daylight-saving changes.

UTC solves ordering, not local intent
Store event instants in UTC so systems can compare and order them consistently. But converting a future appointment to UTC and discarding its zone loses the original intent. If time-zone rules change before the appointment, should it remain at 09:00 local or at the previously calculated instant? Most human schedules intend the former.
Store local date/time and an IANA or Windows zone identifier for future appointments and recurrence. A derived UTC instant can support queries, but it may need recalculation after time-zone database updates. Record the source offset when audit or legal evidence requires what the user saw at entry.
DateTime.Kind is metadata, not validation
DateTime can be UTC, local or unspecified. APIs and serializers may infer conversions from Kind, while database round trips may lose it. An unspecified value is not automatically wrong; it is appropriate for a wall-clock value that has not yet been interpreted in a zone.
Do not call ToUniversalTime on an unspecified value until you know which zone owns it; the method may assume the server's local zone. Servers should generally run with an explicit predictable timezone, but correctness must not depend on machine locale.
DateTimeOffset identifies an instant and remembers one offset. Two values with different offsets can represent the same instant. It still does not preserve the region's future rules.
Daylight-saving transitions create invalid and ambiguous times
When clocks move forward, some local times never occur. When clocks move backward, a local time can occur twice with different offsets. Scheduling 01:30 without a policy is incomplete in affected zones.
Validate local input against the chosen zone. For invalid times, ask the user to choose another value or apply a documented shift policy. For ambiguous times, request or choose the earlier/later occurrence explicitly and preserve the selected offset. Silent framework defaults turn twice-yearly ambiguity into intermittent production defects.
Not every zone uses a one-hour change, and transition rules change politically. Never hard-code “add one hour for daylight saving.” Use maintained time-zone data.
Windows and IANA zone IDs are an integration boundary
Windows and Linux ecosystems historically use different identifiers. Modern .NET provides cross-platform support in many cases, but databases, browsers and external APIs may still use different namespaces. Decide on a canonical stored identifier and map only at boundaries.
Do not store display names such as “British Time”; they are localised and ambiguous. Validate zone IDs at input and define behaviour when an identifier is unknown on a host. Container base-image timezone data must be updated like any other dependency.
Durations need a monotonic clock
Subtracting wall-clock timestamps to measure request duration can fail when the system clock is corrected. Use a monotonic source such as Stopwatch or framework timing instrumentation for elapsed time. Wall clocks answer “when”; monotonic clocks answer “how long.”
Inject TimeProvider into code that needs current time, timers or delays. Tests can control time without sleeping, and production code stops scattering DateTime.UtcNow. Capture “now” once per business decision so a boundary does not move between comparisons.
API formats must include the intended semantics
Use ISO 8601/RFC 3339-style timestamps with an explicit Z or offset for instants. Reject or clearly define offset-less timestamps at public boundaries. A string ending in Z is UTC; a local appointment also needs its zone identifier if future local intent matters.
Dates should be date-only strings, not midnight UTC timestamps. Midnight converted westward becomes the previous calendar day. Durations need documented units or an unambiguous duration format; an integer named timeout invites milliseconds/seconds mistakes.
Preserve enough precision for the domain and database. Round deliberately. Equality failures caused by one store truncating fractional seconds should be prevented at mapping or comparison boundaries.
Database types can erase meaning
Select columns that match instant, date, time and offset semantics. SQL Server datetimeoffset preserves an offset; a UTC datetime2 convention can store instants but does not enforce Kind. PostgreSQL timestamp types have their own semantics. Test the actual provider round trip.
A database default and application clock can disagree. Choose which clock is authoritative for created timestamps and concurrency. Database-generated audit instants may be useful; domain-effective dates usually come from explicit business input.
Recurrence is not repeated duration
“Every day at 09:00 local” cannot be implemented as add 24 hours to the previous instant across daylight-saving transitions. Recalculate each occurrence from the local rule and zone. “Every 24 hours” is a different valid requirement.
Store the recurrence rule, zone, start/end conditions and policy for invalid or ambiguous occurrences. Materialise a bounded future window for querying and recalculate when rules or schedules change. Never generate an infinite series inside a request.
Test the boundaries the calendar supplies
- Before, during and after spring-forward and fall-back transitions.
- Ambiguous and invalid local input under the chosen policy.
- Month end, leap day and year boundary.
- Zones with non-hour offsets and unusual transitions.
- Server running in a different timezone and culture.
- Serialization and actual database precision round trips.
- Time-zone rule update for a future appointment.
- Elapsed timing while wall clock changes.
- Recurrence across DST and missed scheduler runs.
Use fixed clocks for deterministic tests and generated cases across zones. Do not make the suite wait for real time to pass.
Production checklist
- Name the temporal meaning before choosing a type.
- Use UTC for instants, not for date-only values.
- Store zone IDs with future local intent.
- Handle invalid and ambiguous local times explicitly.
- Keep zone identifier mapping at system boundaries.
- Use monotonic time for elapsed duration.
- Inject
TimeProviderand capture now once. - Make API offsets, zones, units and precision explicit.
- Test provider round trips and recurrence transitions.
Related C3 Software guides
Frequently asked questions
Should all dates be stored in UTC?
No. Store instants in UTC. Birthdays and business-effective dates are calendar values and should remain dates.
Is DateTimeOffset a timezone?
No. It identifies an instant with an offset, but does not carry regional rules for future daylight-saving transitions.
How should future appointments be stored?
Preserve local date/time and the zone identifier, with a derived instant where useful for querying.
How should .NET code test time?
Inject TimeProvider for current time and timers, and use monotonic timing for elapsed duration.
Remove temporal ambiguity from business software
C3 Software Limited has built and supported business software in the UK since 2001. If schedules or reports behave differently by region, talk to C3 Software about a focused design review.