Skip to the content.

PostgreSQL transport provider for SlimMessageBus

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:

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:

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:

How it works

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.