tutorial

Monitoring AWS SQS with Vigilmon: Queue Depth, Consumer Health Checks & Dead-Letter Alerting

Practical guide to uptime and health monitoring for AWS SQS — HTTP health endpoints for consumers, heartbeat patterns for workers, dead-letter queue alerting, and queue depth awareness with Vigilmon.

AWS SQS queues are deceptively quiet. Messages pile up, consumers stall, and dead-letter queues fill silently — all while your application looks healthy from the outside. CloudWatch can graph queue depth, but it lives inside your AWS account and can be misconfigured by the same team that caused the problem. Vigilmon adds an independent external layer: HTTP health checks on your consumer services, heartbeat monitors for scheduled workers, and webhook alerts when things go wrong.

This tutorial shows you how to build meaningful monitoring for SQS-based architectures using Vigilmon.

What You'll Build

  • A health endpoint on your SQS consumer service that checks real queue connectivity
  • A Vigilmon HTTP monitor with appropriate timeout settings for queue consumers
  • A heartbeat pattern for batch/scheduled SQS workers
  • Dead-letter queue (DLQ) awareness via a lightweight Lambda probe
  • An alerting strategy for queue depth spikes and consumer failures

Prerequisites

  • AWS account with at least one SQS queue and a consumer service (any runtime)
  • AWS CLI or CDK/SAM configured locally
  • A free account at vigilmon.online

Step 1: Add a Health Endpoint to Your Consumer Service

Your SQS consumer is typically a long-running service (ECS task, EC2 instance, Lambda with event source mapping). Add a /health endpoint that probes real SQS connectivity.

Node.js (Express)

// routes/health.js
import { SQSClient, GetQueueAttributesCommand } from "@aws-sdk/client-sqs";

const sqs = new SQSClient({});

export async function healthHandler(req, res) {
  const checks = {};
  let ok = true;

  // Verify we can read the queue
  try {
    const result = await sqs.send(
      new GetQueueAttributesCommand({
        QueueUrl: process.env.QUEUE_URL,
        AttributeNames: ["ApproximateNumberOfMessages", "ApproximateNumberOfMessagesNotVisible"],
      })
    );
    const depth = parseInt(result.Attributes.ApproximateNumberOfMessages, 10);
    const inFlight = parseInt(result.Attributes.ApproximateNumberOfMessagesNotVisible, 10);
    checks.sqs = "ok";
    checks.queueDepth = depth;
    checks.inFlight = inFlight;

    // Alert-level check: flag if queue is unusually deep
    if (depth > parseInt(process.env.QUEUE_DEPTH_WARN || "1000", 10)) {
      checks.sqs = "backlogged";
      ok = false;
    }
  } catch (err) {
    checks.sqs = `error: ${err.message}`;
    ok = false;
  }

  res.status(ok ? 200 : 503).json({
    status: ok ? "ok" : "degraded",
    service: "sqs-consumer",
    checks,
  });
}

Python (FastAPI)

# routers/health.py
import os
import boto3
from fastapi import APIRouter
from botocore.exceptions import ClientError

router = APIRouter()
sqs = boto3.client("sqs")

@router.get("/health")
async def health():
    checks = {}
    ok = True

    try:
        resp = sqs.get_queue_attributes(
            QueueUrl=os.environ["QUEUE_URL"],
            AttributeNames=["ApproximateNumberOfMessages"],
        )
        depth = int(resp["Attributes"]["ApproximateNumberOfMessages"])
        checks["sqs"] = "ok"
        checks["queueDepth"] = depth
        warn_threshold = int(os.environ.get("QUEUE_DEPTH_WARN", "1000"))
        if depth > warn_threshold:
            checks["sqs"] = "backlogged"
            ok = False
    except ClientError as e:
        checks["sqs"] = f"error: {e.response['Error']['Code']}"
        ok = False

    return {
        "status": "ok" if ok else "degraded",
        "service": "sqs-consumer",
        "checks": checks,
    }

Go (net/http)

// internal/health/handler.go
package health

import (
    "context"
    "encoding/json"
    "net/http"
    "os"
    "strconv"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/service/sqs"
)

func Handler(client *sqs.Client) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        checks := map[string]any{}
        ok := true

        out, err := client.GetQueueAttributes(context.Background(), &sqs.GetQueueAttributesInput{
            QueueUrl:       aws.String(os.Getenv("QUEUE_URL")),
            AttributeNames: []types.QueueAttributeName{"ApproximateNumberOfMessages"},
        })
        if err != nil {
            checks["sqs"] = "error: " + err.Error()
            ok = false
        } else {
            depth, _ := strconv.Atoi(out.Attributes["ApproximateNumberOfMessages"])
            checks["sqs"] = "ok"
            checks["queueDepth"] = depth
        }

        w.Header().Set("Content-Type", "application/json")
        if !ok {
            w.WriteHeader(http.StatusServiceUnavailable)
        }
        json.NewEncoder(w).Encode(map[string]any{"status": map[bool]string{true: "ok", false: "degraded"}[ok], "checks": checks})
    }
}

Step 2: Configure the Vigilmon HTTP Monitor

  1. Log in to VigilmonAdd Monitor → HTTP.
  2. URL: your consumer service's health endpoint (e.g. https://consumer.example.com/health).
  3. Check interval: 60 seconds.
  4. Response timeout: 10 seconds.
  5. Expected status: 200.
  6. JSON assertion: path status, expected value ok.

Tip: If your consumer runs inside a VPC with no public endpoint, expose the health route through an internal ALB or API Gateway VPC link, or use a lightweight reverse proxy (nginx, Caddy) that makes the health port accessible externally.


Step 3: Heartbeat Monitoring for SQS Workers

Scheduled or batch workers that drain SQS queues are invisible to HTTP monitors. Use Vigilmon's Heartbeat monitor and have the worker ping it after each successful batch.

Worker heartbeat (Node.js)

// worker/index.mjs
import { SQSClient, ReceiveMessageCommand, DeleteMessageCommand } from "@aws-sdk/client-sqs";

const sqs = new SQSClient({});

async function processBatch() {
  const { Messages } = await sqs.send(
    new ReceiveMessageCommand({
      QueueUrl: process.env.QUEUE_URL,
      MaxNumberOfMessages: 10,
      WaitTimeSeconds: 20,
    })
  );

  if (!Messages?.length) return;

  for (const msg of Messages) {
    await processMessage(msg);
    await sqs.send(
      new DeleteMessageCommand({
        QueueUrl: process.env.QUEUE_URL,
        ReceiptHandle: msg.ReceiptHandle,
      })
    );
  }

  // Ping Vigilmon: heartbeat only fires when work succeeded
  await fetch(process.env.VIGILMON_HEARTBEAT_URL, { method: "POST" });
}

// Long-poll loop
while (true) {
  await processBatch().catch(console.error);
}

In Vigilmon, create a Heartbeat monitor:

  • Set the grace period to 2× your expected poll interval (e.g., 4 minutes for a 2-minute loop).
  • If the heartbeat stops arriving, Vigilmon fires an alert — meaning your worker crashed or the queue drained unexpectedly.

Step 4: Dead-Letter Queue Probe with Lambda

When messages land in a DLQ, your application has a silent failure. Run a lightweight Lambda on a schedule to check DLQ depth and ping a Vigilmon heartbeat only when the DLQ is empty.

// dlq-probe/index.mjs
import { SQSClient, GetQueueAttributesCommand } from "@aws-sdk/client-sqs";

const sqs = new SQSClient({});

export async function handler() {
  const result = await sqs.send(
    new GetQueueAttributesCommand({
      QueueUrl: process.env.DLQ_URL,
      AttributeNames: ["ApproximateNumberOfMessages"],
    })
  );

  const dlqDepth = parseInt(result.Attributes.ApproximateNumberOfMessages, 10);

  if (dlqDepth === 0) {
    // DLQ is healthy — send heartbeat
    await fetch(process.env.VIGILMON_HEARTBEAT_URL, { method: "POST" });
    console.log("DLQ is empty — heartbeat sent");
  } else {
    // Do NOT ping — Vigilmon will fire a missed-heartbeat alert
    console.warn(`DLQ has ${dlqDepth} messages — withholding heartbeat`);
  }
}

Schedule this Lambda every 5 minutes with EventBridge. In Vigilmon, set the heartbeat grace period to 10 minutes. Any messages in the DLQ for more than 10 minutes will trigger an alert.


Step 5: Alerting Strategy

| Alert channel | When to use | |---|---| | Email / PagerDuty | Consumer service returns 503 (queue unreachable) | | Slack webhook | Heartbeat missed (worker stalled) | | Slack + Email | DLQ heartbeat missed (messages failing processing) | | Status page | Public-facing services backed by SQS |

Configure alert escalation in Vigilmon: notify immediately on first failure, re-notify after 5 minutes if still down. This catches real outages while tolerating transient SQS API blips.


What Vigilmon Catches That CloudWatch Misses

| Scenario | CloudWatch | Vigilmon | |---|---|---| | Consumer service crashes (HTTP 503) | Needs explicit ALB target group alarm | HTTP monitor catches immediately | | Worker silently stops processing | Needs metric math on NumberOfMessagesSent | Heartbeat grace period fires alert | | DLQ accumulates failed messages | Needs DLQ depth alarm + SNS | DLQ probe + heartbeat monitor | | AWS Console or IAM access breaks | Your alarms may also fail | Vigilmon is external — unaffected | | Cross-account queue access issue | Needs account-level alarm | HTTP monitor catches at the consumer |


SQS makes async architectures resilient, but resilience is invisible without monitoring. Consumer crashes, worker stalls, and DLQ backlogs all look like silence. External health checks from Vigilmon give you the independent signal you need — before your customers notice.

Start monitoring your SQS consumers 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 →