SQL Server Service Broker: What It Is, How It Works, and How to Use It

SQL Server Service Broker: What It Is, How It Works, and How to Use It – SQLYARD

SQL Server Service Broker: What It Is, How It Works, and How to Use It


Service Broker sits inside SQL Server, available since SQL Server 2005, and most DBAs have never used it intentionally. You have used it without knowing it because Database Mail, Service Broker, and several internal SQL Server features run on top of it. But building your own Service Broker implementations, understanding what queues, services, contracts, and routes actually are, and knowing how to troubleshoot when messages stop flowing is knowledge most DBAs never acquire.

This article changes that. It covers every component in plain English with the postal service analogy that makes the architecture stick, walks through complete working examples including the DDL schema change logging pattern, SQL Agent error alerting, and async trigger processing, and gives you the diagnostic queries and health checks to know whether your Service Broker setup is working or silently broken.

SQL Server Architecture

Service Broker: The Complete Map

Components  ·  Flow  ·  Health Checks  ·  SQLYARD.com


The 7 Building Blocks (in order)
📋
1. Message Type
Defines the format and validation rules for a message. What does the data look like? XML, binary, or empty?
📜
2. Contract
Defines which message types are used in a conversation and who can send them. The rules of the conversation.
📬
3. Queue
The mailbox. Messages sit here until a stored procedure processes them. Lives inside the database. Survives restarts and backups.
🏢
4. Service
The address. A named endpoint that receives messages. Always associated with one queue. Messages sent to a service land in its queue.
🗺️
5. Route
Tells SQL Server where to find a service. Required for cross-instance communication. Within the same database, routing is automatic.
💬
6. Conversation / Dialog
The communication channel between two services. Always bidirectional. Has a unique handle used to send messages. Must be explicitly ended.
7. Activation
The stored procedure that automatically runs when messages arrive in the queue. No polling needed. SQL Server launches it when work appears.

The Postal Service Analogy
Message Type
The type of item you can mail: letter, package, postcard
Contract
The rules: who sends what, who receives what
Queue
The mailbox at the destination address
Service
The street address the mail is sent to
Route
The postal route map to find the address
Conversation
An ongoing correspondence between two people
Activation
The mail carrier who rings the bell when mail arrives

Real-World Use Cases
DDL Change Logging Async Trigger Processing Schema Audit Trail SQL Agent Error Alerting Cross-DB Messaging

Quick Health Checks
is_receive_enabled = 1
Queue is active and accepting messages
is_receive_enabled = 0
Queue disabled. Check for poison messages.
transmission_queue rows
Messages waiting to send. Check routes.
message_count growing
Activation not running. Check procedure.
conversation_state = DI
Disconnected. End conversation and clean up.

1 What Service Broker Is and Why It Exists Beginner

Service Broker is SQL Server’s built-in asynchronous messaging system. It has been part of the SQL Server engine since SQL Server 2005 and it solves a specific problem: how do you do work that takes time without making the original transaction wait for it?

Consider a trigger that fires when a row is inserted. The trigger needs to send an email, log an audit record to another database, update a summary table, and call an external web service. Every one of those operations holds the original INSERT transaction open and makes the inserting user wait. If the email server is slow or the external service times out, the original transaction times out too.

Service Broker breaks that coupling. Instead of doing the work inside the trigger, the trigger puts a message in a queue and returns immediately. The original transaction commits without waiting. A separate stored procedure reads the queue and does the actual work asynchronously, in its own transaction, at its own pace. The two operations are now decoupled.

Everything in Service Broker is transactional. Messages in queues survive server restarts. They survive database backups and restores. A message sent to a queue is not lost if the server crashes before the activation procedure processes it. This durability is what distinguishes Service Broker from a simple in-memory queue.

You already use Service Broker. Database Mail in SQL Server runs entirely on Service Broker. When you send an email with msdb.dbo.sp_send_dbmail, your call puts a message in a Service Broker queue in msdb and returns immediately. A background activation procedure reads the queue and actually sends the email. Every email you have ever sent through Database Mail went through Service Broker.

2 The Seven Building Blocks Explained Beginner

Service Broker has seven components that you create in a specific order. Each builds on the previous one. Understanding what each is before you build anything makes the whole system coherent.

Message Type

A message type defines the format of the data that can be sent in a message. Think of it as defining what kind of thing you can put in an envelope. You can specify that messages of this type must be valid XML (VALIDATION = WELL_FORMED_XML), must conform to a specific XML schema (VALIDATION = VALID_XML WITH SCHEMA COLLECTION), must be empty (VALIDATION = EMPTY), or can contain anything (VALIDATION = NONE). For most DBA use cases, NONE or WELL_FORMED_XML is sufficient.

Contract

A contract defines which message types can be used in a conversation and which party can send them. A contract can specify that the initiator (the sender) can send message type A, the target (the receiver) can send message type B, and either party can send message type C. Contracts enforce the protocol of communication. Think of it as the rules of the correspondence: what you can say and who gets to say it.

Queue

A queue is the mailbox. It is a database object that stores incoming messages until a stored procedure reads and processes them. Queues are transactional, durable, and live inside the database. If you back up and restore the database, messages in the queues come along. Queues can have an activation stored procedure defined on them that SQL Server automatically runs when messages arrive.

Service

A service is a named endpoint that receives messages. It is always associated with exactly one queue. When another service sends a message to a named service, SQL Server places that message in the queue associated with the target service. The service name is what you address messages to, like a street address. The queue is the actual mailbox at that address.

Route

A route tells SQL Server where to find a service. For conversations within the same database, routes are automatic and you do not need to create them. For cross-database or cross-instance conversations, you create routes that map service names to network addresses. Routes live in the msdb database as system routes or in the current database as user routes.

Conversation / Dialog

A conversation (also called a dialog) is the communication channel between two services. Every message sent or received belongs to a conversation. Conversations are always bidirectional: both the initiator and the target can send messages within the same conversation. A conversation has a unique handle (a GUID) that you use to send messages and that you must use to end the conversation when work is complete. Conversations that are never explicitly ended accumulate as orphaned conversations that consume resources.

Activation

Activation is the mechanism by which SQL Server automatically runs a stored procedure when messages arrive in a queue. You associate a stored procedure with a queue and set the maximum number of concurrent instances SQL Server can run. When messages arrive, SQL Server launches the procedure automatically. When the queue is empty, the procedure exits. You do not need polling loops or SQL Agent jobs to process messages.

3 How a Message Flows: End to End Beginner

Here is the complete journey of one message through Service Broker within a single database:

-- Complete message flow within one database:

Step 1: A trigger or stored procedure begins a conversation
        BEGIN DIALOG @handle
        FROM SERVICE [InitiatorService]
        TO SERVICE 'TargetService'
        ON CONTRACT [MyContract]
        WITH ENCRYPTION = OFF;

Step 2: A message is sent on that conversation handle
        SEND ON CONVERSATION @handle
        MESSAGE TYPE [MyMessageType]
        (@messageBody);  -- your XML or text payload

Step 3: The SEND completes within the original transaction.
        The original transaction commits.
        The message now sits in the target queue.

Step 4: SQL Server detects the message in the queue.
        If activation is configured, SQL Server runs
        the activation stored procedure automatically.

Step 5: The activation procedure reads the message:
        RECEIVE TOP(1)
            @messageType = message_type_name,
            @message     = message_body,
            @handle      = conversation_handle
        FROM TargetQueue;

Step 6: The procedure processes the message and
        ends the conversation:
        END CONVERSATION @handle;

Step 7: An EndDialog message is sent back to the
        initiator side and the conversation is closed.
        If the initiator queue is not being processed,
        these EndDialog messages accumulate -- make sure
        you process both sides of every conversation.

4 Example 1: Async DDL Schema Change Logging Intermediate

This is the pattern the user described: a DDL trigger fires on schema changes, sends a message via Service Broker, and an activation stored procedure logs the change to an audit table. This approach is better than logging directly in the DDL trigger because the trigger returns immediately without waiting for the logging operation.

-- ============================================================
-- STEP 1: Enable Service Broker on the database
-- ============================================================

ALTER DATABASE YourDatabase SET ENABLE_BROKER WITH ROLLBACK IMMEDIATE;

-- Verify it is enabled
SELECT name, is_broker_enabled
FROM sys.databases
WHERE name = 'YourDatabase';

-- ============================================================
-- STEP 2: Create the audit log table
-- ============================================================

USE YourDatabase;

CREATE TABLE dbo.SchemaChangeLog (
    LogID           INT IDENTITY(1,1) PRIMARY KEY,
    EventType       NVARCHAR(100) NOT NULL,
    SchemaName      NVARCHAR(128),
    ObjectName      NVARCHAR(128),
    ObjectType      NVARCHAR(128),
    LoginName       NVARCHAR(256),
    DatabaseName    NVARCHAR(128),
    EventXML        XML,
    LoggedAt        DATETIME2 DEFAULT SYSDATETIME()
);

-- ============================================================
-- STEP 3: Create Service Broker objects
-- ============================================================

-- Message type: well-formed XML containing DDL event data
CREATE MESSAGE TYPE [//SchemaChange/EventMessage]
    VALIDATION = WELL_FORMED_XML;

-- Contract: the initiator (trigger) sends the message
CREATE CONTRACT [//SchemaChange/Contract] (
    [//SchemaChange/EventMessage] SENT BY INITIATOR
);

-- Queue: where messages will sit until processed
CREATE QUEUE dbo.SchemaChangeQueue
    WITH STATUS = ON,
         RETENTION = OFF;

-- Initiator queue and service (needed to receive EndDialog messages)
CREATE QUEUE dbo.SchemaChangeSenderQueue
    WITH STATUS = ON;

-- Target service: receives schema change messages
CREATE SERVICE [//SchemaChange/TargetService]
    ON QUEUE dbo.SchemaChangeQueue
    ([//SchemaChange/Contract]);

-- Initiator service: sends schema change messages
CREATE SERVICE [//SchemaChange/InitiatorService]
    ON QUEUE dbo.SchemaChangeSenderQueue;

-- ============================================================
-- STEP 4: Create the activation stored procedure
-- This runs automatically when messages arrive in the queue
-- ============================================================

CREATE PROCEDURE dbo.usp_ProcessSchemaChangeQueue
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE
        @handle     UNIQUEIDENTIFIER,
        @msgType    NVARCHAR(256),
        @msgBody    XML;

    WHILE 1 = 1
    BEGIN
        BEGIN TRANSACTION;

        RECEIVE TOP(1)
            @handle  = conversation_handle,
            @msgType = message_type_name,
            @msgBody = CAST(message_body AS XML)
        FROM dbo.SchemaChangeQueue;

        IF @@ROWCOUNT = 0
        BEGIN
            ROLLBACK TRANSACTION;
            BREAK;
        END

        IF @msgType = '//SchemaChange/EventMessage'
        BEGIN
            -- Parse the DDL event XML and log to audit table
            INSERT INTO dbo.SchemaChangeLog
                (EventType, SchemaName, ObjectName, ObjectType,
                 LoginName, DatabaseName, EventXML)
            SELECT
                @msgBody.value('(/EVENT_INSTANCE/EventType)[1]',   'NVARCHAR(100)'),
                @msgBody.value('(/EVENT_INSTANCE/SchemaName)[1]',  'NVARCHAR(128)'),
                @msgBody.value('(/EVENT_INSTANCE/ObjectName)[1]',  'NVARCHAR(128)'),
                @msgBody.value('(/EVENT_INSTANCE/ObjectType)[1]',  'NVARCHAR(128)'),
                @msgBody.value('(/EVENT_INSTANCE/LoginName)[1]',   'NVARCHAR(256)'),
                @msgBody.value('(/EVENT_INSTANCE/DatabaseName)[1]','NVARCHAR(128)'),
                @msgBody;

            END CONVERSATION @handle;
        END
        ELSE IF @msgType = 'http://schemas.microsoft.com/SQL/ServiceBroker/EndDialog'
            OR @msgType = 'http://schemas.microsoft.com/SQL/ServiceBroker/Error'
        BEGIN
            -- Always handle EndDialog and Error messages
            END CONVERSATION @handle;
        END

        COMMIT TRANSACTION;
    END
END
GO

-- ============================================================
-- STEP 5: Attach activation to the queue
-- ============================================================

ALTER QUEUE dbo.SchemaChangeQueue
    WITH ACTIVATION (
        STATUS              = ON,
        PROCEDURE_NAME      = dbo.usp_ProcessSchemaChangeQueue,
        MAX_QUEUE_READERS   = 1,   -- 1 reader for sequential processing
        EXECUTE AS OWNER
    );

-- ============================================================
-- STEP 6: Create the DDL trigger
-- This fires on schema changes and sends a Service Broker message
-- The trigger returns immediately -- no waiting for logging
-- ============================================================

CREATE TRIGGER trig_DDLSchemaChanges
ON DATABASE
FOR DDL_DATABASE_LEVEL_EVENTS  -- captures all DDL at database level
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE
        @handle  UNIQUEIDENTIFIER,
        @event   XML = EVENTDATA();  -- DDL event data as XML

    -- Only capture schema-related events
    IF @event.value('(/EVENT_INSTANCE/EventType)[1]', 'NVARCHAR(100)')
       NOT IN ('CREATE_TABLE','ALTER_TABLE','DROP_TABLE',
               'CREATE_INDEX','DROP_INDEX','ALTER_INDEX',
               'CREATE_PROCEDURE','ALTER_PROCEDURE','DROP_PROCEDURE',
               'CREATE_VIEW','ALTER_VIEW','DROP_VIEW',
               'CREATE_FUNCTION','ALTER_FUNCTION','DROP_FUNCTION',
               'CREATE_TRIGGER','ALTER_TRIGGER','DROP_TRIGGER',
               'RENAME','CREATE_SCHEMA','DROP_SCHEMA')
        RETURN;

    -- Begin a dialog to the target service
    BEGIN DIALOG @handle
        FROM SERVICE [//SchemaChange/InitiatorService]
        TO SERVICE   '//SchemaChange/TargetService'
        ON CONTRACT  [//SchemaChange/Contract]
        WITH ENCRYPTION = OFF;

    -- Send the event data as a message
    SEND ON CONVERSATION @handle
        MESSAGE TYPE [//SchemaChange/EventMessage]
        (@event);

    -- Trigger returns. Message is queued.
    -- Activation procedure handles the logging asynchronously.
END
GO

Why this is better than logging directly in the trigger: The trigger completes in microseconds because it only sends a message. The logging work happens in a separate transaction asynchronously. If the log table is slow or temporarily locked, it does not block the original DDL operation. The message is durable: if the server restarts before the activation procedure runs, the message is still in the queue when the server comes back up.

5 Example 2: Event Notifications for Server-Level DDL Intermediate

For schema changes that span multiple databases or need to be captured at the server level, SQL Server’s Event Notification feature uses Service Broker to deliver DDL and SQL Trace events asynchronously. This is different from a DDL trigger: event notifications run outside the scope of the triggering transaction and can capture events at the server level rather than just the database level.

-- Event Notification: capture CREATE_TABLE at server level
-- using Service Broker as the delivery mechanism

USE msdb;  -- Server-level event notifications use msdb

-- Create the notification queue in msdb
CREATE QUEUE dbo.ServerDDLNotifyQueue
    WITH STATUS = ON;

-- Create the notification service
CREATE SERVICE [//Server/DDLNotifyService]
    ON QUEUE dbo.ServerDDLNotifyQueue
    ([http://schemas.microsoft.com/SQL/Notifications/PostEventNotification]);

-- Create the server-level event notification
-- This captures CREATE_TABLE events across ALL databases on this instance
CREATE EVENT NOTIFICATION EN_CreateTable
    ON SERVER
    FOR CREATE_TABLE
    TO SERVICE '//Server/DDLNotifyService', 'current database';

-- The notification message is XML in the same EVENTDATA() format
-- Read it from the queue using the same RECEIVE pattern:

-- Read and display recent server-level DDL events
SELECT TOP 10
    CAST(message_body AS XML)                           AS EventXML,
    CAST(message_body AS XML).value(
        '(/EVENT_INSTANCE/EventType)[1]', 'NVARCHAR(100)')  AS EventType,
    CAST(message_body AS XML).value(
        '(/EVENT_INSTANCE/DatabaseName)[1]', 'NVARCHAR(128)') AS DatabaseName,
    CAST(message_body AS XML).value(
        '(/EVENT_INSTANCE/ObjectName)[1]', 'NVARCHAR(128)')   AS ObjectName,
    CAST(message_body AS XML).value(
        '(/EVENT_INSTANCE/LoginName)[1]', 'NVARCHAR(256)')    AS LoginName,
    queuing_order,
    enqueue_time
FROM dbo.ServerDDLNotifyQueue
WITH (NOLOCK)  -- read without removing (just for viewing)
ORDER BY queuing_order DESC;

-- Clean up event notification when no longer needed
DROP EVENT NOTIFICATION EN_CreateTable ON SERVER;

6 Example 3: Async Trigger Processing for Heavy OLTP Intermediate

When a trigger on a high-volume OLTP table does heavy work (complex calculations, external logging, aggregation updates), it slows every INSERT, UPDATE, or DELETE on that table. Service Broker decouples the trigger work from the original DML transaction.

-- Pattern: trigger sends message, processor does the heavy work

-- The lightweight trigger (runs in milliseconds)
CREATE TRIGGER trg_Orders_AfterInsert
ON dbo.Orders
AFTER INSERT
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE
        @handle  UNIQUEIDENTIFIER,
        @payload XML;

    -- Build a lightweight XML payload with just the key info
    SELECT @payload = (
        SELECT
            i.OrderID,
            i.CustomerID,
            i.OrderTotal,
            i.OrderDate,
            SUSER_SNAME() AS LoginName
        FROM inserted i
        FOR XML PATH('Order'), ROOT('Orders')
    );

    -- Send to the processing queue and return immediately
    BEGIN DIALOG @handle
        FROM SERVICE [//Orders/InitiatorService]
        TO SERVICE   '//Orders/ProcessorService'
        ON CONTRACT  [//Orders/Contract]
        WITH ENCRYPTION = OFF;

    SEND ON CONVERSATION @handle
        MESSAGE TYPE [//Orders/NewOrderMessage]
        (@payload);

    -- Trigger is done. Original INSERT commits without waiting.
END
GO

-- The async processor (runs in its own transaction)
-- This is where the heavy work happens
CREATE PROCEDURE dbo.usp_ProcessOrderQueue
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE
        @handle  UNIQUEIDENTIFIER,
        @msgType NVARCHAR(256),
        @payload XML;

    WHILE 1 = 1
    BEGIN
        BEGIN TRANSACTION;

        RECEIVE TOP(1)
            @handle  = conversation_handle,
            @msgType = message_type_name,
            @payload = CAST(message_body AS XML)
        FROM dbo.OrderProcessingQueue;

        IF @@ROWCOUNT = 0
        BEGIN
            ROLLBACK TRANSACTION;
            BREAK;
        END

        IF @msgType = '//Orders/NewOrderMessage'
        BEGIN
            -- Do the heavy work here: update aggregates,
            -- log to data warehouse, call external systems
            -- This runs in its own transaction, decoupled
            -- from the original INSERT that triggered it

            DECLARE @CustomerID INT =
                @payload.value('(/Orders/Order/CustomerID)[1]', 'INT');
            DECLARE @OrderTotal DECIMAL(18,2) =
                @payload.value('(/Orders/Order/OrderTotal)[1]', 'DECIMAL(18,2)');

            -- Update customer summary (heavy operation, safe here)
            UPDATE dbo.CustomerSummary
            SET    TotalOrders = TotalOrders + 1,
                   TotalRevenue = TotalRevenue + @OrderTotal,
                   LastOrderDate = SYSDATETIME()
            WHERE  CustomerID = @CustomerID;

            END CONVERSATION @handle;
        END
        ELSE
        BEGIN
            END CONVERSATION @handle;
        END

        COMMIT TRANSACTION;
    END
END
GO

7 Example 4: SQL Agent Error Log Alerting Intermediate

SQL Server Agent can fire alerts based on error conditions. Service Broker can be used to capture those alerts and route them to a logging table or notification queue. The most practical approach for error log alerting combines SQL Agent alerts with Service Broker event notifications.

-- ============================================================
-- Capture SQL Server error events via Event Notification
-- and log them using Service Broker
-- ============================================================

USE msdb;

-- Queue for error notifications
CREATE QUEUE dbo.ErrorAlertQueue
    WITH STATUS = ON;

-- Service for error alerts
CREATE SERVICE [//Alerts/ErrorNotifyService]
    ON QUEUE dbo.ErrorAlertQueue
    ([http://schemas.microsoft.com/SQL/Notifications/PostEventNotification]);

-- Error log table
USE YourDatabase;
CREATE TABLE dbo.SQLServerErrorLog (
    ErrorLogID      INT IDENTITY(1,1) PRIMARY KEY,
    EventType       NVARCHAR(100),
    Severity        INT,
    ErrorNumber     INT,
    ErrorMessage    NVARCHAR(MAX),
    LoginName       NVARCHAR(256),
    HostName        NVARCHAR(256),
    CapturedAt      DATETIME2 DEFAULT SYSDATETIME(),
    EventXML        XML
);

-- Activation procedure to process error alerts
CREATE PROCEDURE dbo.usp_ProcessErrorAlerts
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE
        @handle  UNIQUEIDENTIFIER,
        @msgType NVARCHAR(256),
        @event   XML;

    WHILE 1 = 1
    BEGIN
        BEGIN TRANSACTION;

        RECEIVE TOP(1)
            @handle  = conversation_handle,
            @msgType = message_type_name,
            @event   = CAST(message_body AS XML)
        FROM msdb.dbo.ErrorAlertQueue;

        IF @@ROWCOUNT = 0 BEGIN ROLLBACK TRANSACTION; BREAK; END

        IF @msgType = 'http://schemas.microsoft.com/SQL/Notifications/PostEventNotification'
        BEGIN
            INSERT INTO YourDatabase.dbo.SQLServerErrorLog
                (EventType, Severity, ErrorNumber, ErrorMessage,
                 LoginName, HostName, EventXML)
            SELECT
                @event.value('(/EVENT_INSTANCE/EventType)[1]',  'NVARCHAR(100)'),
                @event.value('(/EVENT_INSTANCE/Severity)[1]',   'INT'),
                @event.value('(/EVENT_INSTANCE/Error)[1]',      'INT'),
                @event.value('(/EVENT_INSTANCE/TextData)[1]',   'NVARCHAR(MAX)'),
                @event.value('(/EVENT_INSTANCE/LoginName)[1]',  'NVARCHAR(256)'),
                @event.value('(/EVENT_INSTANCE/HostName)[1]',   'NVARCHAR(256)'),
                @event;

            END CONVERSATION @handle;
        END
        ELSE
            END CONVERSATION @handle;

        COMMIT TRANSACTION;
    END
END
GO

-- Attach activation to the error queue
ALTER QUEUE msdb.dbo.ErrorAlertQueue
    WITH ACTIVATION (
        STATUS            = ON,
        PROCEDURE_NAME    = YourDatabase.dbo.usp_ProcessErrorAlerts,
        MAX_QUEUE_READERS = 1,
        EXECUTE AS OWNER
    );

-- Create the event notification for severity 17+ errors
-- (Severity 17+ are resource errors worth logging)
CREATE EVENT NOTIFICATION EN_ErrorSeverity17Plus
    ON SERVER
    FOR ERRORLOG
    TO SERVICE '//Alerts/ErrorNotifyService', 'current database'
    WHERE severity >= 17;

8 Checking Whether Service Broker Is Working Beginner

Service Broker can fail silently. Messages pile up in a queue with nothing processing them and no error appears in SQL Server Management Studio. These are the first diagnostic queries to run when something seems wrong.

-- ============================================================
-- CHECK 1: Is Service Broker enabled on the database?
-- ============================================================

SELECT name, is_broker_enabled, service_broker_guid
FROM sys.databases
WHERE name = DB_NAME();

-- is_broker_enabled = 0 means NOTHING in Service Broker
-- on this database will work. Enable it first:
-- ALTER DATABASE YourDatabase SET ENABLE_BROKER WITH ROLLBACK IMMEDIATE;

-- ============================================================
-- CHECK 2: Queue health -- are queues active and how many
-- messages are waiting?
-- ============================================================

SELECT
    name                    AS QueueName,
    is_receive_enabled,
    is_enqueue_enabled,
    activation_procedure,
    is_activation_enabled,
    max_readers,
    -- Count messages currently in the queue
    (SELECT COUNT(*)
     FROM sys.service_broker_endpoints) AS BrokerEndpoints
FROM sys.service_queues
WHERE is_ms_shipped = 0  -- user-created queues only
ORDER BY name;

-- Check actual message counts per queue
SELECT
    q.name                  AS QueueName,
    q.is_receive_enabled,
    q.is_enqueue_enabled,
    q.is_activation_enabled,
    q.max_readers,
    -- Message count (requires querying the queue object itself)
    OBJECT_NAME(q.object_id) AS QueueObjectName
FROM sys.service_queues q
WHERE q.is_ms_shipped = 0;

-- Count messages in a specific queue
SELECT COUNT(*) AS MessageCount
FROM dbo.SchemaChangeQueue WITH (NOLOCK);

-- ============================================================
-- CHECK 3: Are there messages stuck in the transmission queue?
-- (Only relevant for cross-database or cross-instance messaging)
-- ============================================================

SELECT
    to_service_name,
    to_broker_instance,
    message_type_name,
    transmission_status,    -- NULL = OK, error text = problem
    enqueue_time,
    DATEDIFF(MINUTE, enqueue_time, GETDATE()) AS MinutesWaiting
FROM sys.transmission_queue
ORDER BY enqueue_time;

-- Non-null transmission_status means messages cannot be delivered
-- Common causes: route missing, broker not enabled on target,
-- network issue for cross-instance

-- ============================================================
-- CHECK 4: Conversation state -- any conversations in bad states?
-- ============================================================

SELECT
    far_service,
    state_desc,
    COUNT(*)                AS ConversationCount,
    MAX(DATEDIFF(HOUR, start_time, GETDATE())) AS MaxAgeHours
FROM sys.conversation_endpoints
GROUP BY far_service, state_desc
ORDER BY state_desc, ConversationCount DESC;

-- States to watch for:
-- STARTED_OUTBOUND (SO): conversation started, waiting for response
-- CONVERSING (CO): active and healthy
-- DISCONNECTED_INBOUND (DI): partner disconnected, needs cleanup
-- CLOSED (CD): finished, should be cleaned up
-- ERROR (ER): error occurred, end this conversation

9 Poison Messages: The Silent Queue Killer Intermediate

A poison message is a message that causes the activation stored procedure to fail and roll back the transaction. When the same transaction rolls back five times trying to process the same message, SQL Server automatically disables the queue by setting is_receive_enabled = 0. Everything stops. New messages cannot be received. No error is raised to your application. The queue simply goes silent.

This is the most common Service Broker production problem and it is entirely preventable with proper error handling in the activation procedure.

-- ============================================================
-- DETECT a disabled queue
-- ============================================================

SELECT name, is_receive_enabled, is_enqueue_enabled
FROM sys.service_queues
WHERE is_receive_enabled = 0   -- queue is disabled
AND   is_ms_shipped = 0;

-- If you find a disabled queue, there is a poison message in it

-- ============================================================
-- FIND the poison message
-- ============================================================

-- Peek at the messages in the disabled queue without removing them
SELECT TOP 10
    conversation_handle,
    message_type_name,
    CAST(message_body AS NVARCHAR(MAX)) AS MessageBody,
    message_sequence_number,
    enqueue_time
FROM dbo.SchemaChangeQueue WITH (NOLOCK)
ORDER BY enqueue_time;

-- ============================================================
-- RESOLVE the poison message
-- ============================================================

-- Step 1: Re-enable the queue
ALTER QUEUE dbo.SchemaChangeQueue WITH STATUS = ON;

-- Step 2: Receive and end the conversation for the bad message
-- Do this in a separate transaction to isolate it
DECLARE @handle UNIQUEIDENTIFIER;
DECLARE @msgType NVARCHAR(256);

BEGIN TRANSACTION;

RECEIVE TOP(1)
    @handle  = conversation_handle,
    @msgType = message_type_name
FROM dbo.SchemaChangeQueue;

-- End the conversation with an error to notify the sender
END CONVERSATION @handle
    WITH ERROR = 500
    DESCRIPTION = N'Poison message detected and removed';

COMMIT TRANSACTION;

-- ============================================================
-- PREVENT poison messages with proper error handling
-- in the activation procedure
-- ============================================================

-- The activation procedure should handle errors gracefully
-- rather than rolling back and re-queuing the same message

CREATE PROCEDURE dbo.usp_SafeQueueProcessor
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @handle  UNIQUEIDENTIFIER;
    DECLARE @msgType NVARCHAR(256);
    DECLARE @body    XML;

    WHILE 1 = 1
    BEGIN
        BEGIN TRANSACTION;

        BEGIN TRY
            RECEIVE TOP(1)
                @handle  = conversation_handle,
                @msgType = message_type_name,
                @body    = CAST(message_body AS XML)
            FROM dbo.SchemaChangeQueue;

            IF @@ROWCOUNT = 0
            BEGIN
                ROLLBACK TRANSACTION;
                BREAK;
            END

            -- Process the message here...
            -- If this fails it goes to the CATCH block

            IF @msgType = '//SchemaChange/EventMessage'
                -- Do work here
                END CONVERSATION @handle;
            ELSE
                END CONVERSATION @handle;

            COMMIT TRANSACTION;

        END TRY
        BEGIN CATCH
            -- On error: end the conversation with error
            -- rather than rolling back and requeueing
            -- This prevents the poison message cycle
            IF @@TRANCOUNT > 0
            BEGIN
                END CONVERSATION @handle
                    WITH ERROR = ERROR_NUMBER()
                    DESCRIPTION = ERROR_MESSAGE();
                COMMIT TRANSACTION;  -- commit the END CONVERSATION
            END

            -- Log the error for investigation
            INSERT INTO dbo.ServiceBrokerErrorLog
                (QueueName, ErrorNumber, ErrorMessage, ConversationHandle, LoggedAt)
            VALUES
                ('SchemaChangeQueue', ERROR_NUMBER(),
                 ERROR_MESSAGE(), @handle, SYSDATETIME());
        END CATCH
    END
END
GO

10 Stuck Conversations and How to Clean Them Up Intermediate

Every conversation must be explicitly ended. If the activation procedure ends the target side of a conversation but nothing processes the corresponding EndDialog message on the initiator side, those conversations accumulate as orphaned endpoints. Over months this can grow to thousands of stale conversation endpoints consuming memory and appearing in diagnostic queries.

-- ============================================================
-- FIND orphaned and stale conversations
-- ============================================================

SELECT
    conversation_handle,
    state_desc,
    far_service,
    start_time,
    DATEDIFF(HOUR, start_time, GETDATE()) AS AgeHours,
    is_initiator
FROM sys.conversation_endpoints
WHERE state_desc IN ('DISCONNECTED_INBOUND', 'CLOSED', 'ERROR')
OR    DATEDIFF(DAY, start_time, GETDATE()) > 7  -- older than 7 days
ORDER BY start_time;

-- ============================================================
-- CLEAN UP orphaned conversations
-- Do this in batches to avoid long-running transactions
-- ============================================================

DECLARE @handle UNIQUEIDENTIFIER;
DECLARE @count  INT = 0;

DECLARE cleanup_cursor CURSOR FAST_FORWARD FOR
    SELECT conversation_handle
    FROM sys.conversation_endpoints
    WHERE state_desc IN ('DISCONNECTED_INBOUND', 'CLOSED', 'ERROR')
    OR    DATEDIFF(DAY, start_time, GETDATE()) > 30;

OPEN cleanup_cursor;
FETCH NEXT FROM cleanup_cursor INTO @handle;

WHILE @@FETCH_STATUS = 0 AND @count < 1000  -- batch limit
BEGIN
    BEGIN TRY
        END CONVERSATION @handle WITH CLEANUP;
        SET @count += 1;
    END TRY
    BEGIN CATCH
        -- Some conversations may already be gone
    END CATCH
    FETCH NEXT FROM cleanup_cursor INTO @handle;
END

CLOSE cleanup_cursor;
DEALLOCATE cleanup_cursor;

PRINT CAST(@count AS VARCHAR) + ' conversations cleaned up.';

-- ============================================================
-- Handle EndDialog messages on the initiator queue
-- If you never process these, they accumulate
-- ============================================================

DECLARE @handle UNIQUEIDENTIFIER;
DECLARE @msgType NVARCHAR(256);

-- Drain the initiator queue of EndDialog messages
WHILE 1 = 1
BEGIN
    BEGIN TRANSACTION;

    RECEIVE TOP(10)
        @handle  = conversation_handle,
        @msgType = message_type_name
    FROM dbo.SchemaChangeSenderQueue;  -- initiator queue

    IF @@ROWCOUNT = 0
    BEGIN
        ROLLBACK TRANSACTION;
        BREAK;
    END

    END CONVERSATION @handle;
    COMMIT TRANSACTION;
END

11 The Complete Troubleshooting Query Toolkit Beginner

Run these queries when investigating any Service Broker issue. They cover every layer from database configuration to individual message state.

-- ============================================================
-- FULL SERVICE BROKER HEALTH CHECK
-- Run this whenever something seems wrong
-- ============================================================

-- 1. Database-level broker status
SELECT name, is_broker_enabled, service_broker_guid
FROM sys.databases
ORDER BY name;

-- 2. All user queues: status and message counts
SELECT
    q.name                          AS QueueName,
    q.is_receive_enabled,
    q.is_enqueue_enabled,
    q.is_activation_enabled,
    q.max_readers,
    q.activation_procedure,
    -- Count messages in each queue
    (SELECT COUNT(*) FROM sys.objects o2
     WHERE o2.object_id = q.object_id) AS ConfirmExists
FROM sys.service_queues q
WHERE q.is_ms_shipped = 0
ORDER BY q.name;

-- 3. All services and which queue they use
SELECT
    s.name                          AS ServiceName,
    q.name                          AS QueueName,
    q.is_receive_enabled
FROM sys.services s
JOIN sys.service_queues q ON q.object_id = s.service_queue_id
WHERE s.principal_id IS NOT NULL  -- user-created services
ORDER BY s.name;

-- 4. All contracts and message types
SELECT
    c.name                          AS ContractName,
    mt.name                         AS MessageTypeName,
    cmt.is_sent_by_initiator,
    cmt.is_sent_by_target
FROM sys.service_contracts c
JOIN sys.service_contract_message_usages cmt
    ON cmt.service_contract_id = c.service_contract_id
JOIN sys.service_message_types mt
    ON mt.message_type_id = cmt.message_type_id
WHERE c.principal_id IS NOT NULL  -- user-created contracts
ORDER BY c.name, mt.name;

-- 5. Conversation endpoint summary
SELECT
    state_desc,
    COUNT(*)                        AS Count,
    MIN(start_time)                 AS OldestConversation,
    MAX(start_time)                 AS NewestConversation
FROM sys.conversation_endpoints
GROUP BY state_desc
ORDER BY Count DESC;

-- 6. Transmission queue: messages that could not be delivered
SELECT
    to_service_name,
    transmission_status,
    message_type_name,
    DATEDIFF(MINUTE, enqueue_time, GETDATE()) AS MinutesStuck
FROM sys.transmission_queue
ORDER BY enqueue_time;

-- 7. Routes
SELECT name, remote_service_name, broker_instance, address
FROM sys.routes
ORDER BY name;

-- 8. Recent Service Broker events in error log
EXEC xp_readerrorlog 0, 1, N'broker', NULL, NULL, NULL, N'desc';
SymptomFirst CheckLikely CauseFix
Messages not being processedQueue message count growingActivation not running or disabledCheck is_activation_enabled, verify procedure name, check for errors in procedure
Queue suddenly goes silentis_receive_enabled = 0Poison message disabled the queueRe-enable queue, receive and end the poison message conversation
Messages stuck in transmission_queueNon-null transmission_statusRoute missing or broker not enabled on targetCheck routes, verify target database has broker enabled
Thousands of conversation endpointssys.conversation_endpoints countEndDialog messages not being processedDrain initiator queue, run conversation cleanup
Service Broker not working at allis_broker_enabled = 0Broker not enabled on databaseALTER DATABASE SET ENABLE_BROKER WITH ROLLBACK IMMEDIATE

References


Discover more from SQLYARD

Subscribe to get the latest posts sent to your email.

Leave a Reply

Discover more from SQLYARD

Subscribe now to keep reading and get access to the full archive.

Continue reading