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

> Npgsql throws "Cannot write DateTime with Kind=Unspecified to PostgreSQL type timestamp with time zone" the first time a non-UTC DateTime reaches the database. The legacy switch makes it go away and your timestamps wrong. Normalizing Kind at the boundary makes both go away.

Published: 2026-08-22. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/ef-core-postgresql-datetime-utc-error

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**](https://milanjovanovic.tech/blog/log-sql-queries-ef-core) 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](https://milanjovanovic.tech/blogs/articles/ef-core-postgresql-datetime-utc-error/normalize-boundary.png)

**Producing timestamps yourself** is the easy case:

```csharp
// 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:

```csharp
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:

```csharp
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:

```csharp
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:

```csharp
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**](https://milanjovanovic.tech/blog/ef-core-custom-conventions).

## What About the Legacy Switch?

The escape hatch exists:

```csharp
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**](https://milanjovanovic.tech/blog/ef-core-postgresql-getting-started), 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**](https://milanjovanovic.tech/blog/efcore-migrations-a-detailed-guide).

## 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.
