PostgreSQL transport provider for SlimMessageBus
July 5, 2026 ยท View on GitHub
Please read the Introduction before reading this provider documentation.
About
The PostgreSQL transport provider allows a shared PostgreSQL database to act as the message broker for collaborating producers and consumers.
This transport is useful for applications that already operate PostgreSQL and do not need a dedicated messaging broker yet.
Configuration
Install the transport package:
dotnet add package SlimMessageBus.Host.PostgreSql
The configuration is arranged via the .WithProviderPostgreSql(cfg => {}) method on the message bus builder.
using SlimMessageBus.Host.PostgreSql;
services.AddSlimMessageBus(mbb =>
{
mbb.WithProviderPostgreSql(cfg =>
{
cfg.ConnectionString = "...";
cfg.DatabaseSchemaName = "smb";
cfg.DatabaseTableName = "messages";
cfg.PollDelay = TimeSpan.FromMilliseconds(250);
cfg.PollBatchSize = 10;
cfg.LockDuration = TimeSpan.FromSeconds(30);
cfg.MaxDeliveryAttempts = 10;
});
mbb.Produce<PingMessage>(x => x.DefaultQueue("ping-queue"));
mbb.Consume<PingMessage>(x => x.Queue("ping-queue"));
mbb.Produce<OrderSubmitted>(x => x.DefaultTopic("orders").ToTopic());
mbb.Consume<OrderSubmitted>(x => x.Topic("orders", "billing"));
mbb.Consume<OrderSubmitted>(x => x.Topic("orders", "shipping"));
mbb.AddServicesFromAssemblyContaining<PingConsumer>();
mbb.AddJsonSerializer();
});
Provider settings
The most commonly configured settings are:
ConnectionString- required PostgreSQL connection string.DatabaseSchemaName- schema containing the transport tables. Defaults topublic.DatabaseTableName- base message table name. The durable subscription table uses the same base name with a_subscriptionssuffix.DatabaseMigrationsTableName- table used to track transport schema migrations.CommandTimeout- optional PostgreSQL command timeout.TransactionIsolationLevel- isolation level used for schema provisioning. Defaults toReadCommitted.PollDelay- delay used when no message is available or after a transient polling error.PollBatchSize- maximum number of messages locked by one polling operation.LockDuration- how long a message lock is held before another consumer may pick it up.MaxDeliveryAttempts- number of processing attempts before a message is marked aborted.NotifyOnPublish- when enabled, producers callpg_notifyafter inserting messages. Polling remains the correctness mechanism.SchemaCreationRetryandOperationRetry- retry settings for schema creation and regular database operations.
PostgreSQL identifiers configured through DatabaseSchemaName, DatabaseTableName, and DatabaseMigrationsTableName are validated and quoted. Use letters, numbers, and underscores, and do not start identifiers with a number.
Queues, topics, and request/response
Use DefaultQueue() and Queue() for competing-consumer queues:
mbb.Produce<PingMessage>(x => x.DefaultQueue("ping-queue"));
mbb.Consume<PingMessage>(x => x.Queue("ping-queue"));
Use DefaultTopic().ToTopic() and Topic(topic, subscriptionName) for durable pub/sub:
mbb.Produce<OrderSubmitted>(x => x.DefaultTopic("orders").ToTopic());
mbb.Consume<OrderSubmitted>(x => x.Topic("orders", "billing"));
mbb.Consume<OrderSubmitted>(x => x.Topic("orders", "shipping"));
Request/response endpoints can also use PostgreSQL queues or topics:
mbb.Handle<PingRequest, PingResponse>(x => x.Queue("ping-handler"));
mbb.ExpectRequestResponses(x => x.ReplyToQueue("replies"));
Message id generation
The transport stores messages with two identifiers:
sequence_id- abigserialphysical key used for insert locality and ordered polling.id- a logicaluuidmessage id used by the transport when completing or failing messages.
By default, PostgreSQL uses PostgreSqlMessageIdGenerationMode.ClientGuidGenerator with PostgreSqlSequentialGuidGenerator, which creates sequential-ish UUIDs client-side for better index locality than random UUIDs.
You can change the id strategy:
mbb.WithProviderPostgreSql(cfg =>
{
cfg.ConnectionString = "...";
cfg.IdGeneration.Mode = PostgreSqlMessageIdGenerationMode.DatabaseRandomUuid;
});
Available modes:
ClientGuidGenerator- client-side UUID generation. Defaults toPostgreSqlSequentialGuidGenerator, butGuidGeneratororGuidGeneratorTypecan be replaced.DatabaseRandomUuid- uses PostgreSQLgen_random_uuid().
How it works
- A messages table stores exchanged messages.
- A subscriptions table stores durable topic subscriptions configured by consumers.
- Queue consumers compete for rows using
FOR UPDATE SKIP LOCKED. - Topic publishes create one row per configured subscription.
- Message rows use a
bigserialphysical key for insert locality and a logicaluuidmessage id. - The default client-side id generator is sequential-ish for index locality. Random database ids can be selected through
cfg.IdGeneration. - Producers optionally call
pg_notifyafter inserting messages. Polling remains the correctness mechanism.
Polling and locking
Consumers poll the shared message table in batches. PostgreSQL uses FOR UPDATE SKIP LOCKED so competing consumers can skip rows already locked by another instance.
When a consumer locks a row, the transport stores the consumer instance id and lock expiration. If the process stops before completing the message, the row becomes visible again after LockDuration.
pg_notify is used as a lightweight wake-up hint when NotifyOnPublish is enabled. It is not used as the durable delivery mechanism; message rows in the database remain the source of truth.
Retries and failed messages
Successful processing marks the row as complete. Failed processing increments delivery_attempt, clears the lock, and makes the row available for another attempt. Once MaxDeliveryAttempts is reached, the row is marked aborted and will no longer be delivered.
The transport retries transient PostgreSQL errors around schema provisioning and operations according to SchemaCreationRetry and OperationRetry.
Schema provisioning
The provider provisions the required message, subscription, and migration tables during bus startup. All cooperating services should use the same database, schema, and table names.
Testing locally
The integration tests use Testcontainers and require Docker to be running:
dotnet test src/Tests/SlimMessageBus.Host.PostgreSql.Test/SlimMessageBus.Host.PostgreSql.Test.csproj --filter "Category=Integration"
The repository also contains infrastructure.ps1 for standing up shared development infrastructure used by broader integration test runs.