Guide · CloudWatch Synthetics
CloudWatch Synthetics Alarms — SuccessPercent metric, PutMetricAlarm, SNS
CloudWatch Synthetics canaries automatically publish metrics to the CloudWatchSynthetics namespace after every run — you do not emit them manually. The key metric for alerting is SuccessPercent: it is 100 when all runs in a period passed, 0 when all failed, and proportional values in between. Create a CloudWatch alarm on this metric using PutMetricAlarmCommand from @aws-sdk/client-cloudwatch, and wire it to an SNS topic for email, Slack webhook, or PagerDuty delivery. The two settings that determine alarm sensitivity are Threshold (the minimum acceptable pass rate — typically 100 for a strict alert, or 80–90 for a flapping-resilient one) and EvaluationPeriods + DatapointsToAlarm (how many consecutive periods must breach before the alarm fires).
TL;DR
Use PutMetricAlarmCommand from @aws-sdk/client-cloudwatch. Set Namespace: 'CloudWatchSynthetics', MetricName: 'SuccessPercent', Dimensions: [{ Name: 'CanaryName', Value: 'my-canary' }]. Set Threshold: 100, ComparisonOperator: 'LessThanThreshold', EvaluationPeriods: 2, DatapointsToAlarm: 2 to alarm after 2 consecutive failures. Set Period to match the canary schedule interval (e.g., 300 for a 5-minute canary). Add AlarmActions: [snsTopicArn] for notifications. The alarm IAM requirement is cloudwatch:PutMetricAlarm for your MCP server role.
CloudWatch Synthetics metrics reference
All metrics are published to the CloudWatchSynthetics namespace with the CanaryName dimension. They are emitted after each run completes — not on a fixed time interval.
| Metric name | Unit | Description |
|---|---|---|
SuccessPercent | Percent | 100 if the run passed, 0 if it failed. Aggregate over multiple data points to compute a pass rate. |
Duration | Milliseconds | Total execution time for the canary run, from Lambda cold start through all steps completing. |
Failed | Count | 1 for each failed run, 0 for each passed run. Use Sum to count total failures in a window. |
FailedRequests | Count | Number of individual HTTP requests within a run that received non-2xx responses (browser canaries only). |
For step-level metrics (individual pass/fail per named step), CloudWatch Synthetics also publishes metrics under CloudWatchSynthetics with both CanaryName and StepName dimensions. These are only available in the console; the API does not expose step-level metrics separately — use the run artifact logs for step-level debugging.
Creating a canary failure alarm with PutMetricAlarmCommand
The alarm fires when SuccessPercent falls below the threshold for the specified number of consecutive evaluation periods. Set Period to match the canary schedule interval so each evaluation period corresponds to one canary run.
import {
CloudWatchClient,
PutMetricAlarmCommand,
type PutMetricAlarmCommandInput,
} from '@aws-sdk/client-cloudwatch';
const cw = new CloudWatchClient({ region: process.env.AWS_REGION ?? 'us-east-1' });
export async function createCanaryAlarm(params: {
canaryName: string;
alarmName?: string; // defaults to '{canaryName}-failure-alarm'
snsTopicArn: string; // ARN of the SNS topic to notify
scheduleIntervalSeconds: number; // must match the canary's schedule interval
consecutiveFailuresToAlarm?: number; // default 2 — alarms after N consecutive failures
}): Promise<void> {
const alarmName = params.alarmName ?? `${params.canaryName}-failure-alarm`;
const datapoints = params.consecutiveFailuresToAlarm ?? 2;
const input: PutMetricAlarmCommandInput = {
AlarmName: alarmName,
AlarmDescription: `CloudWatch Synthetics canary ${params.canaryName} is failing`,
// The metric to monitor
Namespace: 'CloudWatchSynthetics',
MetricName: 'SuccessPercent',
Dimensions: [{ Name: 'CanaryName', Value: params.canaryName }],
// Period must match the canary schedule interval so each period = 1 run
Period: params.scheduleIntervalSeconds, // e.g., 300 for a 5-minute canary
// Use Average — SuccessPercent is 100 or 0 per run, so Average = pass rate
Statistic: 'Average',
// Alarm fires when SuccessPercent < 100 (i.e., any failure)
ComparisonOperator: 'LessThanThreshold',
Threshold: 100,
// Require 2 consecutive failures before alarming (reduces flapping)
EvaluationPeriods: datapoints,
DatapointsToAlarm: datapoints,
// TreatMissingData: 'notBreaching' means no data = OK (canary might be stopped)
// Use 'breaching' if you want missing data (canary down) to also alarm
TreatMissingData: 'notBreaching',
// Actions: SNS topic ARN to notify when alarm fires
AlarmActions: [params.snsTopicArn],
OKActions: [params.snsTopicArn], // also notify when alarm resolves
InsufficientDataActions: [], // no action when data is missing
};
await cw.send(new PutMetricAlarmCommand(input));
}
Setting DatapointsToAlarm to 2 out of EvaluationPeriods 2 means the alarm fires only after two consecutive canary failures. This eliminates false positives from single transient failures (network blips, cold-start timeouts). For higher-stakes endpoints, set both to 1 to alarm on the first failure.
Setting up an SNS topic for canary notifications
The alarm fires by publishing to an SNS topic. Create the topic and add subscribers before wiring it to the alarm.
import {
SNSClient,
CreateTopicCommand,
SubscribeCommand,
} from '@aws-sdk/client-sns';
const sns = new SNSClient({ region: process.env.AWS_REGION ?? 'us-east-1' });
export async function createAlertTopic(params: {
topicName: string;
emailAddresses?: string[]; // email subscribers
httpsEndpoints?: string[]; // webhook URLs for Slack/PagerDuty
}): Promise<string> { // returns topic ARN
// Create the topic
const createResponse = await sns.send(
new CreateTopicCommand({ Name: params.topicName })
);
const topicArn = createResponse.TopicArn!;
// Add email subscribers
for (const email of params.emailAddresses ?? []) {
await sns.send(new SubscribeCommand({
TopicArn: topicArn,
Protocol: 'email',
Endpoint: email,
// Email subscriptions require confirmation — the subscriber
// receives a confirmation email and must click to activate
}));
}
// Add HTTPS webhook endpoints (for Slack incoming webhooks, PagerDuty, etc.)
for (const url of params.httpsEndpoints ?? []) {
await sns.send(new SubscribeCommand({
TopicArn: topicArn,
Protocol: 'https',
Endpoint: url,
// HTTPS subscriptions auto-confirm if the endpoint returns 200
// Slack/PagerDuty webhooks do — custom endpoints must handle SNS confirmation
}));
}
return topicArn;
}
// Full setup: create topic + create alarm in one call
export async function setupCanaryAlerting(params: {
canaryName: string;
scheduleIntervalSeconds: number;
alertEmails: string[];
}) {
// 1. Create SNS topic
const topicArn = await createAlertTopic({
topicName: `${params.canaryName}-alerts`,
emailAddresses: params.alertEmails,
});
// 2. Create the alarm (SNS topic must exist first)
await createCanaryAlarm({
canaryName: params.canaryName,
snsTopicArn: topicArn,
scheduleIntervalSeconds: params.scheduleIntervalSeconds,
});
return topicArn;
}
Alarm for sustained partial failure (flapping resilience)
The default Threshold: 100 alarms on any single failure. For endpoints that have occasional transient failures (acceptable pass rate 95%), use a threshold below 100 and a longer evaluation window.
// Alarm if average pass rate drops below 80% over the last 6 runs
export async function createFlappingResilientAlarm(
canaryName: string,
snsTopicArn: string,
scheduleIntervalSeconds: number,
): Promise<void> {
await cw.send(new PutMetricAlarmCommand({
AlarmName: `${canaryName}-degraded-alarm`,
AlarmDescription: `Canary ${canaryName} pass rate below 80% over last 6 runs`,
Namespace: 'CloudWatchSynthetics',
MetricName: 'SuccessPercent',
Dimensions: [{ Name: 'CanaryName', Value: canaryName }],
Period: scheduleIntervalSeconds,
Statistic: 'Average',
// Alarm when average SuccessPercent is below 80 over 6 consecutive periods
ComparisonOperator: 'LessThanThreshold',
Threshold: 80,
EvaluationPeriods: 6,
DatapointsToAlarm: 4, // 4 out of 6 periods below threshold = sustained degradation
TreatMissingData: 'notBreaching',
AlarmActions: [snsTopicArn],
OKActions: [snsTopicArn],
}));
}
Composite alarms for multi-canary monitoring
When you have multiple canaries monitoring different aspects of the same service, a composite alarm lets you aggregate them — alarming only when multiple individual alarms are in ALARM state simultaneously, reducing noise from isolated endpoint failures.
import {
PutCompositeAlarmCommand,
} from '@aws-sdk/client-cloudwatch';
export async function createCompositeAlarm(params: {
compositeName: string;
childAlarmNames: string[]; // must all exist before creating composite
snsTopicArn: string;
// alarmRule: fires when ANY child alarm is in ALARM state
operator?: 'ANY' | 'ALL'; // default: ANY
}): Promise<void> {
const operator = params.operator ?? 'ANY';
// Build rule: "ALARM(alarm1) OR ALARM(alarm2) OR ..."
const alarmRule = operator === 'ANY'
? params.childAlarmNames.map(n => `ALARM("${n}")`).join(' OR ')
: params.childAlarmNames.map(n => `ALARM("${n}")`).join(' AND ');
await cw.send(new PutCompositeAlarmCommand({
AlarmName: params.compositeName,
AlarmDescription: `Composite alarm — ${operator} of ${params.childAlarmNames.length} canary alarms`,
AlarmRule: alarmRule,
AlarmActions: [params.snsTopicArn],
OKActions: [params.snsTopicArn],
}));
}
// Example: notify only when BOTH health check and login flow canaries fail
// (reduces false positives from isolated network issues)
await createCompositeAlarm({
compositeName: 'api-platform-down',
childAlarmNames: ['health-check-canary-alarm', 'login-flow-canary-alarm'],
snsTopicArn: 'arn:aws:sns:us-east-1:123456789012:on-call-alerts',
operator: 'ALL',
});
Reading alarm state from an MCP tool
Use DescribeAlarmsCommand to retrieve the current alarm state for status dashboards or automated remediation workflows.
import {
DescribeAlarmsCommand,
type MetricAlarm,
} from '@aws-sdk/client-cloudwatch';
export async function getAlarmState(alarmName: string): Promise<{
state: string;
reason: string;
updatedAt: Date | undefined;
}> {
const response = await cw.send(
new DescribeAlarmsCommand({ AlarmNames: [alarmName] })
);
const alarm: MetricAlarm | undefined = response.MetricAlarms?.[0];
if (!alarm) throw new Error(`Alarm not found: ${alarmName}`);
return {
state: alarm.StateValue ?? 'UNKNOWN', // 'OK' | 'ALARM' | 'INSUFFICIENT_DATA'
reason: alarm.StateReason ?? '',
updatedAt: alarm.StateUpdatedTimestamp,
};
}
// Get alarm state for all canary alarms
export async function getAllCanaryAlarmStates(canaryNames: string[]) {
const alarmNames = canaryNames.map(n => `${n}-failure-alarm`);
const response = await cw.send(
new DescribeAlarmsCommand({
AlarmNames: alarmNames,
MaxRecords: 100,
})
);
return Object.fromEntries(
(response.MetricAlarms ?? []).map(alarm => [
// Strip the '-failure-alarm' suffix to get back the canary name
alarm.AlarmName?.replace('-failure-alarm', '') ?? alarm.AlarmName,
{
state: alarm.StateValue,
reason: alarm.StateReason,
updatedAt: alarm.StateUpdatedTimestamp,
},
])
);
}
Alarm states: OK (threshold not breached), ALARM (threshold breached — notification sent), INSUFFICIENT_DATA (no data in the evaluation window — canary may be stopped or newly created). The TreatMissingData setting controls whether INSUFFICIENT_DATA transitions to ALARM or stays INSUFFICIENT_DATA.
IAM requirements
The MCP server role that creates and manages alarms needs the following permissions. The SNS publish permission is needed only if the MCP server itself publishes to SNS — for alarm-triggered SNS, CloudWatch handles publishing automatically with its own service role.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ManageCanaryAlarms",
"Effect": "Allow",
"Action": [
"cloudwatch:PutMetricAlarm",
"cloudwatch:DeleteAlarms",
"cloudwatch:DescribeAlarms",
"cloudwatch:DescribeAlarmHistory",
"cloudwatch:PutCompositeAlarm"
],
"Resource": "*"
},
{
"Sid": "ManageSNSTopics",
"Effect": "Allow",
"Action": [
"sns:CreateTopic",
"sns:Subscribe",
"sns:ListSubscriptionsByTopic",
"sns:GetTopicAttributes"
],
"Resource": "*"
},
{
"Sid": "GetSyntheticsMetrics",
"Effect": "Allow",
"Action": [
"cloudwatch:GetMetricStatistics",
"cloudwatch:GetMetricData",
"cloudwatch:ListMetrics"
],
"Resource": "*"
}
]
}
CloudWatch alarms that publish to SNS do not require any SNS permission on the calling IAM role — the CloudWatch service has its own permissions to publish to SNS topics. However, the SNS topic must have a resource policy allowing CloudWatch to publish to it. When creating topics via the console, this policy is added automatically; when creating via SDK, you may need to add it explicitly using SetTopicAttributesCommand to set the Policy attribute.
Error handling reference
| Error | Cause | Fix |
|---|---|---|
InvalidParameterValueException | Period not aligned with canary schedule, or DatapointsToAlarm > EvaluationPeriods | Set Period equal to canary schedule interval; DatapointsToAlarm must be ≤ EvaluationPeriods |
LimitExceededException | Account alarm limit reached (default 5000 metric alarms) | Delete unused alarms or request quota increase via Service Quotas |
ResourceNotFoundException | PutCompositeAlarm references a child alarm that does not exist | Create all child alarms before creating the composite alarm |
| Alarm stays INSUFFICIENT_DATA | Canary is stopped, or metric name/dimension is wrong | Verify namespace is CloudWatchSynthetics and dimension name is CanaryName (case-sensitive) |
| Alarm fires but no SNS email received | Email subscription not confirmed, or SNS topic in different region | Check SNS subscription status — email subscriptions require manual confirmation click; SNS topic must be in the same region as the alarm |
Related pages
- CloudWatch Synthetics overview — CreateCanaryCommand, canary lifecycle, IAM
- Writing API canary scripts — Node.js handler, synthetics module, packaging
- Reading canary run results — GetCanaryRunsCommand, run states, artifact retrieval
- CloudWatch alarm management
- CloudWatch composite alarms