← Back to Blog

DateTime vs DateTimeOffset in .NET: Comparison Pitfalls

DateTime comparison ignores Kind, so Local, Utc and Unspecified compare by ticks. Tested examples, the Unix-timestamp trap, and the DateTimeOffset fix.

Pankaj Kumar
Senior Software Engineer — .NET, Blazor, ASP.NET Core
Articles are drafted with AI assistance and reviewed by the author. Code samples and results are run before publication, and each article says how its results were produced.
Published 01 Jun 2026· Last reviewed 14 Oct 2026· 7 min read · About the author →
All results below were produced on .NET 8.0.31 on a machine set to India Standard Time (UTC+05:30), on 14 Oct 2026. Results that depend on the machine's time zone are labelled.
Key takeaways
  • DateTime comparison operators compare raw ticks and ignore DateTimeKind, so a Local value can compare 'greater' than a UTC value that is hours later
  • DateTimeOffset.FromUnixTimeSeconds(x).DateTime returns Kind=Unspecified; use .UtcDateTime to get Kind=Utc
  • ToUniversalTime() treats Unspecified as Local, and ToLocalTime() treats Unspecified as UTC, so the result depends on the machine's time zone
  • DateTimeOffset compares the actual instant and has no Kind property; it carries an offset instead
  • Store and compare UTC instants, and inject TimeProvider so time-based logic can be tested
Table of Contents

The core problem: comparison ignores Kind

A DateTime has a Kind property (Local, Utc or Unspecified), but the comparison operators and DateTime.Compare look only at the raw tick count. They do not convert anything. So two values that represent very different real moments can compare in a misleading order.

Test setup: a due time of 12:00 UTC and a local-kind value of 12:01. The results below are from a machine set to UTC+05:30, where 12:01 local time is actually 06:31 UTC, five and a half hours before the due time:

ComparisonResultWhy
local 12:01 > utc 12:00 (DateTime operator)TrueOnly ticks are compared; Kind is ignored
DateTime.Compare(local 12:01, utc 12:00)1Same: Kind is ignored
DateTimeOffset(12:01 +05:30) > DateTimeOffset(12:00 Z)FalseCompares the instants: 06:31 UTC vs 12:00 UTC

The DateTime version says the due time has already passed. The DateTimeOffset version correctly says it has not. On a server whose local zone is UTC the two would agree, which is exactly why this kind of bug can pass on a cloud host and fail on a developer machine, or the reverse.

Trap: the Unix timestamp conversion

APIs often give you a Unix timestamp in seconds. Converting it looks harmless, but the property you pick on the result matters. Using the timestamp 1753084800 (which is 2025-07-21 08:00:00 UTC):

var offset = DateTimeOffset.FromUnixTimeSeconds(1753084800);

offset.DateTime.Kind     // Unspecified   (not Utc!)
offset.UtcDateTime.Kind  // Utc

It is easy to assume .DateTime gives you a UTC value. It does not: it drops the offset and returns Kind=Unspecified. Use .UtcDateTime if you need a DateTime, or better, keep the DateTimeOffset and never lose the offset.

Unspecified is interpreted differently by different methods

An Unspecified value carries no zone, so the conversion methods have to guess, and they guess in opposite directions. On the UTC+05:30 test machine, for a value of 12:00 with Kind=Unspecified:

  • ToUniversalTime() returned 06:30 UTC, treating it as local time.
  • ToLocalTime() returned 17:30, treating it as UTC.

The same code on a server in a different zone gives different numbers. That machine-dependence is the underlying danger of Unspecified.

Where Unspecified usually comes from

A common source is the database. SQL Server's datetime2 column stores no Kind and no offset, so a DateTime read back through EF Core normally has Kind=Unspecified, even if you saved a UTC value. If you must keep DateTime properties, tell EF Core that the stored values are UTC with a value converter (this compiles on EF Core 8; it was not run against a SQL Server database):

modelBuilder.Entity<Invoice>()
    .Property(i => i.DueDateUtc)
    .HasConversion(
        toDb   => toDb,
        fromDb => DateTime.SpecifyKind(fromDb, DateTimeKind.Utc));

This only labels the value; it is correct only if you really store UTC. The cleaner option is to use DateTimeOffset for these properties, which EF Core maps to SQL Server's datetimeoffset column and which keeps the offset.

The fix: compare instants with DateTimeOffset

public bool IsOverdue(long dueUnixSeconds)
{
    var dueAt = DateTimeOffset.FromUnixTimeSeconds(dueUnixSeconds);
    return DateTimeOffset.UtcNow > dueAt;   // instant vs instant: no time-zone ambiguity
}

DateTimeOffset has no Kind property (confirmed by reflection); it stores a date, a time and the offset from UTC, and comparisons use the underlying UTC instant. It does not store a time-zone name, so it is right for "this exact moment" (due dates, created-at, expiry) but not for "every day at 9 am in London", which needs TimeZoneInfo.

Make the clock injectable with TimeProvider

Time-based logic is hard to test when it calls DateTime.Now directly. .NET 8 added TimeProvider, so you can inject the clock and control it in tests:

public class OverdueChecker
{
    private readonly TimeProvider _time;
    public OverdueChecker(TimeProvider time) => _time = time;

    public bool IsOverdue(DateTimeOffset dueAt) => _time.GetUtcNow() > dueAt;
}

// production: services.AddSingleton(TimeProvider.System);

Audit checklist

  • Search your code for DateTime.Now; for anything that is stored, compared or sent to another system, use a UTC instant instead.
  • Search for DateTimeKind.Unspecified and for .DateTime on a DateTimeOffset (use .UtcDateTime when you need UTC).
  • Never compare a DateTime from one source with a DateTime from another without knowing both Kinds.
  • Check what instant an external timestamp really represents with the Timestamp Converter before relying on it.

Update, 14 Oct 2026: an earlier version of this article told the topic as a first-person production incident, and it contained incorrect statements (for example about which Kind a Unix-timestamp conversion returns). That story was not a real event; the article has been rewritten and every result above was re-run.

Try the free tool

Timestamp Converter

Convert Unix timestamps to human-readable dates and back. Useful for checking what instant a timestamp from an API really represents.

Open Timestamp Converter