Job Store
The job store gives NBatch restart-from-failure capability. It tracks which chunks have been processed, so if a job crashes mid-way, the next run resumes where it left off instead of reprocessing everything. After a successful run the job automatically resets, so the next run starts fresh — a scheduled job reprocesses on every interval.
The job store lives in a separate package — install it alongside the core:
dotnet add package NBatch.EntityFrameworkCore
Enabling the Job Store
var job = Job.CreateBuilder("csv-import")
.UseJobStore(connectionString) // SQL Server (default)
.AddStep("import", step => step
.ReadFrom(reader)
.WriteTo(writer)
.WithChunkSize(100))
.Build();
NBatch will automatically create the required tracking tables (nbatch.jobs, nbatch.steps, nbatch.step_exceptions) if they don’t exist.
Supported Providers
// SQL Server (default)
.UseJobStore(connStr, DatabaseProvider.SqlServer)
// PostgreSQL
.UseJobStore(connStr, DatabaseProvider.PostgreSql)
// SQLite
.UseJobStore(connStr, DatabaseProvider.Sqlite)
// MySQL / MariaDB (.NET 8 & 9 only)
.UseJobStore(connStr, DatabaseProvider.MySql)
The DatabaseProvider enum:
| Value | Provider | Notes |
|---|---|---|
SqlServer | Microsoft SQL Server | Default |
PostgreSql | PostgreSQL via Npgsql | |
Sqlite | SQLite | |
MySql | MySQL / MariaDB via Pomelo | .NET 8 & .NET 9 only |
Note: MySQL support uses the Pomelo.EntityFrameworkCore.MySql provider, which does not yet support .NET 10. NBatch will throw
PlatformNotSupportedExceptionif you selectDatabaseProvider.MySqlon .NET 10.
How It Works
- When a job starts, NBatch creates or updates a job record in the tracking database and marks the run as in-flight.
- Before each chunk is processed, NBatch inserts a step record for the chunk, marked in-flight (presumed failed).
- After each chunk completes, the step record is updated with the real outcome. A chunk whose record was never updated — a process kill or power loss mid-chunk — therefore reads as failed, never as complete: an interrupted chunk is always re-processed, and records can’t be silently lost.
- When the run finishes, the outcome is recorded on the job record.
- On the next run, that outcome decides between reset and resume. The resume position is always the last committed chunk boundary, independent of the chunk size the failed run used — you can safely restart with a different
ChunkSize.
| Previous run | Next run |
|---|---|
| Completed successfully | Starts fresh — step progress resets to the beginning, tasklets run again |
| Failed | Resumes at the failed chunk’s start and retries it; tasklets that already completed are skipped |
| Crashed mid-run | Resumes at the interrupted chunk’s start — the in-flight record reads as failed |
| Cancelled | Resumes; the chunk that was aborted is retried |
Restart Flow
Run 1: Chunk 0 [ok] -> Chunk 1 [ok] -> Chunk 2 [ok] -> Chunk 3 [CRASH]
Run 2: Resumes from Chunk 3 -> Chunk 3 [ok] -> Chunk 4 [ok] -> Done!
Run 3: Previous run succeeded -> starts fresh from Chunk 0
Because a failed run may retry a partially processed chunk, delivery is at-least-once: writers should be idempotent or tolerate re-written items.
Note: Running the same job concurrently (two hosts, or a manual run overlapping a scheduled one) is not supported — both runs would read and advance the same progress.
Schema
All tracking tables are created under the nbatch schema:
| Table | Purpose |
|---|---|
nbatch.jobs | One row per job — name, creation date, last run, last run outcome |
nbatch.steps | One row per chunk processed — step index, items count, errors |
nbatch.step_exceptions | One row per skipped item — item index, message, stack trace |
Upgrading from NBatch 2.x
Version 3 adds a last_run_success column to nbatch.jobs. NBatch adds it automatically (and idempotently) the first time it touches an existing 2.x database. If your database user cannot run DDL, apply it manually — see the CHANGELOG for the per-provider ALTER TABLE statements. The first v3 run against a v2 database resumes (old behavior); auto-reset applies from the next completed run onward.
With Dependency Injection
When using AddNBatch(), you can configure the job store inside the job builder:
builder.Services.AddNBatch(nbatch =>
{
nbatch.AddJob("csv-import", job => job
.UseJobStore(connStr, DatabaseProvider.PostgreSql)
.AddStep("import", step => step
.ReadFrom(reader)
.WriteTo(writer)
.WithChunkSize(100)))
.RunEvery(TimeSpan.FromHours(1));
});
In-Memory Mode (Default)
If you don’t call .UseJobStore(...), NBatch uses an in-memory repository. This is lightweight and suitable for:
- One-off scripts
- Development and testing
- Jobs where reprocessing is acceptable
// No .UseJobStore() -- runs with in-memory tracking
var job = Job.CreateBuilder("simple-job")
.AddStep("work", step => step
.ReadFrom(reader)
.WriteTo(writer))
.Build();
Note: In-memory mode does not persist state between runs. Restarting the application will reprocess all data.
Resetting the Job Store
A successful run resets automatically, so manual resets are rarely needed. To force a failed job to start from scratch instead of resuming (e.g. after fixing bad source data), clear the tracking tables:
-- Clear all tracking data
DELETE FROM nbatch.step_exceptions;
DELETE FROM nbatch.steps;
DELETE FROM nbatch.jobs;
Or drop and recreate the database if using Docker:
docker compose down -v
docker compose up -d
Next: DI & Hosted Service →