SQL Server

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

The Nimbus SQL Server transport stores messages in database tables and delivers them via polling. If your application already uses SQL Server, this is the easiest way to add messaging — no extra broker, no new infrastructure to manage, and HA through your existing SQL Server setup.

Messages are dequeued atomically using DELETE ... OUTPUT with UPDLOCK, READPAST hints, so multiple consumers can drain a queue in parallel without duplicates or blocking.

Installation

dotnet add package Nimbus.Transports.SqlServer

Quick Start

using Nimbus;
using Nimbus.Configuration;
using Nimbus.Transports.SqlServer;

var bus = new BusBuilder()
    .Configure()
    .WithNames("OrderService", Environment.MachineName)
    .WithTransport(new SqlServerTransportConfiguration()
        .WithConnectionString("Server=localhost;Database=Nimbus;Integrated Security=true;")
        .WithAutoCreateSchema())
    .WithTypesFrom(typeProvider)
    .WithAutofacDefaults(container)
    .Build();

await bus.Start();

WithAutoCreateSchema() creates the three required tables (NimbusMessages, NimbusSubscriptions, NimbusDeadLetters) on startup if they don’t exist. It’s idempotent and 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 SqlServerTransportConfiguration()
    .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:

NimbusMessages

CREATE TABLE dbo.NimbusMessages (
    MessageId    UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
    Destination  NVARCHAR(255)    NOT NULL,
    Body         VARBINARY(MAX)   NOT NULL,
    VisibleAfter DATETIME2        NOT NULL,
    ExpiresAt    DATETIME2        NULL
);

CREATE INDEX IX_NimbusMessages_Dequeue
    ON dbo.NimbusMessages (Destination, VisibleAfter)
    INCLUDE (ExpiresAt);

NimbusSubscriptions

CREATE TABLE dbo.NimbusSubscriptions (
    TopicName       NVARCHAR(255) NOT NULL,
    SubscriberQueue NVARCHAR(255) NOT NULL,
    LastSeenAt      DATETIME2     NOT NULL DEFAULT SYSUTCDATETIME(),
    PRIMARY KEY (TopicName, SubscriberQueue)
);

NimbusDeadLetters

CREATE TABLE dbo.NimbusDeadLetters (
    MessageId           UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
    OriginalDestination NVARCHAR(255)    NULL,
    Body                VARBINARY(MAX)   NOT NULL,
    DeliveryAttempts    INT              NOT NULL,
    FailedAt            DATETIME2        NOT NULL DEFAULT SYSUTCDATETIME()
);

Idle Subscription Cleanup

Every multicast event subscriber registers a per-instance row in the subscriptions table. To stop rows (and the messages fanned out to them) piling up when an instance restarts or crashes, each subscription heartbeats while its instance is running, and a periodic sweep deletes subscriptions whose heartbeat has lapsed along with any messages still queued for them:

var bus = new BusBuilder()
    .Configure()
    .WithNames("OrderService", Environment.MachineName)
    .WithTransport(new SqlServerTransportConfiguration().WithConnectionString("...").WithAutoCreateSchema())
    .WithAutoDeleteOnIdle(TimeSpan.FromDays(1)) // Optional, defaults to 4 days, minimum 5 minutes
    .WithTypesFrom(typeProvider)
    .WithAutofacDefaults(container)
    .Build();

Publishers also skip subscriptions whose heartbeat has lapsed, so nothing is written for a subscriber that has gone. All timing uses database time, so clock skew between application hosts doesn’t matter, and every instance runs the sweep safely.

If an instance is paused longer than the idle window, messages published to its subscription in the meantime are lost, and the subscription is recreated when the instance resumes. Raise WithAutoDeleteOnIdle if you rely on instances going quiet for extended periods.

Databases created by earlier versions need a LastSeenAt column on the subscriptions table. WithAutoCreateSchema() adds it automatically; otherwise run:

ALTER TABLE dbo.NimbusSubscriptions ADD LastSeenAt DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME();

Limitations

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

Compatibility

  • ✅ SQL Server 2016+
  • ✅ Azure SQL Database
  • ✅ SQL Server on Linux (Docker)
  • ✅ Microsoft.Data.SqlClient 5.x