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:
| Comparison | Result | Why |
|---|---|---|
local 12:01 > utc 12:00 (DateTime operator) | True | Only ticks are compared; Kind is ignored |
DateTime.Compare(local 12:01, utc 12:00) | 1 | Same: Kind is ignored |
DateTimeOffset(12:01 +05:30) > DateTimeOffset(12:00 Z) | False | Compares 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.Unspecifiedand for.DateTimeon aDateTimeOffset(use.UtcDateTimewhen you need UTC). - Never compare a
DateTimefrom one source with aDateTimefrom 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.