AWS SNS fans messages out to dozens of subscribers in milliseconds — but when a subscriber is unhealthy, SNS keeps retrying silently until it gives up and drops the message. You won't see a red dashboard; you'll see a missing order confirmation email six hours after the Lambda started returning 500s. CloudWatch can track delivery failures, but it's inside your AWS account and can be disrupted by the same incident you're trying to detect. Vigilmon gives you an external, independent view: HTTP monitors on your subscriber services, heartbeat monitors for topic workers, and delivery failure probes that alert before messages are lost.
This tutorial walks you through monitoring SNS-based architectures with Vigilmon.
What You'll Build
- A health endpoint on your SNS subscriber service that probes real topic access
- A Vigilmon HTTP monitor with appropriate settings for push subscribers
- A heartbeat pattern for Lambda and SQS subscribers
- A delivery failure probe that withholds a heartbeat when failures accumulate
- An alerting strategy for subscriber outages and topic delivery problems
Prerequisites
- AWS account with at least one SNS topic and a subscriber service (HTTP endpoint, Lambda, or SQS)
- AWS CLI or CDK/SAM configured locally
- A free account at vigilmon.online
Step 1: Add a Health Endpoint to Your Subscriber Service
HTTP/HTTPS subscribers are the easiest to monitor: SNS pushes directly to your endpoint. Add a /health route that checks real SNS connectivity.
Node.js (Express)
// routes/health.js
import { SNSClient, GetTopicAttributesCommand } from "@aws-sdk/client-sns";
const sns = new SNSClient({});
export async function healthHandler(req, res) {
const checks = {};
let ok = true;
// Verify we can read the topic we're subscribed to
try {
const result = await sns.send(
new GetTopicAttributesCommand({
TopicArn: process.env.SNS_TOPIC_ARN,
})
);
const subsCount = result.Attributes?.SubscriptionsConfirmed ?? "unknown";
checks.sns = "ok";
checks.confirmedSubscriptions = subsCount;
} catch (err) {
checks.sns = `error: ${err.message}`;
ok = false;
}
// Check our own subscription is still confirmed
try {
const { SNSClient: SNS, ListSubscriptionsByTopicCommand } = await import("@aws-sdk/client-sns");
const result = await sns.send(
new ListSubscriptionsByTopicCommand({ TopicArn: process.env.SNS_TOPIC_ARN })
);
const sub = result.Subscriptions?.find(
(s) => s.Endpoint === process.env.SERVICE_URL + "/sns"
);
checks.subscription = sub?.SubscriptionArn?.startsWith("arn:") ? "confirmed" : "pending_or_missing";
if (checks.subscription !== "confirmed") ok = false;
} catch (err) {
checks.subscription = `error: ${err.message}`;
ok = false;
}
res.status(ok ? 200 : 503).json({
status: ok ? "ok" : "degraded",
service: "sns-subscriber",
checks,
});
}
Python (FastAPI)
# routers/health.py
import os
import boto3
from fastapi import APIRouter
from botocore.exceptions import ClientError
router = APIRouter()
sns = boto3.client("sns")
@router.get("/health")
async def health():
checks = {}
ok = True
try:
resp = sns.get_topic_attributes(TopicArn=os.environ["SNS_TOPIC_ARN"])
checks["sns"] = "ok"
checks["confirmedSubscriptions"] = resp["Attributes"].get("SubscriptionsConfirmed", "unknown")
except ClientError as e:
checks["sns"] = f"error: {e.response['Error']['Code']}"
ok = False
return {
"status": "ok" if ok else "degraded",
"service": "sns-subscriber",
"checks": checks,
}
Go (net/http)
// internal/health/handler.go
package health
import (
"context"
"encoding/json"
"net/http"
"os"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sns"
)
func Handler(client *sns.Client) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
checks := map[string]any{}
ok := true
_, err := client.GetTopicAttributes(context.Background(), &sns.GetTopicAttributesInput{
TopicArn: aws.String(os.Getenv("SNS_TOPIC_ARN")),
})
if err != nil {
checks["sns"] = "error: " + err.Error()
ok = false
} else {
checks["sns"] = "ok"
}
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],
"service": "sns-subscriber",
"checks": checks,
})
}
}
Step 2: Configure the Vigilmon HTTP Monitor
- Log in to Vigilmon → Add Monitor → HTTP.
- URL: your subscriber service health endpoint (e.g.
https://notifications.example.com/health). - Check interval: 60 seconds.
- Response timeout: 10 seconds.
- Expected status:
200. - JSON assertion: path
status, expected valueok.
Tip: SNS HTTP subscribers must respond to the subscription confirmation request before they receive messages. If your health endpoint returns 200 but your
/snshandler is broken, messages are silently dropped. Include asubscription: confirmedcheck in the health response as shown above.
Step 3: Heartbeat Monitoring for Lambda Subscribers
Lambda subscribers run on-demand — there is no persistent process to probe. Use Vigilmon's Heartbeat monitor and ping it after successful message processing.
// handlers/notifications.mjs
export async function handler(event) {
for (const record of event.Records) {
const message = JSON.parse(record.Sns.Message);
await processNotification(message);
}
// Ping Vigilmon heartbeat after successful batch
await fetch(process.env.VIGILMON_HEARTBEAT_URL, { method: "POST" });
console.log(`Processed ${event.Records.length} SNS records — heartbeat sent`);
}
Set the grace period to 2–3× the expected maximum gap between topic messages. For critical notification pipelines (email sends, payment events), set it as low as 5 minutes.
Store the heartbeat URL in SSM Parameter Store:
aws ssm put-parameter \
--name "/myapp/prod/vigilmon-heartbeat-sns" \
--value "https://vigilmon.online/api/heartbeat/<your-id>" \
--type SecureString
Reference it in your SAM/CDK configuration as an environment variable resolved from SSM.
Step 4: Delivery Failure Probe with CloudWatch
SNS records delivery failures in CloudWatch. A lightweight Lambda scheduled every 5 minutes can read that metric and withhold a Vigilmon heartbeat when failures accumulate.
# dlf-probe/handler.py
import os
import boto3
from datetime import datetime, timedelta, timezone
cloudwatch = boto3.client("cloudwatch")
def handler(event, context):
end = datetime.now(timezone.utc)
start = end - timedelta(minutes=5)
resp = cloudwatch.get_metric_statistics(
Namespace="AWS/SNS",
MetricName="NumberOfNotificationsFailed",
Dimensions=[{"Name": "TopicName", "Value": os.environ["TOPIC_NAME"]}],
StartTime=start,
EndTime=end,
Period=300,
Statistics=["Sum"],
)
total_failures = sum(dp["Sum"] for dp in resp.get("Datapoints", []))
if total_failures == 0:
# No failures — send heartbeat
import urllib.request
req = urllib.request.Request(
os.environ["VIGILMON_HEARTBEAT_URL"],
method="POST"
)
urllib.request.urlopen(req)
print("No delivery failures — heartbeat sent")
else:
# Withhold heartbeat — Vigilmon will alert
print(f"SNS delivery failures: {int(total_failures)} — heartbeat withheld")
Schedule with EventBridge every 5 minutes. Set the Vigilmon heartbeat grace period to 10 minutes.
Step 5: Fan-Out Architecture Monitoring
SNS fan-out (one topic → multiple SQS queues, Lambdas, HTTP endpoints) means one topic failure can break multiple downstream services. Monitor each subscriber independently.
| Vigilmon monitor | What it watches |
|---|---|
| notifications-http | HTTP subscriber health |
| notifications-lambda-heartbeat | Lambda subscriber heartbeat |
| notifications-sqs-depth | SQS dead-letter queue probe |
| delivery-failures-heartbeat | CloudWatch failure probe |
Group them under one Status Page for a unified view. Configure alerts so the whole team sees the same incident, not four separate Slack messages.
Step 6: Alerting Strategy
| Alert type | Recommended channel | |---|---| | HTTP subscriber 503 | Email + PagerDuty | | Lambda heartbeat missed | Slack + PagerDuty | | Delivery failure probe triggered | Email + Slack | | Multiple subscribers degraded | Status page + PagerDuty escalation |
Use alert escalation: notify immediately, re-notify every 5 minutes if still down. SNS delivery retries exhaust quickly (default 3 retries over ~20 seconds for HTTP), so detection needs to be fast.
What Vigilmon Catches That CloudWatch Misses
| Scenario | CloudWatch | Vigilmon | |---|---|---| | HTTP subscriber returns 503 | Needs DeliveryFailures alarm | HTTP monitor catches immediately | | Lambda subscriber stops processing | Needs custom metric | Heartbeat grace period fires alert | | Subscription accidentally unconfirmed | No built-in alert | Health endpoint confirms subscription status | | AWS account has IAM issue | Your alarms may also break | Vigilmon is external — unaffected | | Delivery failures accumulate silently | Needs metric alarm + SNS (ironic) | Failure probe + heartbeat covers this |
SNS looks reliable until a subscriber goes down and messages disappear without a trace. Delivery retries are silent, and by the time you notice, events are lost. External monitoring from Vigilmon gives you the independent signal you need — before your message pipeline runs dry.
Start monitoring your SNS subscribers in under 5 minutes — register free at vigilmon.online.