Guide · EventBridge Scheduler · Schedule Groups
EventBridge Scheduler Groups for MCP Servers — Multi-Tenant Scheduling, Quotas, Cost Allocation
EventBridge Scheduler groups are organizational containers for schedules — they control quota allocation, enable cost allocation via tagging, and provide a lifecycle boundary for multi-environment or multi-tenant deployments. Every schedule belongs to exactly one group; schedules cannot be moved between groups after creation. The default group is always present and cannot be deleted — it is the correct destination for simple single-account setups where organizational partitioning is not needed. Named groups become essential when you build per-tenant monitoring (each customer's MCP server checks run in their own group), per-environment isolation (prod/staging/dev groups with independent quota headroom), or when you need to tear down an entire set of schedules atomically (deleting a group deletes all schedules in it — the cascade-delete pattern for environment teardown). The quota system is hierarchical: 1 million schedules per account-region in total, with a sub-quota of 10,000 schedules per group. Planning groups correctly prevents one team or tenant from exhausting the global quota and blocking others.
TL;DR
Create a named group per environment or tenant: aws scheduler create-schedule-group --name mcp-monitoring-prod --tags Environment=prod,Team=platform. Create schedules in the group with --group-name mcp-monitoring-prod. To tear down an environment, delete the group — all its schedules are deleted atomically: aws scheduler delete-schedule-group --name mcp-monitoring-prod. Monitor group-level invocation metrics via CloudWatch with dimension ScheduleGroup=mcp-monitoring-prod. See the observability guide for per-group alarm patterns.
Default group vs named groups
The default group (default) is created automatically in every account-region pair. It cannot be deleted, renamed, or tagged at the group level. All schedules created without --group-name land in the default group.
# Schedules without --group-name go to the default group
aws scheduler create-schedule --name my-schedule ...
# Equivalent to:
aws scheduler create-schedule --name my-schedule --group-name default ...
# List schedules in the default group
aws scheduler list-schedules --group-name default
# Get a schedule in the default group
aws scheduler get-schedule --name my-schedule --group-name default
# IMPORTANT: --group-name is required in get-schedule and delete-schedule
# even for the default group — omitting it causes:
# "ResourceNotFoundException: Schedule my-schedule does not exist"
# (because the API looks in the default group and the group name must match)
Named groups should be created for:
- Environment isolation:
mcp-monitoring-prod,mcp-monitoring-staging— independent quota headroom and lifecycle management per environment - Multi-tenant monitoring: one group per customer (e.g.,
tenant-acme-corp,tenant-globex) — enables per-tenant cost allocation, quota guardrails, and atomic teardown on customer offboarding - Team ownership: groups align with IAM policies so each team can only read/write schedules in their own group, reducing cross-team interference
Group lifecycle operations
# Create a group with tags for cost allocation
aws scheduler create-schedule-group \
--name mcp-monitoring-prod \
--tags '[
{"Key":"Environment","Value":"prod"},
{"Key":"Team","Value":"platform"},
{"Key":"CostCenter","Value":"platform-monitoring"},
{"Key":"Service","Value":"alivemcp"}
]'
# List all groups in the account-region
aws scheduler list-schedule-groups \
--query 'ScheduleGroups[*].{Name:Name,State:State,Arn:Arn}'
# Get group details (no schedule listing — use list-schedules per group)
aws scheduler get-schedule-group --name mcp-monitoring-prod
# Delete group — CASCADE DELETES ALL SCHEDULES IN THE GROUP
# This is irreversible and immediate — there is no soft-delete
aws scheduler delete-schedule-group --name mcp-monitoring-prod
# Update tags on a group (tags can be added/removed after creation)
aws scheduler tag-resource \
--resource-arn "arn:aws:scheduler:us-east-1:123456789012:schedule-group/mcp-monitoring-prod" \
--tags '[{"Key":"CostCenter","Value":"monitoring-v2"}]'
aws scheduler untag-resource \
--resource-arn "arn:aws:scheduler:us-east-1:123456789012:schedule-group/mcp-monitoring-prod" \
--tag-keys '["CostCenter"]'
The cascade-delete behavior on delete-schedule-group is a feature for environment teardown — deleting a staging group removes all staging schedules atomically without iterating through each schedule. The flip side is that this is permanent and irreversible: there is no "restore from trash" or confirmation prompt in the API. In IaC workflows, protect production groups with CloudFormation deletion protection or Terraform's lifecycle { prevent_destroy = true }.
Quota management
EventBridge Scheduler has a layered quota system. Understanding the limits prevents accidental quota exhaustion in multi-tenant deployments:
| Quota | Default limit | Adjustable | Scope |
|---|---|---|---|
| Schedules per account-region | 1,000,000 | No | Hard ceiling across all groups |
| Schedules per group | 10,000 | No (by design) | Per named group including default |
| Schedule groups per account-region | 500 | Yes (Service Quotas) | Account-region |
| API requests per second | Varies by operation | Yes | Account-region (create/update/delete: 10 TPS; list/get: 100 TPS) |
For multi-tenant monitoring with one group per tenant, 500 groups × 10,000 schedules = 5 million schedule slots, but the account ceiling is 1 million. The bottleneck is the account-region ceiling, not the per-group limit, for large deployments. At scale, consider multiple AWS accounts (one per tenant cluster) rather than multiple groups within a single account.
# Check current schedule count in a group
aws scheduler list-schedules \
--group-name mcp-monitoring-prod \
--query 'length(Schedules)'
# For large groups (>1000 schedules), paginate:
aws scheduler list-schedules \
--group-name mcp-monitoring-prod \
--max-results 100 \
--query '{Count:length(Schedules),Next:NextToken}'
# Count completed one-time schedules eating quota
aws scheduler list-schedules \
--group-name mcp-monitoring-prod \
--query 'length(Schedules[?State==`COMPLETED`])'
# Clean up completed one-time schedules
# (best practice: set ActionAfterCompletion=DELETE at schedule creation)
for name in $(aws scheduler list-schedules \
--group-name mcp-monitoring-prod \
--query 'Schedules[?State==`COMPLETED`].Name' \
--output text); do
aws scheduler delete-schedule \
--name "$name" \
--group-name mcp-monitoring-prod
done
Completed one-time schedules remain in the group (in COMPLETED state) and count against the 10,000-per-group quota unless you delete them or set ActionAfterCompletion: DELETE at creation. For high-volume one-time scheduling (e.g., scheduling a check for each new MCP server registered), always use ActionAfterCompletion: DELETE to avoid quota accumulation.
Per-group IAM access control
IAM policies can be scoped to specific schedule groups, enabling team-based access control where each team can only manage schedules in their own group.
# IAM policy: allow a service role to manage only schedules in mcp-monitoring-prod group
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"scheduler:CreateSchedule",
"scheduler:UpdateSchedule",
"scheduler:DeleteSchedule",
"scheduler:GetSchedule",
"scheduler:ListSchedules"
],
"Resource": [
"arn:aws:scheduler:us-east-1:123456789012:schedule/mcp-monitoring-prod/*"
]
},
{
"Effect": "Allow",
"Action": [
"scheduler:GetScheduleGroup",
"scheduler:ListScheduleGroups"
],
"Resource": "arn:aws:scheduler:us-east-1:123456789012:schedule-group/mcp-monitoring-prod"
},
{
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::123456789012:role/SchedulerExecutionRole-prod",
"Condition": {
"StringEquals": {
"iam:PassedToService": "scheduler.amazonaws.com"
}
}
}
]
}
# The schedule ARN resource pattern uses group name in the path:
# arn:aws:scheduler:region:account:schedule/GROUP-NAME/SCHEDULE-NAME
# This means you can scope Create/Update/Delete to a specific group.
The iam:PassRole condition iam:PassedToService: scheduler.amazonaws.com scopes the pass to Scheduler only — without this condition, the policy would allow the role to be passed to any AWS service, which is a privilege escalation risk. Also scope iam:PassRole to the specific execution role ARN(s) that are legitimate for this group, not a wildcard.
IaC patterns
# Terraform
resource "aws_scheduler_schedule_group" "mcp_monitoring_prod" {
name = "mcp-monitoring-prod"
tags = {
Environment = "prod"
CostCenter = "platform-monitoring"
}
}
resource "aws_scheduler_schedule" "health_probe" {
name = "mcp-health-probe-5m"
group_name = aws_scheduler_schedule_group.mcp_monitoring_prod.name
flexible_time_window {
mode = "FLEXIBLE"
maximum_window_in_minutes = 1
}
schedule_expression = "rate(5 minutes)"
target {
arn = aws_lambda_function.mcp_health_probe.arn
role_arn = aws_iam_role.scheduler_execution.arn
input = jsonencode({
scheduledTime = "<$.context.scheduledTime>"
scheduleArn = "<$.context.scheduleArn>"
})
retry_policy {
maximum_retry_attempts = 1
maximum_event_age_in_seconds = 300
}
dead_letter_config {
arn = aws_sqs_queue.scheduler_dlq.arn
}
}
}
# CDK (TypeScript)
import { CfnScheduleGroup, CfnSchedule } from 'aws-cdk-lib/aws-scheduler';
const group = new CfnScheduleGroup(this, 'McpMonitoringGroup', {
name: 'mcp-monitoring-prod',
tags: [
{ key: 'Environment', value: 'prod' },
{ key: 'CostCenter', value: 'platform-monitoring' },
],
});
new CfnSchedule(this, 'HealthProbe', {
name: 'mcp-health-probe-5m',
groupName: group.name,
flexibleTimeWindow: { mode: 'FLEXIBLE', maximumWindowInMinutes: 1 },
scheduleExpression: 'rate(5 minutes)',
target: {
arn: healthProbeFn.functionArn,
roleArn: schedulerRole.roleArn,
input: JSON.stringify({
scheduledTime: '<$.context.scheduledTime>',
scheduleArn: '<$.context.scheduleArn>',
}),
retryPolicy: {
maximumRetryAttempts: 1,
maximumEventAgeInSeconds: 300,
},
deadLetterConfig: { arn: schedulerDlq.queueArn },
},
});
Note the Terraform resource name is aws_scheduler_schedule_group (not aws_cloudwatch_event_rule — that is the old CloudWatch Events resource). The CDK L1 construct is CfnScheduleGroup — there are no L2 constructs for Scheduler in CDK as of 2026; use the L1 Cfn constructs directly. The higher-level @aws-cdk/aws-scheduler-alpha library is available but marked experimental.
Failure modes reference
| Failure | Symptom | Fix |
|---|---|---|
| Group quota exhaustion | ConflictException: Maximum number of schedules in a schedule group exceeded | 10,000 schedules per group limit hit; delete completed one-time schedules or create a new group and distribute schedules across groups |
| Accidental group cascade-delete | All schedules in group silently deleted along with group | Use Terraform prevent_destroy or CloudFormation deletion policy RETAIN on the group resource for production groups |
| Schedule not found after create | ResourceNotFoundException on get-schedule | --group-name required on get/update/delete even for default group; omitting it defaults to wrong lookup; always specify group name explicitly |
| CDK Cfn construct missing group name | Schedules land in default group; IaC drift when group is renamed | Always specify groupName: group.name (using CDK reference, not hardcoded string) so CDK creates the group before the schedule |
| Completed one-time schedules block new creates | CreateSchedule fails with quota error despite apparent room | COMPLETED schedules count against quota; run periodic cleanup of COMPLETED state schedules or use ActionAfterCompletion: DELETE on all one-time schedules |