Guide · CloudWatch Synthetics
CloudWatch Synthetics GetCanaryRunsCommand — run status, artifacts, state machine
To read the history of a CloudWatch Synthetics canary, use GetCanaryRunsCommand from @aws-sdk/client-synthetics. This returns a paginated list of CanaryRun objects — each with a status state (RUNNING, PASSED, or FAILED), timing fields, and an S3 artifact location pointing to the run's logs and screenshots. For dashboard use-cases where you need just the last run result for many canaries at once, DescribeCanariesLastRunCommand is more efficient than calling GetCanaryRunsCommand per canary. The two things to get right: the run Status.State field (PASSED/FAILED) is different from the canary's lifecycle state (RUNNING/STOPPED), and artifact retrieval requires separate S3 SDK calls — the ArtifactS3Location is an S3 URI prefix, not a direct download URL.
TL;DR
Call GetCanaryRunsCommand({ Name: 'my-canary' }) to get recent run history. Each CanaryRun has Status.State (PASSED/FAILED/RUNNING), Timeline.Started/Timeline.Completed, and ArtifactS3Location (S3 prefix for logs and screenshots). For the latest run across all canaries in one call, use DescribeCanariesLastRunCommand. Paginate with NextToken. Run data is retained for the number of days configured in SuccessRetentionPeriodInDays and FailureRetentionPeriodInDays when the canary was created.
Getting recent canary runs with GetCanaryRunsCommand
GetCanaryRunsCommand returns runs for a single canary, ordered by start time descending (most recent first). The default page size returns up to 100 runs; set MaxResults to limit the response.
import {
SyntheticsClient,
GetCanaryRunsCommand,
type CanaryRun,
} from '@aws-sdk/client-synthetics';
const synthetics = new SyntheticsClient({
region: process.env.AWS_REGION ?? 'us-east-1',
});
export async function getRecentRuns(
canaryName: string,
maxResults = 10,
): Promise<CanaryRun[]> {
const response = await synthetics.send(
new GetCanaryRunsCommand({
Name: canaryName,
MaxResults: maxResults,
})
);
return response.CanaryRuns ?? [];
}
// Example usage
const runs = await getRecentRuns('my-api-canary', 5);
for (const run of runs) {
console.log({
runId: run.Id,
state: run.Status?.State, // 'PASSED', 'FAILED', or 'RUNNING'
stateReason: run.Status?.StateReason, // error message if FAILED
startedAt: run.Timeline?.Started,
completedAt: run.Timeline?.Completed,
durationMs: run.Timeline?.Completed && run.Timeline.Started
? run.Timeline.Completed.getTime() - run.Timeline.Started.getTime()
: null,
artifactPrefix: run.ArtifactS3Location, // S3 prefix for logs/screenshots
});
}
Canary run state machine
Each CanaryRun.Status.State reflects the outcome of a single execution. This is distinct from the canary lifecycle state (RUNNING means the canary is scheduled and active — not that the current run is in progress).
| Run state | Meaning | Notes |
|---|---|---|
RUNNING | This specific run is currently executing | No completed time yet; ArtifactS3Location may be empty |
PASSED | All steps completed without throwing an error | Contributes to SuccessPercent metric = 100 |
FAILED | At least one step threw or the timeout was exceeded | Check Status.StateReason for the error message |
A canary run enters FAILED state when: (1) the script's handler function throws an unhandled error, (2) a step passed to executeStep throws, (3) the run exceeds RunConfig.TimeoutInSeconds, or (4) the Lambda cold-start + execution exceeds the Lambda timeout (the Synthetics runtime sets the Lambda timeout to RunConfig.TimeoutInSeconds + 60 as a safety margin). The Status.StateReason field contains the first uncaught error message — use it to distinguish assertion failures from infrastructure errors like Lambda out-of-memory.
Paginating through all run history
Use the NextToken field for pagination when you need more than one page of results. Run history is bounded by SuccessRetentionPeriodInDays and FailureRetentionPeriodInDays set at canary creation time — runs older than the retention period are automatically deleted and won't appear in the response.
export async function getAllRunsForTimeRange(
canaryName: string,
since: Date,
): Promise<CanaryRun[]> {
const allRuns: CanaryRun[] = [];
let nextToken: string | undefined;
do {
const response = await synthetics.send(
new GetCanaryRunsCommand({
Name: canaryName,
MaxResults: 100,
NextToken: nextToken,
})
);
const runs = response.CanaryRuns ?? [];
// Runs are ordered newest-first; stop when we pass the time range
const relevantRuns = runs.filter(
run => run.Timeline?.Started && run.Timeline.Started >= since
);
allRuns.push(...relevantRuns);
// If the last run in this page is older than 'since', stop paginating
const lastRun = runs[runs.length - 1];
if (lastRun?.Timeline?.Started && lastRun.Timeline.Started < since) {
break;
}
nextToken = response.NextToken;
} while (nextToken);
return allRuns;
}
// Compute pass rate for a time range
export function computePassRate(runs: CanaryRun[]): number {
const completed = runs.filter(r => r.Status?.State !== 'RUNNING');
if (completed.length === 0) return 1;
const passed = completed.filter(r => r.Status?.State === 'PASSED').length;
return passed / completed.length;
}
Getting the last run for all canaries with DescribeCanariesLastRunCommand
When building a monitoring dashboard that shows the current status of every canary, DescribeCanariesLastRunCommand is more efficient than calling GetCanaryRunsCommand per canary — it returns the most recent run for all canaries in one (paginated) API call.
import {
DescribeCanariesLastRunCommand,
type CanaryLastRun,
} from '@aws-sdk/client-synthetics';
export async function getDashboardStatus(): Promise<CanaryLastRun[]> {
const results: CanaryLastRun[] = [];
let nextToken: string | undefined;
do {
const response = await synthetics.send(
new DescribeCanariesLastRunCommand({
NextToken: nextToken,
MaxResults: 50,
})
);
results.push(...(response.CanariesLastRun ?? []));
nextToken = response.NextToken;
} while (nextToken);
return results;
}
// Build a status map: canaryName → { state, lastRunAt, reason }
export async function buildStatusMap() {
const lastRuns = await getDashboardStatus();
return Object.fromEntries(
lastRuns.map(entry => [
entry.CanaryName,
{
state: entry.LastRun?.Status?.State ?? 'UNKNOWN',
reason: entry.LastRun?.Status?.StateReason,
lastRunAt: entry.LastRun?.Timeline?.Completed,
durationMs: entry.LastRun?.Timeline?.Completed && entry.LastRun.Timeline.Started
? entry.LastRun.Timeline.Completed.getTime() - entry.LastRun.Timeline.Started.getTime()
: null,
},
])
);
}
DescribeCanariesLastRunCommand returns a CanaryLastRun structure — slightly different from CanaryRun. The run data is nested under LastRun and includes the same Status.State, Timeline, and ArtifactS3Location fields as individual run results.
Retrieving run artifacts from S3
The ArtifactS3Location field on a CanaryRun is an S3 URI prefix (e.g., s3://my-bucket/canary-artifacts/my-canary/YYYY/MM/DD/HH/MM/SS/). To read run logs, list objects under that prefix and download the files you need.
import { S3Client, ListObjectsV2Command, GetObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({ region: process.env.AWS_REGION ?? 'us-east-1' });
export async function getRunArtifacts(artifactS3Location: string) {
// Parse the S3 URI — 's3://bucket/prefix'
const withoutScheme = artifactS3Location.replace('s3://', '');
const slashIdx = withoutScheme.indexOf('/');
const bucket = withoutScheme.slice(0, slashIdx);
const prefix = withoutScheme.slice(slashIdx + 1);
// List all files in the run artifact directory
const listResponse = await s3.send(
new ListObjectsV2Command({ Bucket: bucket, Prefix: prefix })
);
const files = listResponse.Contents ?? [];
return files.map(f => ({
key: f.Key,
size: f.Size,
lastModified: f.LastModified,
// Extract file type from key suffix
type: f.Key?.endsWith('.log') ? 'log'
: f.Key?.endsWith('.json') ? 'json'
: f.Key?.endsWith('.png') ? 'screenshot'
: 'other',
}));
}
export async function getRunLog(artifactS3Location: string): Promise<string> {
const withoutScheme = artifactS3Location.replace('s3://', '');
const slashIdx = withoutScheme.indexOf('/');
const bucket = withoutScheme.slice(0, slashIdx);
const prefix = withoutScheme.slice(slashIdx + 1);
const logKey = `${prefix}canary-run.log`;
const response = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: logKey }));
return response.Body?.transformToString() ?? '';
}
The execution role that runs the canary must have s3:PutObject on the artifact bucket for Synthetics to write artifacts. Your MCP server role needs s3:GetObject and s3:ListBucket on the same bucket to read them back. These are separate permissions on separate roles — a common misconfiguration is adding artifact read permissions to the wrong role.
Correlating runs with CloudWatch metrics
Canary runs automatically emit metrics to CloudWatch under the CloudWatchSynthetics namespace. You can correlate run timestamps with metric data points to build richer monitoring views.
import { CloudWatchClient, GetMetricStatisticsCommand } from '@aws-sdk/client-cloudwatch';
const cw = new CloudWatchClient({ region: process.env.AWS_REGION ?? 'us-east-1' });
export async function getCanarySuccessPercent(
canaryName: string,
startTime: Date,
endTime: Date,
periodSeconds = 300, // 5-minute resolution
): Promise<{ timestamp: Date; successPercent: number }[]> {
const response = await cw.send(new GetMetricStatisticsCommand({
Namespace: 'CloudWatchSynthetics',
MetricName: 'SuccessPercent',
Dimensions: [{ Name: 'CanaryName', Value: canaryName }],
StartTime: startTime,
EndTime: endTime,
Period: periodSeconds,
Statistics: ['Average'],
}));
return (response.Datapoints ?? [])
.sort((a, b) => (a.Timestamp?.getTime() ?? 0) - (b.Timestamp?.getTime() ?? 0))
.map(dp => ({
timestamp: dp.Timestamp!,
successPercent: dp.Average ?? 0,
}));
}
The SuccessPercent metric ranges from 0 to 100. A value of 100 means all runs in the period passed; 0 means all failed. The metric is emitted after each run completes, so the resolution aligns with the canary's Schedule.Expression interval — a 5-minute canary emits one datapoint every 5 minutes.
Filtering runs by state for incident analysis
When investigating a service outage, filter canary runs to find the first failure and calculate the outage duration. This pattern is useful for incident reports and SLA calculations.
export async function getOutagePeriods(
canaryName: string,
since: Date,
): Promise<{ start: Date; end: Date | null; durationMinutes: number | null }[]> {
const runs = await getAllRunsForTimeRange(canaryName, since);
const outages = [];
let outageStart: Date | null = null;
for (const run of runs.reverse()) { // process oldest-first
if (run.Status?.State === 'FAILED' && !outageStart) {
outageStart = run.Timeline?.Started ?? null;
} else if (run.Status?.State === 'PASSED' && outageStart) {
const end = run.Timeline?.Completed ?? null;
outages.push({
start: outageStart,
end,
durationMinutes: end
? Math.round((end.getTime() - outageStart.getTime()) / 60000)
: null,
});
outageStart = null;
}
}
// Open outage (still failing at the end of our time range)
if (outageStart) {
outages.push({ start: outageStart, end: null, durationMinutes: null });
}
return outages;
}
Related pages
- CloudWatch Synthetics overview — CreateCanaryCommand, lifecycle, IAM
- Writing API canary scripts — Node.js handler, synthetics module, packaging
- Alarms for canary failures — SuccessPercent metric, SNS notifications
- CloudWatch alarm management