tutorial

Monitoring Azure Service Bus with Vigilmon: Queue Health, Dead-Letter Alerting & Consumer Heartbeats

Practical guide to uptime monitoring for Azure Service Bus — queue health endpoints, dead-letter monitoring, consumer heartbeats, and alert routing with Vigilmon.

Azure Service Bus is the enterprise messaging backbone for many Azure-based applications — decoupling microservices, enabling async workflows, and buffering traffic spikes. But when a queue's dead-letter sub-queue starts filling up, or a message consumer stops processing and messages accumulate, your application silently degrades. Azure Monitor can alert on Service Bus metrics, but it requires configuration inside the same subscription that might be having problems. Vigilmon gives you an external, independent health signal that works even when your Azure subscription itself has an issue.

This tutorial shows you how to build a production monitoring strategy for Azure Service Bus.

What You'll Build

  • An Azure Function health endpoint that checks Service Bus queue depth and connectivity
  • A consumer heartbeat that pings Vigilmon after each processing batch
  • A dead-letter queue monitor that withholds the heartbeat when messages accumulate
  • Alert channels for email and webhook

Prerequisites

  • Azure subscription with at least one Service Bus namespace and queue or topic
  • Azure Functions app or .NET/Node.js service consuming from Service Bus
  • A free account at vigilmon.online

Step 1: Add a Service Bus Health Endpoint

Deploy an Azure Function HTTP trigger that checks your Service Bus namespace and queue health. This gives Vigilmon a URL to poll every minute.

C# Azure Function

// HealthFunction.cs
using Azure.Messaging.ServiceBus.Administration;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using System.Net;

public class HealthFunction
{
    private readonly ServiceBusAdministrationClient _admin;

    public HealthFunction(ServiceBusAdministrationClient admin)
    {
        _admin = admin;
    }

    [Function("health")]
    public async Task<HttpResponseData> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequestData req)
    {
        var checks = new Dictionary<string, string>();
        var ok = true;

        // Check queue runtime properties
        try
        {
            var queueName = Environment.GetEnvironmentVariable("SERVICE_BUS_QUEUE");
            var props = await _admin.GetQueueRuntimePropertiesAsync(queueName);
            var depth = props.Value.ActiveMessageCount;
            var dlqDepth = props.Value.DeadLetterMessageCount;

            checks["queue_depth"] = depth.ToString();
            checks["dlq_depth"] = dlqDepth.ToString();
            checks["queue"] = "ok";

            if (dlqDepth > int.Parse(Environment.GetEnvironmentVariable("DLQ_THRESHOLD") ?? "10"))
            {
                checks["queue"] = $"dlq_high:{dlqDepth}";
                ok = false;
            }
        }
        catch (Exception ex)
        {
            checks["queue"] = $"error: {ex.Message}";
            ok = false;
        }

        var status = ok ? "ok" : "degraded";
        var statusCode = ok ? HttpStatusCode.OK : HttpStatusCode.ServiceUnavailable;
        var response = req.CreateResponse(statusCode);
        await response.WriteAsJsonAsync(new { status, checks });
        return response;
    }
}

Node.js Azure Function

// src/functions/health.js
const { app } = require("@azure/functions");
const { ServiceBusAdministrationClient } = require("@azure/service-bus");

const admin = new ServiceBusAdministrationClient(
  process.env.SERVICE_BUS_CONNECTION_STRING
);

app.http("health", {
  methods: ["GET"],
  authLevel: "anonymous",
  handler: async (request, context) => {
    const checks = {};
    let ok = true;

    try {
      const queueName = process.env.SERVICE_BUS_QUEUE;
      const props = await admin.getQueueRuntimeProperties(queueName);

      checks.queue_depth = props.activeMessageCount;
      checks.dlq_depth = props.deadLetterMessageCount;
      checks.queue = "ok";

      const dlqThreshold = parseInt(process.env.DLQ_THRESHOLD ?? "10", 10);
      if (props.deadLetterMessageCount > dlqThreshold) {
        checks.queue = `dlq_high:${props.deadLetterMessageCount}`;
        ok = false;
      }
    } catch (err) {
      checks.queue = `error: ${err.message}`;
      ok = false;
    }

    return {
      status: ok ? 200 : 503,
      jsonBody: { status: ok ? "ok" : "degraded", checks },
    };
  },
});

Deploy and note the function URL (available in the Azure portal under your Function App → Functions → health → Get Function URL).


Step 2: Configure the Vigilmon HTTP Monitor

  1. Log in to VigilmonAdd Monitor → HTTP.
  2. URL: your Azure Function health endpoint URL.
  3. Check interval: 60 seconds.
  4. Response timeout: 10 seconds.
  5. Expected status: 200.
  6. JSON assertion: path status, expected value ok.
  7. Alert on: first failure (immediate) and escalate every 5 minutes.

Tip: Set DLQ_THRESHOLD in your function's environment variables. A threshold of 10 messages is reasonable for most queues — adjust based on your normal backlog characteristics.


Step 3: Consumer Heartbeat for Service Bus Processors

If your Service Bus consumer is a background service (not triggered by HTTP), add a Vigilmon heartbeat ping to confirm messages are being processed. Vigilmon will alert you if the consumer stops sending pings.

C# Service Bus processor with heartbeat

// OrderConsumer.cs
using Azure.Messaging.ServiceBus;

public class OrderConsumer : BackgroundService
{
    private readonly ServiceBusProcessor _processor;
    private readonly HttpClient _http;
    private readonly string _heartbeatUrl;
    private DateTimeOffset _lastHeartbeat = DateTimeOffset.MinValue;

    public OrderConsumer(ServiceBusClient client, HttpClient http, IConfiguration config)
    {
        _processor = client.CreateProcessor(config["ServiceBus:Queue"], new ServiceBusProcessorOptions
        {
            AutoCompleteMessages = false,
            MaxConcurrentCalls = 4,
        });
        _http = http;
        _heartbeatUrl = config["Vigilmon:HeartbeatUrl"];
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _processor.ProcessMessageAsync += HandleMessage;
        _processor.ProcessErrorAsync += HandleError;
        await _processor.StartProcessingAsync(stoppingToken);
    }

    private async Task HandleMessage(ProcessMessageEventArgs args)
    {
        try
        {
            await ProcessOrder(args.Message);
            await args.CompleteMessageAsync(args.Message);

            // Ping heartbeat after each successful batch (rate-limit to once per minute)
            if (DateTimeOffset.UtcNow - _lastHeartbeat > TimeSpan.FromMinutes(1))
            {
                await _http.PostAsync(_heartbeatUrl, null);
                _lastHeartbeat = DateTimeOffset.UtcNow;
            }
        }
        catch
        {
            await args.AbandonMessageAsync(args.Message);
            throw;
        }
    }

    private Task HandleError(ProcessErrorEventArgs args)
    {
        // Log but don't kill the processor — messages will retry and eventually DLQ
        Console.Error.WriteLine($"Service Bus error: {args.Exception}");
        return Task.CompletedTask;
    }

    private Task ProcessOrder(ServiceBusReceivedMessage message) { /* ... */ return Task.CompletedTask; }
}

Node.js consumer with heartbeat

// consumer.js
const { ServiceBusClient } = require("@azure/service-bus");

const client = new ServiceBusClient(process.env.SERVICE_BUS_CONNECTION_STRING);
const receiver = client.createReceiver(process.env.SERVICE_BUS_QUEUE);
let lastHeartbeat = 0;

receiver.subscribe({
  processMessage: async (message) => {
    try {
      await processOrder(message);
      await receiver.completeMessage(message);

      // Ping heartbeat at most once per minute
      const now = Date.now();
      if (now - lastHeartbeat > 60_000) {
        await fetch(process.env.VIGILMON_HEARTBEAT_URL, { method: "POST" });
        lastHeartbeat = now;
      }
    } catch (err) {
      await receiver.abandonMessage(message);
      throw err;
    }
  },
  processError: async (err) => {
    console.error("Service Bus error:", err);
  },
});

In Vigilmon, create a Heartbeat monitor with a grace period of 3–5 minutes for a high-throughput queue, or 10–15 minutes for a low-volume queue.


Step 4: Dead-Letter Queue Monitoring

Dead-lettered messages indicate processing failures. Monitor DLQ depth separately so you can alert when the backlog grows beyond a safe threshold.

DLQ depth poller function (runs on a timer trigger)

// src/functions/dlqMonitor.js
const { app } = require("@azure/functions");
const { ServiceBusAdministrationClient } = require("@azure/service-bus");

const admin = new ServiceBusAdministrationClient(
  process.env.SERVICE_BUS_CONNECTION_STRING
);

app.timer("dlqMonitor", {
  schedule: "*/5 * * * *", // every 5 minutes
  handler: async (myTimer, context) => {
    const queueName = process.env.SERVICE_BUS_QUEUE;
    const dlqThreshold = parseInt(process.env.DLQ_THRESHOLD ?? "10", 10);

    try {
      const props = await admin.getQueueRuntimeProperties(queueName);
      const dlqDepth = props.deadLetterMessageCount;

      context.log(`DLQ depth for ${queueName}: ${dlqDepth}`);

      if (dlqDepth > dlqThreshold) {
        context.log.error(`DLQ above threshold (${dlqDepth} > ${dlqThreshold}) — withholding heartbeat`);
        return;
      }

      // DLQ is healthy — send heartbeat
      await fetch(process.env.VIGILMON_DLQ_HEARTBEAT_URL, { method: "POST" });
    } catch (err) {
      context.log.error("DLQ monitor error:", err.message);
    }
  },
});

Create a separate Vigilmon heartbeat monitor for the DLQ poller with a 8-minute grace period.


Step 5: Alerting Strategy

| Alert type | Recommended channel | |---|---| | HTTP health endpoint down | Email + PagerDuty webhook | | Consumer heartbeat missed | Slack webhook + email | | DLQ heartbeat missed | Immediate page — messages being lost | | Status page | Public Vigilmon status page embed |

Configure alert escalation: page immediately on DLQ heartbeat failures (messages are being lost), and give consumer heartbeat failures a 5-minute buffer before escalating.


What Vigilmon Catches That Azure Monitor Misses

| Scenario | Azure Monitor | Vigilmon | |---|---|---| | Queue consumer crashes | Needs metric alert on active message count | Consumer heartbeat missed — alert fires | | DLQ fills with failed messages | Needs explicit DLQ depth alarm | DLQ heartbeat withheld — alert fires | | Service Bus namespace unreachable | Your alarms may also break | External HTTP probe catches it | | Function health endpoint returns 500 | Needs Application Insights alert | HTTP monitor catches immediately | | Consumer processing but business logic broken | Hard to detect with metrics | Heartbeat conditional on success |


Service Bus sits at the heart of your messaging architecture — when it silently fails, downstream services starve for events. External monitoring from Vigilmon gives you the independent vantage point you can't get from Azure's own monitoring stack.

Start monitoring your Azure Service Bus in under 5 minutes — register free at vigilmon.online.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →