Fixing "Cannot Write DateTime with Kind=Local" With Npgsql

Fixing "Cannot Write DateTime with Kind=Local" With Npgsql

6 min read··

databasedotnetef-core

Npgsql refuses to write a DateTime whose Kind is Local or Unspecified into a timestamp with time zone column, because since version 6 that type maps strictly to UTC values. A non-UTC Kind does not identify an absolute instant, so the driver reports an error rather than assuming a time zone. The fix is to normalize instants to UTC at the boundary, not to enable the legacy timestamp switch.

Your app works on SQL Server, or on an older Npgsql version. You point it at PostgreSQL with the current stack and the first save throws:

"Cannot write DateTime with Kind=Unspecified to PostgreSQL type 'timestamp with time zone', only UTC is supported."

Or the Kind=Local variant, same exception, different offender. A search turns up a one-line fix, the Npgsql.EnableLegacyTimestampBehavior switch, at the top of every result.

Do not start there. The exception is Npgsql refusing to guess what your ambiguous DateTime means, and the switch tells it to go back to guessing. The real fix is making your DateTime values unambiguous, and it is not much more work.

Why Npgsql 6+ Is Strict

PostgreSQL has two timestamp types, and they mean different things:

  • timestamp with time zone (timestamptz): an absolute instant. Postgres normalizes it to UTC internally; no zone is stored.
  • timestamp without time zone (timestamp): a wall-clock reading with no inherent zone. "2026-07-03 09:00" and good luck knowing where.

.NET's DateTime carries a Kind flag: Utc, Local, or Unspecified. Before Npgsql 6, the driver accepted ambiguous values and could let time-zone assumptions surface later as stored-data bugs.

Since Npgsql 6, the mapping is principled:

  • DateTime with Kind=Utc maps to timestamptz. This is the default mapping for DateTime properties in EF Core with Npgsql.
  • DateTime with Kind=Local or Unspecified maps to timestamp only.
  • Writing a non-UTC Kind to a timestamptz column is an error, the one you are staring at, because interpreting it would require assuming a time zone.

Reading is symmetric: timestamptz comes back as Kind=Utc. The driver is enforcing a simple invariant: absolute instants cross the wire in UTC.

Step 1: Find the Offending Value

The exception tells you the Kind but not the property. Typical sources, starting with the most common boundaries:

  • DateTime.Now anywhere in the codebase. It produces Kind=Local. Grep for it; every hit is a bug in a service that stores instants.
  • JSON deserialization. A payload with "2026-07-03T09:00:00" (no offset) deserializes to Kind=Unspecified. API DTOs are the top source of Unspecified values.
  • Database reads from other systems, CSV imports, and DateTime.Parse without DateTimeStyles.AdjustToUniversal.
  • Values constructed with new DateTime(...) without specifying a kind: Unspecified by default.
  • Entities materialized by other ORMs or Dapper from timestamp columns.

Temporarily enabling sensitive data logging in EF Core query logging shows the parameter values, which usually pinpoints the property fast.

Step 2: Normalize at the Boundary

The durable fix is a rule: inside the application, instants are UTC. Convert at the edges where non-UTC values enter.

Non-UTC DateTime sources (DateTime.Now with Kind Local, JSON input with Kind Unspecified, and new DateTime with Kind Unspecified) all pass through a single boundary conversion to UTC before being written to a timestamptz column

Producing timestamps yourself is the easy case:

// wrong for stored instants
order.CreatedAt = DateTime.Now;

// right
order.CreatedAt = DateTime.UtcNow;

Better still, take time as a dependency with .NET 8's TimeProvider (timeProvider.GetUtcNow()), which also makes the code testable.

API input deserves an explicit contract. If clients send offsets (2026-07-03T09:00:00+02:00), bind to DateTimeOffset and convert:

public record CreateBookingRequest(DateTimeOffset StartsAt);

var booking = new Booking
{
    StartsAtUtc = request.StartsAt.UtcDateTime // Kind=Utc, unambiguous
};

If clients send zoneless wall-clock times, you must know the intended zone and say so in code:

var zone = TimeZoneInfo.FindSystemTimeZoneById("Europe/Oslo");

var utc = TimeZoneInfo.ConvertTimeToUtc(
    DateTime.SpecifyKind(request.StartsAt, DateTimeKind.Unspecified),
    zone);

Values you already know are UTC but arrive as Unspecified (a UTC string without the Z, a read from a legacy timestamp column) just need their Kind stamped:

var utc = DateTime.SpecifyKind(value, DateTimeKind.Utc);

SpecifyKind does not convert; it asserts. Only use it when the assertion is true.

Step 3: Enforce It in the Model

Boundary discipline decays as teams grow, so back it with a model-wide value converter that makes non-UTC values impossible to persist:

public class UtcDateTimeConverter : ValueConverter<DateTime, DateTime>
{
    public UtcDateTimeConverter()
        : base(
            v => v.Kind == DateTimeKind.Utc ? v : v.ToUniversalTime(),
            v => DateTime.SpecifyKind(v, DateTimeKind.Utc))
    {
    }
}

// In your DbContext:
protected override void ConfigureConventions(
    ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<DateTime>()
        .HaveConversion<UtcDateTimeConverter>();
}

One caveat to apply on purpose: ToUniversalTime() on an Unspecified value assumes the server's local zone, which is only correct if that assumption is. If you would rather fail loudly than guess, throw in the converter for Unspecified instead of converting. On containers pinned to UTC the distinction rarely bites, but decide, do not drift.

Registering converters model-wide like this is the same technique from custom model conventions in EF Core.

What About the Legacy Switch?

The escape hatch exists:

AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);

It restores pre-6 behavior: no Kind validation, timestamptz values read back as Kind=Local, and every ambiguity silently accepted. Legitimate use: a large legacy codebase mid-migration to Postgres, where you need the app running while you clean up call sites incrementally. Set it, file the tech-debt issue, and remove it when the converter and boundary fixes land.

What it is not: a fix. The exception was the only thing standing between you and timestamps that shift by your UTC offset depending on which server wrote them.

Choosing Types Going Forward

  • Instants (created-at, occurred-at, expires-at): DateTime in UTC or DateTimeOffset, column timestamptz. Both work with Npgsql; DateTimeOffset must have offset zero on write, and the offset is not stored. I compared the two types in DateTime vs DateTimeOffset in C#.
  • Wall-clock values (a clinic's opening hour, a scheduled local delivery slot): timestamp column via HasColumnType("timestamp without time zone") with Unspecified Kind, plus a separate time zone id column. Forcing these to UTC destroys information; they are not instants.
  • Dates and times alone: DateOnly and TimeOnly map cleanly to date and time with Npgsql, and dodge the whole Kind circus.

If you are earlier in your Postgres journey, the setup fundamentals are in getting started with EF Core and PostgreSQL, and this error is worth solving properly before data accumulates, because rewriting mis-zoned historical timestamps later is somewhere between painful and impossible, the kind of surgery that needs a carefully staged migration.

Summary

The exception is Npgsql enforcing a good invariant: timestamptz holds absolute instants, and absolute instants must arrive as UTC. A DateTime with Kind=Local or Unspecified is a question, not an answer, and since version 6 the driver refuses to answer it for you.

Fix the sources: UtcNow (or TimeProvider) for generated timestamps, explicit offset or zone handling for external input, SpecifyKind only where UTC is already a fact. Back it with a model-wide UTC converter so the rule enforces itself, and reserve the legacy switch for migration bridges with an expiry date.

Get the invariant in place early. Every week it waits, more ambiguous timestamps land in your tables, and unlike code, stored data does not get fixed by a redeploy.

Frequently Asked Questions

Why does Npgsql throw cannot write DateTime with Kind=Unspecified or Local?

Since Npgsql 6, the PostgreSQL timestamptz type maps strictly to UTC DateTime values. Writing a DateTime whose Kind is Local or Unspecified would be ambiguous, so Npgsql rejects it instead of guessing the time zone.

Should I enable Npgsql.EnableLegacyTimestampBehavior?

Only as a temporary bridge while migrating an old codebase. The legacy switch restores the pre-6 behavior of writing whatever DateTime it gets without validation, which reintroduces silent time zone bugs. New code should normalize to UTC instead.

What is the difference between timestamp and timestamptz in PostgreSQL?

timestamp stores a wall-clock date and time with no time zone meaning. timestamptz stores an absolute instant, normalized to UTC internally. Npgsql maps UTC DateTimes to timestamptz and non-UTC Kinds to timestamp.

How do I fix the error for DateTime values from an API or form?

Values parsed from JSON or user input usually arrive with Kind=Unspecified. Convert them to UTC at the boundary: interpret them in the correct source time zone with TimeZoneInfo, or require offsets in the API contract and accept DateTimeOffset instead.

Does using DateTimeOffset avoid the problem?

Largely, yes. Npgsql maps DateTimeOffset to timestamptz and requires offset zero on write, converting is trivial and unambiguous since any DateTimeOffset identifies an exact instant. The offset itself is not stored, timestamptz keeps only the UTC instant.

Loading comments...

Whenever you're ready, there are 4 ways I can help you:

  1. Pragmatic Clean Architecture: Join 5,000+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
  2. Modular Monolith Architecture: Join 2,800+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
  3. Pragmatic REST APIs: Join 1,900+ students in this course that will teach you how to build production-ready REST APIs using the latest ASP.NET Core features and best practices. It includes a fully functional UI application that we'll integrate with the REST API.
  4. Patreon Community: Join a community of 5,000+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.

The .NET Weekly

Become a Better .NET Software Engineer

Join 66,000+ engineers who are improving their skills every Saturday morning.