If you have ever shipped a feature on a Friday and woken up Monday to a wall of screenshots that all say “Why is my order time wrong”, welcome to the club. Time is the boss battle of backend code. Daylight saving time steals an hour, servers move to new regions, and somewhere a user in London insists they bought sneakers tomorrow. Let’s turn that chaos into boringly correct code.

The sneaky villain hiding in your code

It feels natural to stamp a record with DateTime.Now. It also quietly ties your data to whatever timezone your server is set to. Ship to a new region or container image and your audit trail suddenly speaks a different language.

var serverLocal = DateTime.Now; // Local to the machine
Console.WriteLine(serverLocal.Kind); // Local
var portable = DateTimeOffset.UtcNow; // Stable everywhere
Console.WriteLine(portable.ToString("O")); // ISO 8601 with Z

Use DateTimeOffset.UtcNow when you capture moments in time. It always carries the UTC context.

DateTime vs DateTimeOffset in one minute

  • DateTime can be Local, Utc, or Unspecified. That Kind flag is easy to forget and easy to misuse.
  • DateTimeOffset is a timestamp plus its offset. No guessing. When you say 2026 03 14T16 00 00Z you mean it.

Rule of thumb: capture and persist moments in UTC with DateTimeOffset. Convert for display at the edges.

Model your data the UTC first way

You want one truth in storage and many safe views at the edges. That means UTC in the database and a timezone preference on the user.

public sealed class DundieOrder
{
public Guid Id { get; init; }
public DateTimeOffset CreatedAtUtc { get; init; }
public string UserId { get; init; } = "";
}

When you save, record the moment in UTC. No offsets. No hedging. No guessing.

var order = new DundieOrder
{
Id = Guid.NewGuid(),
CreatedAtUtc = DateTimeOffset.UtcNow,
UserId = currentUserId
};
await db.AddAsync(order);
await db.SaveChangesAsync();

In SQL Server prefer datetimeoffset. In PostgreSQL prefer timestamptz. Both speak UTC fluently.

APIs should speak UTC too

APIs are contracts. Keep them timezone neutral and predictable by returning UTC values. Let the client choose how to show time.

app.MapGet("/orders/{id}", async (Guid id, Db db) =>
{
var o = await db.Orders.FindAsync(id);
return Results.Ok(new { o.Id, createdAtUtc = o.CreatedAtUtc });
});

System.Text.Json will serialize DateTimeOffset as ISO 8601. A trailing Z means UTC.

Users have timezones. Servers do not.

To present a friendly time to a human, convert from UTC using their preferred timezone id. Use IANA ids like Europe/London or America/New_York. On Windows, add the TimeZoneConverter package if you need IANA to Windows mapping.

static DateTime ConvertToUser(DateTimeOffset utc, string tzId)
{
var tz = TimeZoneInfo.FindSystemTimeZoneById(tzId);
return TimeZoneInfo.ConvertTimeFromUtc(utc.UtcDateTime, tz);
}

Example: A user in Dhaka views an order created at 2026 03 14T16 00 00Z and sees 10 00 PM local. The value in storage never changed.

Do not sprinkle TimeZoneInfo everywhere

Keep conversions in one place so they are easy to test and change. Wrap TimeZoneInfo behind a small service.

public interface ITimeZoneService
{
DateTime ToUser(DateTimeOffset utc, string tzId);
DateTimeOffset NowIn(string tzId);
}
public sealed class TimeZoneService : ITimeZoneService
{
public DateTime ToUser(DateTimeOffset utc, string tzId)
{
var tz = TimeZoneInfo.FindSystemTimeZoneById(tzId);
return TimeZoneInfo.ConvertTimeFromUtc(utc.UtcDateTime, tz);
}
}
public sealed class TimeZoneService : ITimeZoneService
{
public DateTimeOffset NowIn(string tzId)
{
var tz = TimeZoneInfo.FindSystemTimeZoneById(tzId);
var now = DateTimeOffset.UtcNow;
return now.ToOffset(tz.GetUtcOffset(now));
}
}

Register once and inject where you present time.

builder.Services.AddSingleton<ITimeZoneService, TimeZoneService>();

Use it in a minimal API without spreading conversions through your code.

app.MapGet("/when", (ITimeZoneService tz, string zone) =>
{
var local = tz.ToUser(DateTimeOffset.UtcNow, zone);
return Results.Ok(new { utc = DateTimeOffset.UtcNow, local });
});

Daylight saving time without tears

Never add or subtract fixed hours for a region. Let TimeZoneInfo evaluate the correct offset for that exact instant.

var utc = new DateTime(2026, 3, 29, 0, 30, 0, DateTimeKind.Utc);
var london = TimeZoneInfo.FindSystemTimeZoneById("Europe/London");
var local = TimeZoneInfo.ConvertTimeFromUtc(utc, london);
Console.WriteLine(local);

On that date the UK jumps forward. TimeZoneInfo will choose the right offset based on historical rules.

Background jobs and schedules

Schedulers often say Run at midnight. If your worker runs in UTC, that is not midnight for anyone. Store schedules in UTC, trigger in UTC, and convert only for status emails and dashboards.

public sealed class JediScheduler
{
private readonly TimeZoneInfo _tz;
public JediScheduler(string tzId) => _tz = TimeZoneInfo.FindSystemTimeZoneById(tzId);
public DateTime NextLocalRun(DateTimeOffset utcNow) =>
TimeZoneInfo.ConvertTimeFromUtc(utcNow.UtcDateTime, _tz).Date.AddDays(1);
}

Make time testable with TimeProvider

Inject TimeProvider so your code does not reach for the system clock during tests. Freeze time in tests and your assertions stop flaking.

public sealed class ClockyService
{
private readonly TimeProvider _time;
public ClockyService(TimeProvider time) => _time = time;
public DateTimeOffset UtcNow() => _time.GetUtcNow();
}

At startup, wire TimeProvider.System. In tests, use a fake provider from Microsoft.Extensions.Time.Testing.

builder.Services.AddSingleton<TimeProvider>(TimeProvider.System);

Quick checklist

  • Store UTC in the database with datetimeoffset or timestamptz
  • Use DateTimeOffset.UtcNow for all captured moments
  • Keep a per user IANA timezone id like Asia/Dhaka
  • Convert only for display using TimeZoneInfo.ConvertTimeFromUtc
  • Do not hardcode offsets like plus 1 or minus 5
  • Return UTC from APIs in ISO 8601 with Z
  • Centralize conversions in a service and inject it

Wrap up

Time is opinionated and global users will expose every shortcut you take. A simple discipline keeps you safe. Capture in UTC, persist in UTC, and convert at the edge with TimeZoneInfo. When London springs forward and New York falls back, your app stays calm and your users keep their clocks.