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:

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:

QuotaDefault limitAdjustableScope
Schedules per account-region1,000,000NoHard ceiling across all groups
Schedules per group10,000No (by design)Per named group including default
Schedule groups per account-region500Yes (Service Quotas)Account-region
API requests per secondVaries by operationYesAccount-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

FailureSymptomFix
Group quota exhaustionConflictException: Maximum number of schedules in a schedule group exceeded10,000 schedules per group limit hit; delete completed one-time schedules or create a new group and distribute schedules across groups
Accidental group cascade-deleteAll schedules in group silently deleted along with groupUse Terraform prevent_destroy or CloudFormation deletion policy RETAIN on the group resource for production groups
Schedule not found after createResourceNotFoundException 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 nameSchedules land in default group; IaC drift when group is renamedAlways specify groupName: group.name (using CDK reference, not hardcoded string) so CDK creates the group before the schedule
Completed one-time schedules block new createsCreateSchedule fails with quota error despite apparent roomCOMPLETED schedules count against quota; run periodic cleanup of COMPLETED state schedules or use ActionAfterCompletion: DELETE on all one-time schedules