A timestamp such as 2026-08-10T09:00:00 becomes ambiguous as soon as it leaves the process that created it. Is it UTC, the sender’s local time, or a local clock value from somewhere else?

Use DateTimeOffset in API contracts, messages, and persisted application data when the value identifies one point on the UTC timeline. The offset travels with the clock value, so another system does not need the sender’s machine settings to interpret it.

Put the offset in the contract

Consider an integration event that records when a payment was captured:

public sealed record PaymentCaptured(
    Guid PaymentId,
    DateTimeOffset CapturedAt);

var message = new PaymentCaptured(
    payment.Id,
    timeProvider.GetUtcNow());

With the normal ASP.NET Core JSON settings, the timestamp has an explicit offset:

{
  "paymentId": "ae385a6b-50c3-49b1-8b09-a9e38426bc18",
  "capturedAt": "2026-08-10T07:00:00+00:00"
}

System.Text.Json reads and writes DateTimeOffset values using its extended ISO 8601 profile. A value such as 2026-08-10T09:00:00+02:00 identifies the same instant as 2026-08-10T07:00:00+00:00. A receiving service can compare either value or normalize it with ToUniversalTime() without guessing where it came from.

A UTC DateTime can work, but the type does not require UTC

DateTime is not automatically wrong. A value whose Kind is DateTimeKind.Utc also identifies an unambiguous instant.

If a contract only says DateTime, the type does not require the producer to supply UTC. The same property can carry Utc, Local, or Unspecified values. An Unspecified value can reach JSON without a UTC marker or offset and leave the next system to invent that missing context.

If an existing boundary strictly accepts and emits UTC DateTime values, and validates that rule, there is little value in changing it for style alone. For a new boundary, DateTimeOffset makes the offset part of the value instead of relying on a separate convention.

An offset is not a time zone

DateTimeOffset does not contain a time-zone identifier or daylight-saving rules. 2026-08-10T09:00:00+02:00 tells you its offset from UTC for that instant. It does not tell you whether the value came from Europe/Berlin or another place with the same offset on that date.

That distinction matters for future local schedules. “Run every day at 09:00 in Europe/Berlin” needs a local time and the Europe/Berlin time zone. The applicable offset can change before the next run. Once you resolve a particular occurrence to an instant, DateTimeOffset is a suitable value to pass to another system.

Use DateOnly when the value is a calendar date such as a birthday. Use TimeProvider.GetTimestamp() and GetElapsedTime() when measuring elapsed time. Those values answer different questions and should not be squeezed into a timestamp type.

Use DateTimeOffset for fields such as CreatedAt, ReceivedAt, CompletedAt, and ExpiresAt when they identify a specific instant. Normalize to UTC where the next system requires it. If the original time zone matters, store its identifier separately rather than trying to recover it from the offset later.