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
- Log in to Vigilmon → Add Monitor → HTTP.
- URL: your Azure Function health endpoint URL.
- Check interval: 60 seconds.
- Response timeout: 10 seconds.
- Expected status:
200. - JSON assertion: path
status, expected valueok. - Alert on: first failure (immediate) and escalate every 5 minutes.
Tip: Set
DLQ_THRESHOLDin 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.