DateTime Mistakes That Are Silently Breaking Your C# App

The small habits around UTC, DateTimeOffset, and time zones that quietly cause the hardest bugs to reproduce in production.

August 27, 2026 · Reviewed on September 20, 2026 · 7 min read · José Antunes

DateTime Mistakes That Are Silently Breaking Your C# App

There’s a special kind of bug report that says, “The timestamp is off by a few hours, but only for some users, and only sometimes.”

If you’ve built anything in .NET that touches dates - logs, APIs, schedules, invoices - you already know this bug.

It hides in production for months, then surfaces the moment a customer in Tokyo files a support ticket about an event that “happened in the future.”

The frustrating part? .NET gives you several date-and-time types because “time” is several different problems. An instant, a calendar date, a wall-clock time, a duration, and a future appointment are not interchangeable. These rules make that distinction explicit.

1. Default to UTC for instants, not for every time concept

For an event that already happened, capture an unambiguous instant. DateTimeOffset.UtcNow is a good default:

DateTimeOffset recordedAt = DateTimeOffset.UtcNow;

Avoid using DateTime.Now as a persisted timestamp. It depends on the host's local time zone, so moving a workload between regions can change its meaning.

But “store everything in UTC” is too broad. A birthday is a DateOnly. A store opens at a TimeOnly. “Run at 09:00 Europe/Lisbon every weekday” is a local date/time plus a time-zone identifier and recurrence rule. Converting that future schedule to UTC once can make it wrong after a daylight-saving or government rule change.

Rule of thumb: use UTC instants for events, and preserve civil-time intent when the business rule is expressed in local time.

2. Prefer DateTimeOffset for timestamps

A DateTimeOffset combines a date and time with its offset from UTC:

2026-05-21T14:30:00+02:00
                    ^ offset from UTC

That value identifies one instant. A DateTime with Kind.Unspecified does not.

One important correction: an offset is not a time zone. +02:00 could describe many zones, and it contains no daylight-saving or historical transition rules. For an order placement or log timestamp, DateTimeOffset is usually enough. For a future appointment whose local wall time matters, also store an IANA or Windows time-zone ID, according to the identifiers your deployment supports.

3. Know what DateTimeKind does, and does not do

Every DateTime has a Kind:

  • Utc: the value represents UTC.
  • Local: the value uses the current machine's local zone.
  • Unspecified: the value has no associated zone semantics.

Kind is not a general time-zone identifier. It cannot represent Europe/Lisbon or Asia/Tokyo, and APIs interpret Unspecified differently depending on the operation. Validate the contract at system boundaries instead of attaching a Kind later and hoping it matches the source.

If an API supplies an instant, accept an offset or Z. If it supplies a local civil time, accept the zone separately and define how ambiguous and invalid daylight-saving times are handled.

4. Parse an explicit contract

DateTime.Parse and DateTimeOffset.Parse are useful when intentionally accepting culture-dependent input. They are risky when a machine-to-machine contract has one documented format.

using System.Globalization;

const string format = "yyyy-MM-dd'T'HH:mm:ss.fffffffzzz";

if (DateTimeOffset.TryParseExact(
    input,
    format,
    CultureInfo.InvariantCulture,
    DateTimeStyles.None,
    out var timestamp))
{
    // Use timestamp.
}

Use TryParseExact with InvariantCulture for a fixed wire format. This example requires an explicit numeric offset. If the contract also permits Z or variable fractional precision, list those accepted formats explicitly. Validation should reject a timestamp with no offset when the field is meant to identify an instant.

5. Use the round-trip format deliberately

The "O" format is invariant and preserves the offset for DateTimeOffset, or the Kind representation for DateTime:

DateTimeOffset timestamp = DateTimeOffset.UtcNow;
string wireValue = timestamp.ToString("O", CultureInfo.InvariantCulture);

DateTimeOffset restored = DateTimeOffset.ParseExact(
    wireValue,
    "O",
    CultureInfo.InvariantCulture,
    DateTimeStyles.None);

It is an excellent default for logs and machine-readable text. Two caveats matter:

  • A DateTime with Kind.Unspecified still emits no offset, so formatting cannot repair ambiguous input.
  • Strings with different offsets do not necessarily sort in instant order. Normalize to UTC before relying on lexical ordering, or sort parsed temporal values.

6. Let TimeZoneInfo apply time-zone rules

Manually adding or subtracting hours is not time-zone conversion. Offsets change because of daylight-saving rules, historical changes, and political decisions.

static DateTimeOffset ConvertInstant(
    DateTimeOffset instant,
    TimeZoneInfo destinationZone) =>
    TimeZoneInfo.ConvertTime(instant, destinationZone);

For local input, explicitly check daylight-saving transitions:

static void ValidateLocalTime(DateTime parsedLocalInput, TimeZoneInfo zone)
{
    DateTime localInput = DateTime.SpecifyKind(
        parsedLocalInput,
        DateTimeKind.Unspecified);

    if (zone.IsInvalidTime(localInput))
        throw new ValidationException("That local time does not exist in this time zone.");

    if (zone.IsAmbiguousTime(localInput))
        throw new ValidationException("That local time occurs twice; choose an offset.");
}

TimeZoneInfo uses the time-zone data available on the host. Keep operating-system/container time-zone data current, and test the identifiers on every target platform. A conversion API can apply known rules; it cannot infer which zone the user intended.

7. Use DateOnly and TimeOnly when that is the domain

Since .NET 6, DateOnly and TimeOnly express date-only and time-only values directly:

DateOnly date = DateOnly.ParseExact(
    "2026-05-21",
    "yyyy-MM-dd",
    CultureInfo.InvariantCulture);

TimeOnly openingTime = TimeOnly.ParseExact(
    "14:30:00",
    "HH:mm:ss",
    CultureInfo.InvariantCulture);

This is not just stylistic. A DateTime that is “supposed to” ignore half its data invites accidental conversion and arithmetic. Match these types to database date and time columns where your provider supports that mapping.

8. Use TimeSpan for durations and a monotonic clock for elapsed time

TimeSpan is the right value type for a duration:

TimeSpan timeout = TimeSpan.FromSeconds(30);

Do not measure operational latency by subtracting two wall-clock readings. System time can jump because of synchronization or administrative changes. Use Stopwatch, or TimeProvider timestamps in modern .NET:

long started = timeProvider.GetTimestamp();
await DoWorkAsync();
TimeSpan elapsed = timeProvider.GetElapsedTime(started);

Calendar arithmetic is a separate problem. “Same local time tomorrow” is not always a 24-hour duration across a daylight-saving transition.

9. Inject TimeProvider when code depends on now

TimeProvider is built into .NET 8 and later, and is available to older supported targets through Microsoft.Bcl.TimeProvider. It gives production code a system clock and tests a controllable clock:

public sealed class InvoiceService(TimeProvider timeProvider)
{
    public DateTimeOffset IssuedAt() => timeProvider.GetUtcNow();
}

The Microsoft.Extensions.TimeProvider.Testing package provides FakeTimeProvider. That removes sleeps and “run this test near midnight” tricks from time-dependent tests.

10. Store the meaning, not just a convenient type

Put it together:

  • Store completed-event timestamps as UTC instants, using a database type and driver mapping whose offset/UTC behavior you have verified.
  • Preserve the original offset only when it has business or audit value.
  • Store a local date/time, time-zone ID, and recurrence rules when future wall-clock intent matters.
  • Never infer a user's zone from the server's local zone. An offset alone is not enough to recover it either.
  • Define precision, inclusive/exclusive boundaries, and ambiguous-time behavior in API and database contracts.

The takeaway

Most production time bugs are type errors disguised as timestamp errors. Ask what the value means before choosing the .NET type:

  • Instant: DateTimeOffset, usually normalized to UTC.
  • UTC-only legacy contract: DateTime with Kind.Utc.
  • Calendar date: DateOnly.
  • Wall-clock time: TimeOnly.
  • Duration: TimeSpan.
  • Future local schedule: local date/time plus a time-zone ID and explicit transition policy.
  • Testable current or elapsed time: TimeProvider.

Fix the model, and the 3 AM “why is this timestamp wrong?” pages become much rarer.

Sources