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
- Service Quotas API and key quota inventory — the Service Quotas API distinguishes between the default quota (what AWS sets for all accounts) and the applied quota (what your account has after approved increases); use
aws service-quotas get-service-quota --service-code lambda --quota-code L-B99A9384to see the applied value andget-aws-default-service-quotato see the original default; the six quotas most likely to constrain MCP server scaling are Lambda concurrent executions (default 1,000, shared account-wide across all Lambda functions), ECS Fargate running tasks per cluster (default 500, reached quickly in multi-tenant deployments), ECS Fargate running tasks per region (default 1,000, an additional ceiling above the per-cluster quota), DynamoDB table count per account per region (default 2,500, relevant for table-per-tenant patterns), API Gateway calls per second (default 10,000 for REST APIs), and CloudWatch alarms (default 5,000, constrains alert coverage for large fleets); useaws service-quotas list-service-quotas --service-code SERVICECODE --query 'Quotas[?Adjustable==`true`]'to discover which limits in your account are adjustable via self-service API versus which require a formal AWS Support case; validate critical quotas at deployment time by adding a startup check in your CDK pipeline or application initialization code that callsGetServiceQuotaCommandfor each quota in your critical path and logs a warning if current usage exceeds 60% of the limit: The most important operational habit for quota management is validating quota headroom at deployment time, not at runtime. The startup check pattern — query the applied quota and current usage on application startup, warn-but-continue if approaching the limit — transforms quota exhaustion from a 2 AM pager event into a deployment-time warning that the team can act on proactively. Organizations running multi-account architectures should use quota templates in the management account to pre-request quota increases for newly created member accounts so each account starts with higher limits than the AWS default without requiring per-account increase requests. Add the quota template for Lambda concurrent executions and ECS Fargate running tasks to your account vending pipeline and every new account will inherit the organization standard from day one. - CloudWatch quota consumption alarms via the
AWS/Usagenamespace — AWS publishes quota consumption metrics automatically to theAWS/Usagenamespace for most services that have service quotas, with four required dimensions:Service(PascalCase service name, e.g.,Lambdanotlambda),Resource(resource type, e.g.,Executions),Type=Resource(always this literal string), andClass=None(always this literal string); create a CloudWatch alarm directly on theResourceCountmetric with the threshold set to 80% of the quota value, usingtreatMissingData: notBreachingbecause quota metrics only emit data points when there is actual usage, andevaluationPeriods: 3withdatapointsToAlarm: 2to avoid false alarms from brief concurrency spikes; a critical limitation is that not all quotas haveAWS/Usagemetrics — ECS running task count per cluster does not publish a usage metric, so ECS quota monitoring requires a periodic Lambda that calls the ECS API, counts running tasks, and publishes a customMCPServerQuota/ECSRunningTasksmetric viaPutMetricDataCommand; the Service Quotas console provides a "Create CloudWatch Alarm" button on each quota's detail page as a one-click alternative to CDK — it creates the same math expression alarm automatically with the current quota value pre-filled: The dimension case-sensitivity requirement trips up teams that grep forResourceCountin the CloudWatch metrics browser and find no results — because they are filtering withService: lambda(lowercase) instead ofService: Lambda(PascalCase). The easiest way to verify dimension values is to open the CloudWatch Metrics console, navigate to theAWS/Usagenamespace, and browse the available metric combinations. The correct dimension values appear there even when the CLI or CDK code has them wrong. Once the alarm is correctly wired, test it by temporarily lowering the threshold below your current usage — the alarm should flip to ALARM state within two minutes of the next metric data point. - Automated quota increase requests with Lambda and EventBridge — the Service Quotas API is fully programmable: a Lambda can submit an increase request (
RequestServiceQuotaIncreaseCommand), store the returned request ID in DynamoDB, and have a second poller Lambda check status every 6 hours (GetRequestedServiceQuotaChangeCommand) and send SNS notifications when the status transitions toAPPROVEDorDENIED; the architecture requires three components: a CloudWatch alarm on quota consumption at 80% threshold that sends to an SNS topic, a requester Lambda subscribed to that topic that performs a dedup check viaListRequestedServiceQuotaChangesByServiceCommandbefore submitting to prevent duplicate requests when the alarm fires multiple times, and a poller Lambda on a 6-hour EventBridge schedule that scans open requests in DynamoDB and notifies the team of status changes; two critical implementation details are that the IAM action namespace isservicequotas:*(notservice-quotas:*— the hyphen is not in the IAM action name despite being in the CLI command prefix), and that non-adjustable quotas require a different error handler path that sends an SNS alert for manual Support case creation instead of attempting a programmatic increase that will fail withServiceQuotasException: This quota is not adjustable: The deduplication check is the most important reliability detail in the automation. A CloudWatch alarm in ALARM state can re-trigger its SNS notifications on each evaluation cycle — typically every minute. Without the dedup check againstListRequestedServiceQuotaChangesByServicefor PENDING requests, the requester Lambda can submit dozens of identical requests in rapid succession before DynamoDB write dedup kicks in. The recommended pattern is a two-layer dedup: first, check for existing PENDING requests via the Service Quotas API itself (since it has the authoritative state); second, use a DynamoDB conditional write withattribute_not_exists(requestId)as an idempotency guard for the tracking record. The tracking table also enables the staleness check in the poller — if a request has been PENDING for more than 7 days, it may have been auto-closed by AWS without a status change event, requiring manual investigation via the Support console. - ECS Fargate and Lambda quota specifics including reserved concurrency and Container Insights — ECS Fargate has four interlocking quota dimensions: running tasks per cluster (default 500), running tasks per region (default 1,000), services per cluster (default 500), and Fargate Spot target capacity units (default 1,000); hitting any one of them causes task scheduling to fail silently with tasks stuck in
PROVISIONINGstate rather than a clear quota error in the ECS console; monitoring requires enabling Container Insights on the cluster (containerInsights: truein CDK) and alarming onECS/ContainerInsights RunningTaskCountat 80% of the per-cluster quota since this metric updates every minute automatically and is more reliable than a custom metric Lambda; for Lambda deployments, the concurrent executions quota is shared account-wide across every Lambda in every service in the account, so a noisy Lambda batch job can exhaust the quota and cause MCP tool call failures even when the MCP-specific Lambdas are well within their expected concurrency; usereservedConcurrentExecutionson critical MCP tool handler functions to dedicate a portion of the account-wide pool exclusively to them and prevent account-wide throttles from cascading into MCP availability incidents; use provisioned concurrency (ProvisionedConcurrencyConfigon a function version) on session establishment handlers where cold start latency is perceptible during the MCP protocol handshake: The optimal time to submit a quota increase request is at 60% utilization, not 80%. If you wait for the 80% alarm to fire before submitting a request, and the approval takes 3 business days, you are operating on a runway of less than 25% of remaining headroom with no slack for unexpected traffic spikes. The 60% threshold as a soft trigger for proactive increase requests — combined with the 80% threshold as the automated request trigger — gives you a two-stage warning system where the first stage is a human check-in and the second stage is automatic escalation. For quotas on critical paths (Lambda concurrent executions, Fargate running tasks), include a workload justification in the resulting Support case: growth trajectory, specific use case, and a target value at 2–3× the current quota (not 10×, since large requests take longer to review).
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:
| Service | Quota name | Default | Quota code | How it manifests for MCP |
|---|---|---|---|---|
| Lambda | Concurrent executions | 1,000 | L-B99A9384 | TooManyRequestsException across all functions in the account; shared with every other Lambda workload |
| ECS | Running Fargate tasks per cluster | 500 | L-3794C6DC | Tasks stuck in PROVISIONING indefinitely; ECS service reports no capacity errors in console |
| ECS | Running Fargate tasks per region | 1,000 | L-DC8A8B7C | Second ceiling above per-cluster quota; hits in multi-cluster deployments |
| DynamoDB | Table count per region | 2,500 | L-F98FE922 | LimitExceededException: Too many tables on table creation; silently breaks new customer onboarding |
| API Gateway | REST API calls per second | 10,000 | L-B3A6C3A9 | HTTP transport throttles; looks like high-latency responses before full rejection |
| CloudWatch | Alarm count | 5,000 | L-74FC7D96 | Prevents 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:
| Dimension | Value | Common mistake |
|---|---|---|
Service | PascalCase service name (e.g., Lambda) | Using the CLI service code lambda (lowercase) instead of Lambda |
Resource | Resource type matching the quota (e.g., Executions) | Guessing the resource name; verify in CloudWatch Metrics console browser |
Type | Always the string Resource | Omitting this dimension — metric query returns no results |
Class | Always the string None | Omitting 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:
| Quota | Default | Quota code | Failure mode |
|---|---|---|---|
| Running Fargate tasks per cluster | 500 | L-3794C6DC | No new tasks scheduled in this cluster; existing tasks run normally |
| Running Fargate tasks per region | 1,000 | L-DC8A8B7C | Second ceiling above per-cluster; hits in multi-cluster or high-density deployments |
| ECS services per cluster | 500 | L-B7F81543 | Cannot create new ECS services — breaks service-per-customer multi-tenant patterns |
| Fargate Spot target capacity units | 1,000 | L-FE37DB9B | Fargate 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:
- Quota inventory and baseline — Run
list-service-quotasfor 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. - Proactive alarms — Wire CloudWatch alarms on
AWS/Usage ResourceCountat 80% for Lambda and API Gateway. Wire alarms onECS/ContainerInsights RunningTaskCount(requires Container Insights enabled) at 80% of the per-cluster Fargate task quota. Create the quota health dashboard withAlarmStatusWidgetconsolidating all quota alarms in one view. Verify alarms are correctly wired by temporarily lowering thresholds and confirming state transitions within two minutes. - 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.RETAINso request history survives stack updates. Verify IAM usesservicequotas:*actions notservice-quotas:*. Test the dedup path by confirming the requester Lambda logs "already have a pending request" on second invocation. - 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
reservedConcurrentExecutionson 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
| Symptom | Likely quota cause | Diagnosis | Fix |
|---|---|---|---|
TooManyRequestsException across all Lambda functions simultaneously | Account-wide concurrent executions quota exhausted | aws lambda get-account-settings --query ConcurrentExecutions; check AWS/Usage ResourceCount Executions in CloudWatch | Add reserved concurrency to MCP handlers; request quota increase; optionally move noisy Lambdas to separate account |
ECS service tasks stuck in PROVISIONING indefinitely | Fargate running tasks per cluster (L-3794C6DC) or per region (L-DC8A8B7C) exhausted | aws ecs describe-services --query 'services[0].events[:10]' for quota error messages; count running tasks vs quota | Request quota increase for per-cluster quota first; check per-region quota if still failing |
DynamoDB LimitExceededException: Too many tables | DynamoDB table count quota (L-F98FE922, default 2,500) reached | aws dynamodb list-tables | jq '.TableNames | length' | Delete unused tables; request quota increase for table-per-tenant deployments |
AWS/Usage ResourceCount shows no data in CloudWatch | Dimension values are wrong (case-sensitive) | Browse CloudWatch Metrics console for the AWS/Usage namespace to see exact dimension values | Use Service: Lambda (PascalCase), not lambda; include both Type=Resource and Class=None |
| Quota alarm fires immediately on deployment even when service is under quota | treatMissingData: breaching set — treats no-data periods as breach | Check alarm configuration in CloudWatch console | Change to treatMissingData: notBreaching; quota metrics only emit when there is actual usage |
Requester Lambda IAM error on RequestServiceQuotaIncrease | IAM policy uses service-quotas:* (with hyphen) instead of servicequotas:* | Check Lambda CloudWatch Logs for AccessDeniedException details | Update IAM policy to use servicequotas:RequestServiceQuotaIncrease (no hyphen) |
| Duplicate quota increase requests submitted | Alarm re-fires before DynamoDB write completes; dedup check race condition | Check Service Quotas console for multiple PENDING requests for same quota | Add dedup check via ListRequestedServiceQuotaChangesByService before submitting; add conditional write with attribute_not_exists(requestId) |
Provisioned concurrency alarm stays in INSUFFICIENT_DATA | Alarm dimensions specify function name but provisioned concurrency is on an alias — requires Resource dimension with alias ARN | Verify alarm dimensions in CloudWatch console | Add Resource dimension with the alias ARN if concurrency is on an alias, not a version directly |