Service Quotas · CloudWatch · Lambda · ECS · 2026-11-08 · Service Quotas arc

AWS Service Quotas for MCP Servers: Monitoring, Alarms, Automation, and ECS/Lambda Specifics

AliveMCP monitors thousands of MCP endpoints every 60 seconds. Among the AWS-deployed servers we watch, there is a category of incidents that is both predictable and completely avoidable: quota exhaustion. A Lambda-based MCP server that handled two hundred thousand tool calls per day without a single error suddenly starts returning TooManyRequestsException across every function in the account — not because the MCP server has a bug, but because a batch data pipeline that shares the account hit the account-wide concurrent executions quota at the same time the MCP load spiked. An ECS Fargate deployment that auto-scaled reliably for months suddenly stops accepting new tasks — not because the ECS service is misconfigured, but because the cluster silently hit the per-cluster Fargate task quota at task number 501, and the new tasks enter PROVISIONING state indefinitely without a clear error message. A multi-tenant MCP server that creates a DynamoDB table per customer starts failing table creation for new customers — not because DynamoDB is degraded, but because the account reached the 2,500 table count default quota, a limit that most teams never think about until they hit it at 2:00 AM during a customer onboarding push. These incidents share a characteristic: they look like software bugs from the outside. The CloudWatch alarms fire on symptoms — high error rates, failed health checks, connection timeouts — without pointing at the actual cause. The fix in every case is not a code change but a quota increase request, which takes 1–3 business days when done reactively through the AWS console, and can be fully automated for the most common quotas. The uncomfortable truth is that quota management is treated as an operational afterthought by most MCP server teams. They create the resources, they wire up health checks, they deploy monitoring dashboards — and they never look at the Service Quotas console until something breaks. This guide synthesizes four deep-dives — the Service Quotas API and key quota table for MCP server infrastructure, CloudWatch quota consumption alarms via the AWS/Usage namespace, automated quota increase requests with Lambda and EventBridge, and ECS Fargate and Lambda quota specifics including reserved concurrency and Container Insights — into a complete quota management strategy for MCP server operators who want to stop hitting invisible ceilings at the worst possible moment.

TL;DR

Part 1: The Service Quotas API and the quota inventory every MCP team should know

AWS service quotas are per-account, per-region ceilings on resource creation and throughput. When you exhaust one, the error message is often indistinguishable from a software bug at the application layer. TooManyRequestsException from Lambda is the same exception class whether it results from function-level throttling (which you can fix with code changes) or account-wide concurrent executions quota exhaustion (which requires a quota increase request). The Service Quotas API exposes the full picture — both what your account's limits currently are and how to request changes.

The API distinguishes between two values for each quota: the default quota (the value AWS sets for all new accounts) and the applied quota (the value your account actually has, which may be higher after approved increase requests). Check the applied quota when making operational decisions — the default is only relevant for understanding how much headroom an increase request has bought you.

# Check the applied quota for Lambda concurrent executions in your account
aws service-quotas get-service-quota \
  --service-code lambda \
  --quota-code L-B99A9384 \
  --query '{Name:Quota.QuotaName,Applied:Quota.Value,Adjustable:Quota.Adjustable}'

# Check the AWS default (before any increases) to understand your headroom
aws service-quotas get-aws-default-service-quota \
  --service-code lambda \
  --quota-code L-B99A9384 \
  --query '{Name:Quota.QuotaName,Default:Quota.Value}'

# List all adjustable quotas for a service — focus your monitoring on these
aws service-quotas list-service-quotas \
  --service-code ecs \
  --query 'Quotas[?Adjustable==`true`].{Name:QuotaName,Value:Value,Code:QuotaCode}' \
  --output table

# See if any increase requests are pending or were recently denied
aws service-quotas list-requested-service-quota-changes \
  --status PENDING \
  --query 'RequestedQuotas[*].{Service:ServiceCode,Quota:QuotaName,Desired:DesiredValue,Status:Status}'

The six quotas most likely to constrain MCP server scaling, with their quota codes:

ServiceQuota nameDefaultQuota codeHow it manifests for MCP
LambdaConcurrent executions1,000L-B99A9384TooManyRequestsException across all functions in the account; shared with every other Lambda workload
ECSRunning Fargate tasks per cluster500L-3794C6DCTasks stuck in PROVISIONING indefinitely; ECS service reports no capacity errors in console
ECSRunning Fargate tasks per region1,000L-DC8A8B7CSecond ceiling above per-cluster quota; hits in multi-cluster deployments
DynamoDBTable count per region2,500L-F98FE922LimitExceededException: Too many tables on table creation; silently breaks new customer onboarding
API GatewayREST API calls per second10,000L-B3A6C3A9HTTP transport throttles; looks like high-latency responses before full rejection
CloudWatchAlarm count5,000L-74FC7D96Prevents adding new alarms; monitoring coverage degrades silently as fleet grows

Validate these quotas at deployment time, not at runtime. Add a startup check to your CDK pipeline or application initialization that queries the applied quota and current usage for each quota in your critical path and logs a warning before the ceiling is approached:

import { ServiceQuotasClient, GetServiceQuotaCommand } from '@aws-sdk/client-service-quotas';
import { CloudWatchClient, GetMetricStatisticsCommand } from '@aws-sdk/client-cloudwatch';

const quotasClient = new ServiceQuotasClient({ region: process.env.AWS_REGION! });
const cloudwatch = new CloudWatchClient({ region: process.env.AWS_REGION! });

const MCP_QUOTA_CHECKS = [
  {
    serviceCode: 'lambda',
    quotaCode: 'L-B99A9384',
    quotaName: 'Lambda concurrent executions',
    usageMetric: {
      namespace: 'AWS/Usage',
      metricName: 'ResourceCount',
      dimensionName: 'Resource',
      dimensionValue: 'Executions',
    },
    warnAtPercent: 60,
  },
  {
    serviceCode: 'dynamodb',
    quotaCode: 'L-F98FE922',
    quotaName: 'DynamoDB table count per region',
    warnAtPercent: 60,
  },
];

async function validateQuotaHeadroom(): Promise<void> {
  for (const check of MCP_QUOTA_CHECKS) {
    const { Quota } = await quotasClient.send(new GetServiceQuotaCommand({
      ServiceCode: check.serviceCode,
      QuotaCode: check.quotaCode,
    }));
    const limit = Quota?.Value ?? 0;

    if (check.usageMetric) {
      const stats = await cloudwatch.send(new GetMetricStatisticsCommand({
        Namespace: check.usageMetric.namespace,
        MetricName: check.usageMetric.metricName,
        Dimensions: [{ Name: check.usageMetric.dimensionName, Value: check.usageMetric.dimensionValue }],
        StartTime: new Date(Date.now() - 5 * 60_000),
        EndTime: new Date(),
        Period: 300,
        Statistics: ['Maximum'],
      }));
      const currentUsage = stats.Datapoints?.[0]?.Maximum ?? 0;
      const pct = (currentUsage / limit) * 100;
      if (pct >= check.warnAtPercent) {
        console.warn(`QUOTA WARNING [${check.quotaName}]: ${currentUsage}/${limit} (${pct.toFixed(1)}%) — request increase now`);
      }
    }
  }
}

For Organizations deployments, pre-request quota increases for all newly created member accounts via quota templates defined in the management account. This prevents every new account from starting at the AWS default quota value:

# Add Lambda concurrent executions to the Organizations quota template
aws service-quotas put-service-quota-into-template \
  --service-code lambda \
  --quota-code L-B99A9384 \
  --aws-region us-east-1 \
  --desired-value 2000

# Apply the template to new member accounts automatically
aws service-quotas associate-service-quota-template

# Verify the template is set correctly
aws service-quotas list-service-quota-increases-by-template \
  --aws-region us-east-1 \
  --query 'ServiceQuotaIncreaseRequestInTemplateList[*].{Service:ServiceCode,Quota:QuotaName,Value:DesiredValue}'

Quota templates apply only to accounts that join the organization after the template is defined. For existing member accounts, submit individual increase requests or use the automated Lambda pipeline described in Part 3.

Part 2: CloudWatch quota consumption alarms on the AWS/Usage namespace

AWS publishes quota consumption metrics automatically to the AWS/Usage CloudWatch namespace for all services with service quotas. No instrumentation is required. These metrics track how close your account is to each service quota in real time, and the data is available in CloudWatch without any setup. The gap between "this feature exists" and "MCP server teams use it" is surprisingly large — most teams discover quota exhaustion reactively rather than via a pre-wired alarm.

The AWS/Usage namespace uses four required dimensions. Getting any one of them wrong means the metric returns no data:

DimensionValueCommon mistake
ServicePascalCase service name (e.g., Lambda)Using the CLI service code lambda (lowercase) instead of Lambda
ResourceResource type matching the quota (e.g., Executions)Guessing the resource name; verify in CloudWatch Metrics console browser
TypeAlways the string ResourceOmitting this dimension — metric query returns no results
ClassAlways the string NoneOmitting this dimension — metric query returns no results

Create a CDK alarm on Lambda concurrent executions directly on the ResourceCount metric. Set the threshold to 80% of the quota value, and use treatMissingData: NOT_BREACHING — quota metrics only emit data points when there is actual usage, so a period with no invocations should not appear as a quota breach:

import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import * as cdk from 'aws-cdk-lib';

// Fetch your account's current applied quota before hardcoding this value:
// aws service-quotas get-service-quota --service-code lambda --quota-code L-B99A9384 --query 'Quota.Value'
const LAMBDA_CONCURRENCY_QUOTA = 1000;  // update after approved increases

const lambdaConcurrencyUsage = new cloudwatch.Metric({
  namespace: 'AWS/Usage',
  metricName: 'ResourceCount',
  dimensionsMap: {
    Service: 'Lambda',      // PascalCase — not 'lambda'
    Resource: 'Executions',
    Type: 'Resource',       // required literal
    Class: 'None',          // required literal
  },
  period: cdk.Duration.minutes(1),
  statistic: 'Maximum',
});

const lambdaConcurrencyQuotaAlarm = new cloudwatch.Alarm(this, 'LambdaQuotaAlarm', {
  alarmName: 'alivemcp-lambda-concurrency-quota-80pct',
  alarmDescription: `Lambda concurrent executions exceeding 80% of quota (${LAMBDA_CONCURRENCY_QUOTA})`,
  metric: lambdaConcurrencyUsage,
  threshold: LAMBDA_CONCURRENCY_QUOTA * 0.8,
  evaluationPeriods: 3,
  datapointsToAlarm: 2,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});

When the quota value changes after an approved increase request, update the LAMBDA_CONCURRENCY_QUOTA constant and redeploy the CDK stack. Alternatively, use a CloudWatch math expression that computes the percentage from a custom metric Lambda that publishes both the current usage and the current quota value — this keeps the alarm threshold accurate automatically, at the cost of an additional metric Lambda.

ECS running task count per cluster does not publish an AWS/Usage metric. For ECS quota monitoring, use a periodic Lambda that queries the ECS API, counts running tasks, publishes the count and quota as custom metrics, and then alarm on the percentage metric:

// quota-monitor-lambda/ecs.ts — run on EventBridge Schedule every 5 minutes
import { ECSClient, ListTasksCommand } from '@aws-sdk/client-ecs';
import { ServiceQuotasClient, GetServiceQuotaCommand } from '@aws-sdk/client-service-quotas';
import { CloudWatchClient, PutMetricDataCommand } from '@aws-sdk/client-cloudwatch';

const ecs = new ECSClient({});
const quotas = new ServiceQuotasClient({});
const cloudwatch = new CloudWatchClient({});

export const handler = async () => {
  const cluster = process.env.CLUSTER_ARN!;
  let totalTasks = 0;
  let nextToken: string | undefined;

  do {
    const result = await ecs.send(new ListTasksCommand({
      cluster,
      desiredStatus: 'RUNNING',
      nextToken,
    }));
    totalTasks += result.taskArns?.length ?? 0;
    nextToken = result.nextToken;
  } while (nextToken);

  const { Quota } = await quotas.send(new GetServiceQuotaCommand({
    ServiceCode: 'ecs',
    QuotaCode: 'L-3794C6DC',
  }));
  const limit = Quota?.Value ?? 500;

  await cloudwatch.send(new PutMetricDataCommand({
    Namespace: 'MCPServerQuota',
    MetricData: [
      { MetricName: 'ECSRunningTasks', Dimensions: [{ Name: 'ClusterArn', Value: cluster }], Value: totalTasks, Unit: 'Count' },
      { MetricName: 'ECSRunningTasksPercent', Dimensions: [{ Name: 'ClusterArn', Value: cluster }], Value: (totalTasks / limit) * 100, Unit: 'Percent' },
    ],
  }));
};

Build a quota health dashboard that consolidates all quota alarms into a single view:

const quotaDashboard = new cloudwatch.Dashboard(this, 'QuotaDashboard', {
  dashboardName: 'alivemcp-quota-health',
});

quotaDashboard.addWidgets(
  new cloudwatch.AlarmStatusWidget({
    title: 'Quota Alarms',
    alarms: [lambdaConcurrencyQuotaAlarm, ecsTasksQuotaAlarm],
    width: 24,
  }),
  new cloudwatch.GraphWidget({
    title: 'Lambda Concurrent Executions vs Quota',
    left: [lambdaConcurrencyUsage],
    leftAnnotations: [
      { value: LAMBDA_CONCURRENCY_QUOTA, label: 'Quota limit', color: '#ff0000' },
      { value: LAMBDA_CONCURRENCY_QUOTA * 0.8, label: '80% threshold', color: '#ff8800' },
    ],
    width: 12,
  }),
);

The Service Quotas console provides a shortcut: navigate to any quota's detail page and click "Create CloudWatch Alarm" to get the same math expression alarm created automatically, with the current quota value pre-filled as the threshold denominator. For one-off alarms, this is faster than the CDK equivalent. For alarms that need to be part of an infrastructure stack and updated when quotas change, the CDK approach keeps the alarm definition version-controlled and consistent with the rest of your monitoring configuration.

Part 3: Automated quota increase requests with Lambda and EventBridge

Quota management that requires an engineer to log into the AWS console at 2 AM and manually submit an increase request is not a system — it is an operational debt that will compound over time as the fleet grows. The Service Quotas API is fully programmable, and the entire lifecycle of a quota increase — detect, request, track, notify — can be automated in a Lambda-based pipeline triggered by CloudWatch alarm state changes.

The architecture has three components. A CloudWatch alarm on quota consumption at 80% threshold fires to an SNS topic. A requester Lambda subscribed to that topic deduplicates against existing pending requests and then calls RequestServiceQuotaIncreaseCommand. A poller Lambda on a 6-hour schedule reads open requests from DynamoDB, checks their status via GetRequestedServiceQuotaChangeCommand, and sends SNS notifications for terminal state transitions.

// quota-requester/index.ts — triggered by SNS alarm topic
import {
  ServiceQuotasClient,
  RequestServiceQuotaIncreaseCommand,
  GetServiceQuotaCommand,
  ListRequestedServiceQuotaChangesByServiceCommand,
} from '@aws-sdk/client-service-quotas';
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';
import { SNSClient, PublishCommand } from '@aws-sdk/client-sns';

const quotas = new ServiceQuotasClient({});
const dynamo = new DynamoDBClient({});
const sns = new SNSClient({});

const QUOTA_CONFIGS = [
  { serviceCode: 'lambda', quotaCode: 'L-B99A9384', quotaName: 'Lambda concurrent executions', multiplier: 2 },
  { serviceCode: 'ecs', quotaCode: 'L-3794C6DC', quotaName: 'ECS Fargate tasks per cluster', multiplier: 2, maxValue: 5000 },
  { serviceCode: 'dynamodb', quotaCode: 'L-F98FE922', quotaName: 'DynamoDB tables per region', multiplier: 2 },
];

export const handler = async (event: any) => {
  const alarmName: string = JSON.parse(event.Records[0].Sns.Message).AlarmName;
  const config = QUOTA_CONFIGS.find(c => alarmName.includes(c.quotaCode));
  if (!config) return;

  // Dedup layer 1: check for existing PENDING request via Service Quotas API
  const existing = await quotas.send(new ListRequestedServiceQuotaChangesByServiceCommand({
    ServiceCode: config.serviceCode,
    Status: 'PENDING',
  }));
  if (existing.RequestedQuotas?.some(r => r.QuotaCode === config.quotaCode)) {
    console.log(`Already have a pending request for ${config.quotaName} — skipping`);
    return;
  }

  const { Quota } = await quotas.send(new GetServiceQuotaCommand({
    ServiceCode: config.serviceCode,
    QuotaCode: config.quotaCode,
  }));
  const currentValue = Quota?.Value ?? 0;
  const desiredValue = Math.min(currentValue * config.multiplier, config.maxValue ?? currentValue * 10);

  let requestId: string;
  try {
    const response = await quotas.send(new RequestServiceQuotaIncreaseCommand({
      ServiceCode: config.serviceCode,
      QuotaCode: config.quotaCode,
      DesiredValue: desiredValue,
    }));
    requestId = response.RequestedQuota?.Id ?? '';
  } catch (err: any) {
    if (err.name === 'NoSuchResourceException' || err.message?.includes('not adjustable')) {
      // Non-adjustable quota — escalate to manual Support case
      await sns.send(new PublishCommand({
        TopicArn: process.env.ALERT_TOPIC_ARN!,
        Subject: `[ACTION REQUIRED] Non-adjustable quota approaching limit: ${config.quotaName}`,
        Message: `Quota ${config.quotaName} is non-adjustable via API. File a Support case to request an increase.`,
      }));
      return;
    }
    throw err;
  }

  // Dedup layer 2: conditional write to prevent duplicate tracking records
  await dynamo.send(new PutItemCommand({
    TableName: process.env.QUOTA_REQUESTS_TABLE!,
    ConditionExpression: 'attribute_not_exists(requestId)',
    Item: {
      requestId: { S: requestId },
      serviceCode: { S: config.serviceCode },
      quotaCode: { S: config.quotaCode },
      quotaName: { S: config.quotaName },
      currentValue: { N: String(currentValue) },
      desiredValue: { N: String(desiredValue) },
      submittedAt: { S: new Date().toISOString() },
      status: { S: 'PENDING' },
    },
  }));

  await sns.send(new PublishCommand({
    TopicArn: process.env.ALERT_TOPIC_ARN!,
    Subject: `[INFO] Quota increase requested automatically: ${config.quotaName}`,
    Message: `Automatic quota increase submitted.\nService: ${config.serviceCode}\nCurrent: ${currentValue} → Requested: ${desiredValue}\nRequest ID: ${requestId}\n\nApproval typically takes 1–3 business days. No action needed unless the request is DENIED.`,
  }));
};

The poller Lambda checks open requests every 6 hours and notifies the team on status changes. Add a staleness check: if a request has been PENDING for more than 7 days without a status change, it may have been auto-closed by AWS silently and requires manual investigation:

// quota-status-poller/index.ts — runs every 6 hours on EventBridge schedule
import { ServiceQuotasClient, GetRequestedServiceQuotaChangeCommand } from '@aws-sdk/client-service-quotas';
import { DynamoDBClient, ScanCommand, UpdateItemCommand } from '@aws-sdk/client-dynamodb';
import { SNSClient, PublishCommand } from '@aws-sdk/client-sns';

const quotas = new ServiceQuotasClient({});
const dynamo = new DynamoDBClient({});
const sns = new SNSClient({});

export const handler = async () => {
  const openRequests = await dynamo.send(new ScanCommand({
    TableName: process.env.QUOTA_REQUESTS_TABLE!,
    FilterExpression: '#s = :pending OR #s = :caseOpened',
    ExpressionAttributeNames: { '#s': 'status' },
    ExpressionAttributeValues: {
      ':pending': { S: 'PENDING' },
      ':caseOpened': { S: 'CASE_OPENED' },
    },
  }));

  for (const item of openRequests.Items ?? []) {
    const requestId = item.requestId.S!;
    const quotaName = item.quotaName.S!;
    const previousStatus = item.status.S!;

    // Staleness check: requests open for 7+ days may have been silently auto-closed
    const submittedAt = new Date(item.submittedAt.S!);
    const daysSinceSubmission = (Date.now() - submittedAt.getTime()) / (1000 * 60 * 60 * 24);
    if (daysSinceSubmission > 7) {
      await sns.send(new PublishCommand({
        TopicArn: process.env.ALERT_TOPIC_ARN!,
        Subject: `[ACTION REQUIRED] Stale quota request needs investigation: ${quotaName}`,
        Message: `Quota increase request for ${quotaName} has been open for ${Math.floor(daysSinceSubmission)} days with no status change.\nRequest ID: ${requestId}\nCheck the Service Quotas console and the associated Support case.`,
      }));
      continue;
    }

    const response = await quotas.send(new GetRequestedServiceQuotaChangeCommand({ RequestId: requestId }));
    const currentStatus = response.RequestedQuota?.Status ?? 'UNKNOWN';

    if (currentStatus !== previousStatus) {
      await dynamo.send(new UpdateItemCommand({
        TableName: process.env.QUOTA_REQUESTS_TABLE!,
        Key: { requestId: { S: requestId } },
        UpdateExpression: 'SET #s = :newStatus, updatedAt = :now',
        ExpressionAttributeNames: { '#s': 'status' },
        ExpressionAttributeValues: {
          ':newStatus': { S: currentStatus },
          ':now': { S: new Date().toISOString() },
        },
      }));

      if (currentStatus === 'APPROVED') {
        await sns.send(new PublishCommand({
          TopicArn: process.env.ALERT_TOPIC_ARN!,
          Subject: `[RESOLVED] Quota increase approved: ${quotaName}`,
          Message: `Quota increase for ${quotaName} has been APPROVED. The new limit is now active.\nUpdate your CDK alarm threshold constants and redeploy.\nRequest ID: ${requestId}`,
        }));
      } else if (currentStatus === 'DENIED') {
        await sns.send(new PublishCommand({
          TopicArn: process.env.ALERT_TOPIC_ARN!,
          Subject: `[ACTION REQUIRED] Quota increase denied: ${quotaName}`,
          Message: `Quota increase for ${quotaName} was DENIED.\nRequest ID: ${requestId}\nCase ID: ${response.RequestedQuota?.CaseId}\nReview the denial reason and re-submit with workload justification, or escalate to a Support case.`,
        }));
      }
    }
  }
};

Wire the two Lambdas together with CDK. The critical IAM detail: the action namespace is servicequotas:* (not service-quotas:*). The CLI command prefix has a hyphen; the IAM action namespace does not. Policies using service-quotas:RequestServiceQuotaIncrease silently fail authorization:

// IAM policy for the requester Lambda — note servicequotas:* not service-quotas:*
requesterLambda.addToRolePolicy(new iam.PolicyStatement({
  actions: [
    'servicequotas:RequestServiceQuotaIncrease',
    'servicequotas:GetServiceQuota',
    'servicequotas:ListRequestedServiceQuotaChangesByService',
  ],
  resources: ['*'],
}));

// Schedule the poller every 6 hours
new events.Rule(this, 'QuotaPollerSchedule', {
  schedule: events.Schedule.rate(cdk.Duration.hours(6)),
  targets: [new targets.LambdaFunction(pollerLambda)],
});

Part 4: ECS Fargate and Lambda quota specifics

The quota management approaches in Parts 1–3 apply to all AWS services, but ECS Fargate and Lambda have deployment-specific details that make generic approaches insufficient for MCP servers. ECS Fargate has four interlocking quota dimensions, any one of which can silently block task scheduling. Lambda's account-wide concurrency pool creates blast radius from unrelated workloads. Getting quota management right for each deployment model requires understanding these deployment-specific failure modes.

ECS Fargate: four quota dimensions and Container Insights monitoring

ECS Fargate does not publish quota consumption to AWS/Usage metrics. The four quota dimensions that gate ECS task scheduling:

QuotaDefaultQuota codeFailure mode
Running Fargate tasks per cluster500L-3794C6DCNo new tasks scheduled in this cluster; existing tasks run normally
Running Fargate tasks per region1,000L-DC8A8B7CSecond ceiling above per-cluster; hits in multi-cluster or high-density deployments
ECS services per cluster500L-B7F81543Cannot create new ECS services — breaks service-per-customer multi-tenant patterns
Fargate Spot target capacity units1,000L-FE37DB9BFargate Spot requests exceeding this fail — affects cost-optimized fleets

When ECS Fargate task scheduling fails due to quota exhaustion, the ECS service reports tasks stuck in PROVISIONING state indefinitely. The error is not surfaced clearly in the ECS console. Use service event logs to find quota error messages:

# Check service events for quota errors
aws ecs describe-services \
  --cluster alivemcp-prod \
  --services alivemcp-mcp-server \
  --query 'services[0].events[:10].{CreatedAt:createdAt,Message:message}'

# Count running tasks to compare against quota
aws ecs list-tasks --cluster alivemcp-prod --desired-status RUNNING \
  | jq '.taskArns | length'

# Get the per-cluster quota
aws service-quotas get-service-quota \
  --service-code ecs --quota-code L-3794C6DC \
  --query 'Quota.Value'

For monitoring, enable Container Insights on the cluster and alarm on ECS/ContainerInsights RunningTaskCount. This metric updates every minute and is more reliable than a custom polling Lambda:

const cluster = new ecs.Cluster(this, 'McpCluster', {
  clusterName: 'alivemcp-prod',
  containerInsights: true,  // publishes ECS/ContainerInsights metrics automatically
  vpc,
});

// Alarm at 80% of the per-cluster task quota (adjust after approved increases)
const fargateTaskAlarm = new cloudwatch.Alarm(this, 'FargateTaskQuotaAlarm', {
  alarmName: 'alivemcp-fargate-task-quota-80pct',
  metric: new cloudwatch.Metric({
    namespace: 'ECS/ContainerInsights',
    metricName: 'RunningTaskCount',
    dimensionsMap: { ClusterName: 'alivemcp-prod' },
    period: cdk.Duration.minutes(1),
    statistic: 'Maximum',
  }),
  threshold: 500 * 0.8,
  evaluationPeriods: 3,
  datapointsToAlarm: 2,
  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});

If Container Insights is not enabled, enable it on the existing cluster without recreating it:

aws ecs update-cluster-settings \
  --cluster alivemcp-prod \
  --settings name=containerInsights,value=enabled

Lambda: reserved concurrency and provisioned concurrency for MCP handlers

Lambda's account-wide concurrent executions quota is shared across every Lambda function in the account. A batch data pipeline that runs in the same account as your MCP server shares the same pool. When the pipeline spikes, it can exhaust the pool and cause TooManyRequestsException errors on every MCP tool call — even if the MCP-specific Lambdas are well within their expected concurrency.

Use reserved concurrency to protect critical MCP tool handler functions from account-wide throttles:

const mcpToolHandler = new lambda.Function(this, 'McpToolHandler', {
  runtime: lambda.Runtime.NODEJS_22_X,
  code: lambda.Code.fromAsset('tool-handler'),
  handler: 'index.handler',
  // Reserve 200 out of the account's concurrent executions pool
  // Other Lambda workloads share the remaining (quota - 200)
  reservedConcurrentExecutions: 200,
  memorySize: 512,
  timeout: cdk.Duration.seconds(30),
});

// Monitor reserved pool utilization separately — this alarm fires when
// the MCP handler itself is approaching its reserved ceiling, independent
// of the account-wide quota alarm
const reservedConcurrencyAlarm = new cloudwatch.Alarm(this, 'ReservedConcurrencyAlarm', {
  alarmName: 'alivemcp-mcp-handler-reserved-concurrency-80pct',
  metric: new cloudwatch.Metric({
    namespace: 'AWS/Lambda',
    metricName: 'ConcurrentExecutions',
    dimensionsMap: { FunctionName: mcpToolHandler.functionName },
    period: cdk.Duration.minutes(1),
    statistic: 'Maximum',
  }),
  threshold: 200 * 0.8,
  evaluationPeriods: 3,
  datapointsToAlarm: 2,
  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});

Reserved concurrency creates a double-edged constraint: the function can never scale beyond the reserved value regardless of the account quota. Set the reserved value at 80–90% of expected peak, not at peak itself. If the MCP server experiences a traffic spike that requires more than the reserved concurrency, calls will be throttled at the function level — which is preferable to account-wide quota exhaustion causing failures everywhere, but still a failure mode that warrants its own alarm and increase path.

For session-establishment handlers where MCP protocol latency is perceptible during cold starts, use provisioned concurrency to pre-warm Lambda instances:

const handlerVersion = mcpToolHandler.currentVersion;

new lambda.ProvisionedConcurrencyConfig(this, 'McpHandlerProvisionedConcurrency', {
  target: handlerVersion,
  provisionedConcurrentExecutions: 10,  // 10 warm instances at all times
});

// Alarm when provisioned pool utilization exceeds 80%
// — remaining 20% handles burst before cold starts begin
new cloudwatch.Alarm(this, 'ProvisionedConcurrencyAlarm', {
  alarmName: 'alivemcp-provisioned-concurrency-utilization-80pct',
  metric: new cloudwatch.Metric({
    namespace: 'AWS/Lambda',
    metricName: 'ProvisionedConcurrencyUtilization',
    dimensionsMap: { FunctionName: mcpToolHandler.functionName },
    period: cdk.Duration.minutes(1),
    statistic: 'Maximum',
  }),
  threshold: 80,
  evaluationPeriods: 5,
  datapointsToAlarm: 3,
  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});

Note that ProvisionedConcurrencyUtilization alarms require the dimension to specify FunctionName with the function name (not ARN). If provisioned concurrency is configured on an alias rather than a version directly, the alarm dimensions must include the Resource dimension with the alias ARN — without it, the metric returns no data and the alarm stays in INSUFFICIENT_DATA state.

Quota increase request timing: submit at 60%, not 80%

The standard guidance for quota alarms is to alarm at 80% utilization. The standard guidance for submitting increase requests is to do so when the alarm fires. The gap between these two pieces of advice is the problem: if you wait for the 80% alarm to fire before submitting a request, and the approval takes 3 business days, you are operating on the remaining 20% of headroom without any slack for unexpected traffic spikes. For quotas on critical scaling paths, the optimal trigger for submitting a proactive increase request is 60% utilization, not 80%.

aws service-quotas request-service-quota-increase \
  --service-code lambda \
  --quota-code L-B99A9384 \
  --desired-value 3000  # request 2–3x current, not 10x; large requests take longer

# After submitting, add justification to the resulting Support case
# Note: the API does not accept a justification text field directly
# Get the case ID and add workload context via aws support add-communication-to-case
aws service-quotas get-requested-service-quota-change \
  --request-id REQ_ID \
  --query '{Status:RequestedQuota.Status,CaseId:RequestedQuota.CaseId}'

For Lambda concurrent executions, ECS Fargate running tasks, and DynamoDB table count, increase requests asking for 2–3× the current quota with a brief workload justification are almost always approved within 1–3 business days. The justification to include in the Support case reply: current usage pattern and growth trajectory, specific use case explaining why the higher quota is needed, and the target value. Requests for 10× increases or requests without justification take longer to review and are more likely to be denied.

Putting it together: a quota management runbook

Quota management for MCP servers has four operational layers, and all four need to be in place before a scaling incident exposes a gap:

  1. Quota inventory and baseline — Run list-service-quotas for Lambda, ECS, DynamoDB, API Gateway, and CloudWatch at initial deployment. Record the applied values in your team wiki or as CDK context. Add the startup validation check that logs warnings at 60% utilization for each critical quota. Set up Organizations quota templates so all new accounts start with the team's standard values.
  2. Proactive alarms — Wire CloudWatch alarms on AWS/Usage ResourceCount at 80% for Lambda and API Gateway. Wire alarms on ECS/ContainerInsights RunningTaskCount (requires Container Insights enabled) at 80% of the per-cluster Fargate task quota. Create the quota health dashboard with AlarmStatusWidget consolidating all quota alarms in one view. Verify alarms are correctly wired by temporarily lowering thresholds and confirming state transitions within two minutes.
  3. Automated increase requests — Deploy the requester Lambda subscribed to the 80% quota alarm SNS topics. Deploy the status poller Lambda on a 6-hour EventBridge schedule. Configure the DynamoDB tracking table with RemovalPolicy.RETAIN so request history survives stack updates. Verify IAM uses servicequotas:* actions not service-quotas:*. Test the dedup path by confirming the requester Lambda logs "already have a pending request" on second invocation.
  4. Deployment-specific protections — For ECS Fargate: enable Container Insights at cluster creation; set up per-cluster and per-region task count alarms. For Lambda: add reservedConcurrentExecutions on critical MCP tool handler functions to protect them from account-wide concurrency exhaustion; add provisioned concurrency on session-establishment handlers if cold start latency is perceptible during MCP handshake; wire separate alarms for reserved pool utilization and provisioned concurrency utilization.

The AliveMCP monitoring service itself uses all four layers. The status monitoring workers that probe each registered MCP endpoint every 60 seconds are Lambda functions with reserved concurrency protecting them from the account-wide pool. The ECS Fargate cluster running the collector service has Container Insights enabled and alarms at 400 running tasks (80% of the 500 default). The startup validation runs at every CDK deployment and warns when any quota is above 60% utilization. And the automated requester Lambda has fired twice over the service's operational history — both times submitting increase requests that were approved within 24 hours, well before the 80% alarm threshold translated into actual service disruption.

Common failures quick reference

SymptomLikely quota causeDiagnosisFix
TooManyRequestsException across all Lambda functions simultaneouslyAccount-wide concurrent executions quota exhaustedaws lambda get-account-settings --query ConcurrentExecutions; check AWS/Usage ResourceCount Executions in CloudWatchAdd reserved concurrency to MCP handlers; request quota increase; optionally move noisy Lambdas to separate account
ECS service tasks stuck in PROVISIONING indefinitelyFargate running tasks per cluster (L-3794C6DC) or per region (L-DC8A8B7C) exhaustedaws ecs describe-services --query 'services[0].events[:10]' for quota error messages; count running tasks vs quotaRequest quota increase for per-cluster quota first; check per-region quota if still failing
DynamoDB LimitExceededException: Too many tablesDynamoDB table count quota (L-F98FE922, default 2,500) reachedaws dynamodb list-tables | jq '.TableNames | length'Delete unused tables; request quota increase for table-per-tenant deployments
AWS/Usage ResourceCount shows no data in CloudWatchDimension values are wrong (case-sensitive)Browse CloudWatch Metrics console for the AWS/Usage namespace to see exact dimension valuesUse Service: Lambda (PascalCase), not lambda; include both Type=Resource and Class=None
Quota alarm fires immediately on deployment even when service is under quotatreatMissingData: breaching set — treats no-data periods as breachCheck alarm configuration in CloudWatch consoleChange to treatMissingData: notBreaching; quota metrics only emit when there is actual usage
Requester Lambda IAM error on RequestServiceQuotaIncreaseIAM policy uses service-quotas:* (with hyphen) instead of servicequotas:*Check Lambda CloudWatch Logs for AccessDeniedException detailsUpdate IAM policy to use servicequotas:RequestServiceQuotaIncrease (no hyphen)
Duplicate quota increase requests submittedAlarm re-fires before DynamoDB write completes; dedup check race conditionCheck Service Quotas console for multiple PENDING requests for same quotaAdd dedup check via ListRequestedServiceQuotaChangesByService before submitting; add conditional write with attribute_not_exists(requestId)
Provisioned concurrency alarm stays in INSUFFICIENT_DATAAlarm dimensions specify function name but provisioned concurrency is on an alias — requires Resource dimension with alias ARNVerify alarm dimensions in CloudWatch consoleAdd Resource dimension with the alias ARN if concurrency is on an alias, not a version directly