RabbitMQ
SQL Server
Messaging Systems
Database Management
Tech Tutorials

How to post messages to RabbitMQ from SQL Server?

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Posting messages to RabbitMQ from SQL Server is a common requirement when a database update must trigger work in another system. The key design decision is where message publishing should happen. You can do it inside SQL Server, but most production systems are safer when SQL only records events and a separate worker publishes to RabbitMQ.

Choose an Integration Pattern

There are three practical patterns:

  1. SQL CLR stored procedure that publishes directly to RabbitMQ.
  2. Service Broker plus an external activator process.
  3. Outbox table written in the same transaction, with a worker process that publishes.

The outbox pattern is usually the most reliable because SQL transactions remain local, network failures do not block writes, and retries are easy to manage. Direct publishing from SQL can work for small systems, but operational risk is higher.

Implement the Outbox Pattern in SQL Server

Create an outbox table that stores pending messages. Insert into business tables and outbox in one transaction.

sql
1CREATE TABLE dbo.OrderOutbox (
2    Id BIGINT IDENTITY(1,1) PRIMARY KEY,
3    EventType NVARCHAR(100) NOT NULL,
4    RoutingKey NVARCHAR(200) NOT NULL,
5    Payload NVARCHAR(MAX) NOT NULL,
6    CreatedAt DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
7    PublishedAt DATETIME2 NULL,
8    RetryCount INT NOT NULL DEFAULT 0,
9    LastError NVARCHAR(1000) NULL
10);
11GO
12
13CREATE TABLE dbo.Orders (
14    OrderId INT IDENTITY(1,1) PRIMARY KEY,
15    CustomerId INT NOT NULL,
16    Amount DECIMAL(12,2) NOT NULL,
17    CreatedAt DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME()
18);
19GO
20
21CREATE OR ALTER PROCEDURE dbo.CreateOrder
22    @CustomerId INT,
23    @Amount DECIMAL(12,2)
24AS
25BEGIN
26    SET NOCOUNT ON;
27
28    BEGIN TRAN;
29
30    INSERT INTO dbo.Orders (CustomerId, Amount)
31    VALUES (@CustomerId, @Amount);
32
33    DECLARE @OrderId INT = SCOPE_IDENTITY();
34
35    INSERT INTO dbo.OrderOutbox (EventType, RoutingKey, Payload)
36    VALUES (
37        N'OrderCreated',
38        N'orders.created',
39        CONCAT(
40            N'{"orderId":', @OrderId,
41            N',"customerId":', @CustomerId,
42            N',"amount":', CONVERT(NVARCHAR(50), @Amount),
43            N'}'
44        )
45    );
46
47    COMMIT TRAN;
48END;
49GO

This gives atomicity: if the order insert fails, the outbox row is not created. If commit succeeds, both are persisted.

Build a Publisher Worker in C#

Run a separate .NET worker that polls pending rows, publishes to RabbitMQ, and marks rows as published. This process can run as a Windows service, container, or scheduled job.

csharp
1using System.Text;
2using Microsoft.Data.SqlClient;
3using RabbitMQ.Client;
4
5var sql = "Server=localhost;Database=SalesDb;Trusted_Connection=True;TrustServerCertificate=True";
6var factory = new ConnectionFactory { HostName = "localhost" };
7
8using var rabbitConn = factory.CreateConnection();
9using var channel = rabbitConn.CreateModel();
10channel.ExchangeDeclare("app.events", ExchangeType.Topic, durable: true);
11
12while (true)
13{
14    using var conn = new SqlConnection(sql);
15    await conn.OpenAsync();
16
17    var selectCmd = new SqlCommand(@"
18        SELECT TOP (50) Id, RoutingKey, Payload
19        FROM dbo.OrderOutbox WITH (READPAST)
20        WHERE PublishedAt IS NULL
21        ORDER BY Id", conn);
22
23    using var reader = await selectCmd.ExecuteReaderAsync();
24    var rows = new List<(long Id, string Key, string Body)>();
25
26    while (await reader.ReadAsync())
27    {
28        rows.Add((reader.GetInt64(0), reader.GetString(1), reader.GetString(2)));
29    }
30
31    foreach (var row in rows)
32    {
33        try
34        {
35            var body = Encoding.UTF8.GetBytes(row.Body);
36            var props = channel.CreateBasicProperties();
37            props.Persistent = true;
38
39            channel.BasicPublish("app.events", row.Key, props, body);
40
41            var markCmd = new SqlCommand(
42                "UPDATE dbo.OrderOutbox SET PublishedAt = SYSUTCDATETIME() WHERE Id = @id", conn);
43            markCmd.Parameters.AddWithValue("@id", row.Id);
44            await markCmd.ExecuteNonQueryAsync();
45        }
46        catch (Exception ex)
47        {
48            var retryCmd = new SqlCommand(@"
49                UPDATE dbo.OrderOutbox
50                SET RetryCount = RetryCount + 1,
51                    LastError = @err
52                WHERE Id = @id", conn);
53            retryCmd.Parameters.AddWithValue("@id", row.Id);
54            retryCmd.Parameters.AddWithValue("@err", ex.Message);
55            await retryCmd.ExecuteNonQueryAsync();
56        }
57    }
58
59    await Task.Delay(TimeSpan.FromSeconds(2));
60}

This loop is intentionally simple. In production, add cancellation tokens, structured logs, and metrics.

When Direct SQL Publishing Is Acceptable

If throughput is low and you control the environment, SQL CLR can be acceptable for internal tools. Service Broker plus external activator can also work if your team already uses Service Broker. The tradeoff is operational complexity. Database teams often prefer keeping SQL focused on persistence and moving broker communication into application services.

Common Pitfalls

  • Publishing inside a trigger and slowing down writes when RabbitMQ is slow.
  • No idempotency on consumers, causing duplicate side effects during retries.
  • Marking rows as published before broker confirmation.
  • Unlimited retries with no dead letter handling strategy.
  • Building JSON payloads by string concatenation without validating payload shape.

Summary

  • Use an outbox table when you need reliability and clear failure handling.
  • Keep SQL transactions local and move broker publishing to a worker service.
  • Publish with durable messages and track retries plus last error.
  • Mark events as published only after successful broker publish.
  • Add consumer idempotency and dead letter policies for end to end safety.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.