PostgreSQL

Use Nimbus with PostgreSQL as a message transport — no additional broker required

The Nimbus PostgreSQL transport stores messages in database tables and delivers them via polling. If your application already uses PostgreSQL, this is the easiest way to add messaging — no extra broker, no new infrastructure, and works with any managed PostgreSQL service (Supabase, Neon, RDS, Cloud SQL, Azure Database for PostgreSQL).

Messages are dequeued atomically using FOR UPDATE SKIP LOCKED, so multiple consumers can drain a queue in parallel without duplicates or blocking.

Installation

dotnet add package Nimbus.Transports.Postgres

Quick Start

using Nimbus;
using Nimbus.Configuration;
using Nimbus.Transports.Postgres;

var bus = new BusBuilder()
    .Configure()
    .WithNames("OrderService", Environment.MachineName)
    .WithTransport(new PostgresTransportConfiguration()
        .WithConnectionString("Host=localhost;Database=nimbus;Username=postgres;Password=secret")
        .WithAutoCreateSchema())
    .WithTypesFrom(typeProvider)
    .WithAutofacDefaults(container)
    .Build();

await bus.Start();

WithAutoCreateSchema() creates the three required tables (nimbus_messages, nimbus_subscriptions, nimbus_dead_letters) using CREATE TABLE IF NOT EXISTS. It’s safe to leave enabled in production. If you prefer to manage the schema yourself, omit it and create the tables manually using the SQL below.

Configuration

Poll Interval

The default poll interval is 1 second. Reduce it for lower latency at the cost of more database queries:

new PostgresTransportConfiguration()
    .WithConnectionString("...")
    .WithPollInterval(TimeSpan.FromMilliseconds(250))
Poll intervalMessage latencyDB queries/consumer/min
100 ms~100 ms600
250 ms~250 ms240
1000 ms (default)~1 s60
5000 ms~5 s12

Schema

Three tables are required. WithAutoCreateSchema() creates these automatically, or run the SQL manually:

nimbus_messages

CREATE TABLE IF NOT EXISTS nimbus_messages (
    message_id    UUID        NOT NULL,
    destination   TEXT        NOT NULL,
    body          BYTEA       NOT NULL,
    visible_after TIMESTAMPTZ NOT NULL,
    expires_at    TIMESTAMPTZ NULL,
    CONSTRAINT pk_nimbus_messages PRIMARY KEY (message_id)
);

CREATE INDEX IF NOT EXISTS ix_nimbus_messages_dequeue
    ON nimbus_messages (destination, visible_after)
    INCLUDE (expires_at);

nimbus_subscriptions

CREATE TABLE IF NOT EXISTS nimbus_subscriptions (
    topic_name       TEXT NOT NULL,
    subscriber_queue TEXT NOT NULL,
    CONSTRAINT pk_nimbus_subscriptions PRIMARY KEY (topic_name, subscriber_queue)
);

nimbus_dead_letters

CREATE TABLE IF NOT EXISTS nimbus_dead_letters (
    message_id           UUID        NOT NULL,
    original_destination TEXT        NULL,
    body                 BYTEA       NOT NULL,
    delivery_attempts    INTEGER     NOT NULL,
    failed_at            TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT pk_nimbus_dead_letters PRIMARY KEY (message_id)
);

Limitations

  • Polling-based delivery means latency is bounded by the poll interval
  • No large message body store — all payloads stored as BYTEA; use a claim-check pattern for large data

Compatibility

  • PostgreSQL 12+
  • PostgreSQL 9.5+ (requires pgcrypto extension for gen_random_uuid())
  • Supabase, Neon, RDS, Cloud SQL, Azure Database for PostgreSQL
  • Npgsql 9.x