Docs / Guides
Storage backends
By default, job and recurring-job state live in memory and are lost on restart. To persist
them, run the chosen backend's Schema.sql against your database, then register
it. Only register one backend — whichever call runs last wins, since they all replace the
same underlying services.
// SQL Server: run Axon.Store.SqlServer/Schema.sql first
var sqlConnectionString = builder.Configuration["Axon:SqlConnectionString"];
if (!string.IsNullOrEmpty(sqlConnectionString))
builder.Services.AddAxonSqlServerStore(sqlConnectionString);
// PostgreSQL: run Axon.Store.Postgres/Schema.sql first
var postgresConnectionString = builder.Configuration["Axon:PostgresConnectionString"];
if (!string.IsNullOrEmpty(postgresConnectionString))
builder.Services.AddAxonPostgresStore(postgresConnectionString);
// MySQL: run Axon.Store.MySql/Schema.sql first
var mySqlConnectionString = builder.Configuration["Axon:MySqlConnectionString"];
if (!string.IsNullOrEmpty(mySqlConnectionString))
builder.Services.AddAxonMySqlStore(mySqlConnectionString);
// SQLite: run Axon.Store.SQLite/Schema.sql first. Single-instance/local-dev only.
var sqliteConnectionString = builder.Configuration["Axon:SQLiteConnectionString"];
if (!string.IsNullOrEmpty(sqliteConnectionString))
builder.Services.AddAxonSQLiteStore(sqliteConnectionString);
// MongoDB: must point at a replica set. No schema to run first.
var mongoConnectionString = builder.Configuration["Axon:MongoConnectionString"];
if (!string.IsNullOrEmpty(mongoConnectionString))
builder.Services.AddAxonMongoDbStore(mongoConnectionString, databaseName: "axon");
Which one to pick
| Backend | Multi-instance safe | Notes |
|---|---|---|
| SQL Server | Yes | UPDLOCK, HOLDLOCK on the claim range. |
| PostgreSQL | Yes | pg_advisory_xact_lock keyed on concurrency group. |
| MySQL | Yes | SELECT ... FOR UPDATE under next-key locking. |
| MongoDB | Yes | Requires a replica set (even single-node) — TryClaimJob's concurrency check runs inside a multi-document transaction, which a standalone server can't open. Managed offerings (Atlas, etc.) are already a replica set. Self-hosted: start mongod --replSet rs0 and run rs.initiate() once. |
| SQLite | No | Single-writer model means it does not support the multi-instance dispatch scenario the other backends target. Use it for a single-instance deployment or local development only. |
Multi-instance safety means: when several Axon.Server instances share one
store, only one of them ever wins the atomic claim on a given job — see
Architecture for how each backend
enforces that.
Redis backplane
If you're running multiple Axon.Server instances behind a load balancer, chain
.AddRedisBackplane(...) so job dispatch and dashboard updates reach a client no
matter which instance's WebSocket it's connected to:
builder.Services.AddAxonServer()
.AddRedisBackplane(builder.Configuration["Axon:RedisConnectionString"]!)
.AddAxonDashboard();
Without this, a client's connection is pinned to whichever instance accepted it, so a job dispatched by instance A never reaches a client connected to instance B. This is a separate concern from storage: dispatch correctness across instances comes from the atomic claim above, regardless of whether a backplane is configured. The backplane is specifically about routing a client's inbound WebSocket traffic to the right instance.
Job data cleanup
By default, completed jobs (Succeeded/Failed/Skipped)
and their history are kept forever. Chain .AddJobCleanup(...) to purge them
after a retention window:
builder.Services.AddAxonServer()
.AddAxonDashboard()
.AddJobCleanup(retention: TimeSpan.FromDays(30));
A background sweep (once an hour by default; override with the pollInterval
parameter) deletes completed jobs whose most recent history entry is older than
retention. Enqueued/Scheduled/Processing/AwaitingParent
jobs are never touched, regardless of age.