# Identity vs Sequence vs HiLo Key Generation in EF Core

> Identity columns are the default, but they only hand you the id after the insert. HiLo assigns ids before SaveChanges, which unlocks setting foreign keys on unsaved object graphs and leaner batch inserts. Here is how all three strategies work and how to pick.

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

Canonical: https://milanjovanovic.tech/blog/ef-core-identity-sequence-hilo

Identity columns and sequences generate the key during the `INSERT`, so the id exists only after `SaveChanges` returns.
HiLo reserves a block of ids from a sequence and assigns them client-side, so an entity has its id the moment you `Add` it.
Use identity by default, and HiLo when your code needs the id before the save.
Sequences match identity's timing but let several tables share one id range.

You build an aggregate in memory: an `Order` with ten `OrderLine` children.
With identity columns, none of those objects has a real id until `SaveChanges` returns, because the database generates the value **during** the insert.

Most of the time EF Core hides this from you.
But the moment you need the id before saving (for an outbox message, a domain event, a log line, a reference from an object EF does not manage), the default strategy becomes a constraint.
Identity, sequence, and HiLo differ in one dimension more than any other: **when the id becomes known**.

![Timeline of id availability across the save pipeline: HiLo knows the id when the entity is added, while sequence and identity only know it after the INSERT executes](https://milanjovanovic.tech/blogs/articles/ef-core-identity-sequence-hilo/id-generation-timing.png)

## Identity: The Default

On SQL Server, an `int` or `long` key maps to an `IDENTITY` column by default.
On PostgreSQL, Npgsql uses `GENERATED BY DEFAULT AS IDENTITY`:

```csharp
public class Order
{
    public long Id { get; set; }   // identity by convention
    public List<OrderLine> Lines { get; set; } = [];
}
```

The id is born inside the `INSERT`.
EF Core appends a `RETURNING` / `OUTPUT` clause to read it back, then runs **relationship fixup**: it patches the real ids into tracked children's foreign keys and inserts them in dependency order.

That machinery is why identity feels free.
It also has consequences:

- Before `SaveChanges`, `order.Id` is `0`. Temporary negative ids exist only inside the change tracker.
- Inserts of parent-child graphs must be ordered (parents first), constraining how EF batches statements.
- Every insert round trip carries the overhead of returning generated values.

For most CRUD workloads, none of this matters.
It starts to matter in outbox and event patterns, where you want to serialize an event containing `order.Id` in the same unit of work, ideally without contorting your code to run after the save. The **transactional outbox** gets much cleaner when ids exist up front.

## Sequence: Identity Timing, More Flexibility

A sequence is a standalone database object, decoupled from any table.
EF Core 7+ supports it directly on SQL Server:

```csharp
modelBuilder.Entity<Order>()
    .Property(o => o.Id)
    .UseSequence("OrderIds");
```

The migration creates the sequence and gives the key column a default constraint of `NEXT VALUE FOR [OrderIds]`, so the database still generates the value during the insert and EF Core reads it back, exactly like identity.
A sequence alone does not give you ids any earlier.
What it does give you:

- **Shared ranges.** Multiple tables can draw from one sequence, guaranteeing ids unique across tables (useful for table-per-concrete-type [**inheritance mappings**](https://milanjovanovic.tech/blog/tph-vs-tpt-ef-core)).
- **No identity semantics on the column.** Plain inserts with explicit ids, no `IDENTITY_INSERT` dance when migrating data.
- **Tunable caching** on the database side (`CACHE 50`) to cut sequence round trips.

Think of sequences as the infrastructure HiLo builds on.

## HiLo: Ids Before SaveChanges

HiLo splits id generation between database and client.
The database sequence hands out **high** values; the application turns each high value into a block of ids and assigns the **low** values itself.

```csharp
modelBuilder.Entity<Order>()
    .Property(o => o.Id)
    .UseHiLo("OrderHiLo");

modelBuilder.Entity<OrderLine>()
    .Property(l => l.Id)
    .UseHiLo("OrderLineHiLo");
```

The migration creates sequences with an increment of 10 (the default block size).
When you `Add` the first entity, EF Core fetches one sequence value and now owns ids, say, 41 through 50, assignable with zero database contact.

The payoff is immediate and visible:

```csharp
var order = new Order();
context.Orders.Add(order);

Console.WriteLine(order.Id); // real, final id. SaveChanges has NOT run.

var outboxMessage = OutboxMessage.From(
    new OrderCreatedEvent(order.Id)); // safe: the id is real

context.Add(outboxMessage);
await context.SaveChangesAsync();
```

Two things just became possible:

- **Foreign keys on unsaved graphs.** You can wire up references between new objects by id, not just by navigation property, including references from things EF does not track (serialized events, cache keys, messages).
- **Leaner batch inserts.** Since ids are known, EF Core sends plain inserts without needing generated keys back, and insert ordering constraints relax. On graphs of thousands of new rows this measurably reduces save time, though for true bulk loads `SqlBulkCopy`-style APIs still win by an order of magnitude, as I showed in [**fast SQL bulk inserts**](https://milanjovanovic.tech/blog/fast-sql-bulk-inserts-with-csharp-and-ef-core).

The costs are mostly aesthetic and operational:

- **Gaps.** An app instance that reserved ids 41-50 and restarted after using 41 discards nine ids. Sequences also produce gaps on rollback. If anyone in your organization believes invoice numbers must be gap-free, ids are the wrong place for that requirement anyway; model document numbers separately.
- **Block size tuning.** Increment 10 means a sequence round trip every 10 inserts per instance. High-throughput insert paths want a bigger block; you set it by configuring the sequence's increment and matching it in `UseHiLo`'s sequence definition.
- **Provider support.** SQL Server and PostgreSQL support HiLo well through their EF providers.

## What About GUIDs?

Client-generated GUIDs solve the same "id before save" problem with zero database coordination:

```csharp
public class Order
{
    public Guid Id { get; set; } = Guid.CreateVersion7();
}
```

Random Version 4 GUIDs fragment clustered indexes badly, which is the historical argument for HiLo.
.NET 9's `Guid.CreateVersion7()` produces time-ordered values that insert nearly sequentially, removing most of that objection.
I compared the index behavior in detail in **GUID v7 in .NET**.

The remaining tradeoff is size and ergonomics: 16 bytes versus 8, and integer ids stay friendlier in URLs, logs, and support conversations.
If you were reaching for HiLo purely to get ids before save, UUIDv7 is the simpler modern answer.
If you want small sequential integers **and** early ids, HiLo remains the only game in town.

## Choosing

- **Default CRUD app, ids never needed pre-save**: identity. Zero configuration, everyone understands it.
- **Ids needed before `SaveChanges`** (outbox, domain events, cross-aggregate references): HiLo for integer keys, UUIDv7 for GUID keys.
- **Heavy graph inserts through EF Core**: HiLo; it strips the returning overhead and ordering constraints. Combine with the batching guidance from [**optimizing bulk database updates**](https://milanjovanovic.tech/blog/optimizing-bulk-database-updates-in-dotnet).
- **Unique ids across several tables, data migrations with explicit ids**: sequence.
- **Distributed id generation across services**: none of these; use UUIDv7 or an id service. Database-coordinated strategies stop at the database boundary.

One migration note: switching strategies on an existing table is a real schema change (dropping identity, creating sequences, seeding the sequence past the current max id).
Plan it like any other [**risky migration**](https://milanjovanovic.tech/blog/ef-core-migrations-best-practices), and rehearse against a production-sized copy.

## Summary

The three strategies answer one question differently: when do you learn the id?
Identity and sequences answer "after the insert", and HiLo answers "the moment you `Add` the entity", because the application owns a reserved block of ids.

That timing difference is not a micro-optimization.
Ids known before `SaveChanges` are what make outbox messages, domain events, and references across unsaved graphs clean to implement, and they let EF Core batch inserts without reading keys back.

Use identity until you feel the constraint.
When you do, reach for HiLo if you value compact integer keys, or UUIDv7 if you value zero coordination.
Both give you the id exactly when the interesting patterns need it: before the save.

## Frequently asked questions

### What is the HiLo pattern in EF Core?

HiLo reserves a block of ids from a database sequence (the high value) and assigns individual ids client-side (the low values). EF Core implements it with UseHiLo. Entities get real ids when added to the context, before SaveChanges runs.

### What is the difference between identity and sequence key generation?

An identity column is owned by one table and generates the value during the insert. A sequence is a standalone database object that the key column draws from through a default constraint, so the timing matches identity, but multiple tables can share one sequence and you can still insert rows with explicit ids.

### Why are my ids not sequential with HiLo?

Each application instance reserves a block of ids, and unused ids from a block are discarded on restart. Gaps are normal with HiLo, and also with identity columns after rollbacks. Never treat database-generated ids as gap-free.

### Does HiLo make inserts faster?

It removes the need for the database to return generated keys during the insert, so EF Core can send plain parameterized inserts in a batch without reading ids back. For large graphs of new entities the difference is measurable, though bulk copy APIs remain much faster for pure bulk loads.

### Should I just use GUIDs instead?

Client-generated GUIDs also give you ids before saving without a database round trip. Random GUIDs fragment indexes, but Version 7 GUIDs are time-ordered and largely fix that. If you do not need small integer keys, UUIDv7 is a strong alternative to HiLo.
