Guide · Service Quotas · Lambda · EventBridge · Automation
Automated Quota Increase Requests for MCP Servers — Lambda and EventBridge
Quota increases that require manual engineer intervention at 2am during a scaling event are operational failures. The Service Quotas API is fully programmable — you can submit an increase request, poll for status, and notify your team when approval is needed, all without anyone logging into the AWS console. For MCP server operators running automated infrastructure, this means quota management follows the same code-review and automation discipline as the rest of your platform. This guide covers triggering quota increases from CloudWatch alarm state changes, the Lambda implementation that calls the Service Quotas API, polling for approval status, and handling non-adjustable quotas that require a Support case.
TL;DR
Create a CloudWatch alarm on quota consumption at 80% threshold (see quota CloudWatch alarms). Connect the alarm's SNS topic to a Lambda via EventBridge. The Lambda calls service-quotas:RequestServiceQuotaIncrease with a desired value 2× the current quota. It stores the returned requestId in DynamoDB. A second Lambda polls pending requests on a schedule and sends SNS notifications when status changes to APPROVED, DENIED, or when the request requires manual Support intervention.
Architecture: event-driven quota management
The automation pipeline has three components:
- Quota threshold alarm — CloudWatch alarm on
AWS/Usage ResourceCountat 80% of quota fires when usage approaches the limit. The alarm sends to an SNS topic. - Increase requester Lambda — Triggered by the SNS alarm topic via EventBridge. Checks whether a pending request already exists for the same quota (to avoid duplicate requests), then calls
RequestServiceQuotaIncreasewith the desired value and stores the request ID in DynamoDB. - Status poller Lambda — Runs every 6 hours on a schedule. Reads all open requests from DynamoDB, polls their status via
GetRequestedServiceQuotaChange, and sends SNS notifications for status changes. Removes completed requests from the tracking table.
Quota increase requester Lambda
// quota-requester/index.ts
import { ServiceQuotasClient, RequestServiceQuotaIncreaseCommand, GetServiceQuotaCommand, ListRequestedServiceQuotaChangesByServiceCommand } from '@aws-sdk/client-service-quotas';
import { DynamoDBClient, PutItemCommand, QueryCommand } 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({});
interface QuotaIncreaseConfig {
serviceCode: string;
quotaCode: string;
quotaName: string;
multiplier: number; // desired = current * multiplier
maxValue?: number; // cap to prevent runaway requests
}
const QUOTA_CONFIGS: QuotaIncreaseConfig[] = [
{ 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) => {
// Parse the CloudWatch alarm name from the SNS event to identify which quota triggered
const alarmName: string = JSON.parse(event.Records[0].Sns.Message).AlarmName;
// Find the matching quota config based on alarm name pattern
const config = QUOTA_CONFIGS.find(c => alarmName.includes(c.quotaCode));
if (!config) {
console.log(`No quota config found for alarm: ${alarmName}`);
return;
}
// Check for existing pending request to avoid duplicates
const existingRequests = await quotas.send(new ListRequestedServiceQuotaChangesByServiceCommand({
ServiceCode: config.serviceCode,
Status: 'PENDING',
}));
const alreadyPending = existingRequests.RequestedQuotas?.some(r => r.QuotaCode === config.quotaCode);
if (alreadyPending) {
console.log(`Already have a pending request for ${config.quotaName} — skipping`);
return;
}
// Get current applied quota
const currentQuota = await quotas.send(new GetServiceQuotaCommand({
ServiceCode: config.serviceCode,
QuotaCode: config.quotaCode,
}));
const currentValue = currentQuota.Quota?.Value ?? 0;
const desiredValue = Math.min(
currentValue * config.multiplier,
config.maxValue ?? currentValue * 10
);
// Submit the increase request
let requestId: string;
try {
const response = await quotas.send(new RequestServiceQuotaIncreaseCommand({
ServiceCode: config.serviceCode,
QuotaCode: config.quotaCode,
DesiredValue: desiredValue,
}));
requestId = response.RequestedQuota?.Id ?? '';
console.log(`Submitted quota increase for ${config.quotaName}: ${currentValue} -> ${desiredValue}, requestId: ${requestId}`);
} catch (err: any) {
if (err.name === 'NoSuchResourceException' || err.message?.includes('not adjustable')) {
// Non-adjustable quota — escalate to manual Support
await sns.send(new PublishCommand({
TopicArn: process.env.ALERT_TOPIC_ARN!,
Subject: `[ACTION REQUIRED] Non-adjustable quota approaching limit`,
Message: `Quota ${config.quotaName} (${config.serviceCode}/${config.quotaCode}) is non-adjustable via API. Current usage near ${currentValue}. File a Support case to request an increase.`,
}));
return;
}
throw err;
}
// Store request ID for status polling
await dynamo.send(new PutItemCommand({
TableName: process.env.QUOTA_REQUESTS_TABLE!,
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' },
},
}));
// Notify the team
await sns.send(new PublishCommand({
TopicArn: process.env.ALERT_TOPIC_ARN!,
Subject: `[INFO] Quota increase requested: ${config.quotaName}`,
Message: `Automatic quota increase request submitted for ${config.quotaName}.\nService: ${config.serviceCode}\nCurrent: ${currentValue}\nRequested: ${desiredValue}\nRequest ID: ${requestId}\n\nApproval typically takes 1-3 business days. No action required unless the request is DENIED.`,
}));
};
Status poller Lambda
// quota-status-poller/index.ts
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 () => {
// Scan for open requests (in a real deployment, use a GSI on status)
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!;
const response = await quotas.send(new GetRequestedServiceQuotaChangeCommand({
RequestId: requestId,
}));
const currentStatus = response.RequestedQuota?.Status ?? 'UNKNOWN';
if (currentStatus !== previousStatus) {
// Update DynamoDB
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() },
},
}));
// Notify on terminal states
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.\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}\n\nPlease review the denial reason in the Service Quotas console and re-submit with additional justification or file a Support case directly.`,
}));
}
}
}
};
CDK wiring for the full automation stack
// cdk/quota-automation-stack.ts
const quotaRequestsTable = new dynamodb.Table(this, 'QuotaRequestsTable', {
tableName: 'alivemcp-quota-requests',
partitionKey: { name: 'requestId', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
const alertTopic = sns.Topic.fromTopicArn(this, 'AlertTopic', ALERT_TOPIC_ARN);
const requesterLambda = new lambda.Function(this, 'QuotaRequester', {
runtime: lambda.Runtime.NODEJS_22_X,
code: lambda.Code.fromAsset('quota-requester'),
handler: 'index.handler',
environment: {
QUOTA_REQUESTS_TABLE: quotaRequestsTable.tableName,
ALERT_TOPIC_ARN: alertTopic.topicArn,
},
});
quotaRequestsTable.grantWriteData(requesterLambda);
alertTopic.grantPublish(requesterLambda);
requesterLambda.addToRolePolicy(new iam.PolicyStatement({
actions: [
'service-quotas:RequestServiceQuotaIncrease',
'service-quotas:GetServiceQuota',
'service-quotas:ListRequestedServiceQuotaChangesByService',
],
resources: ['*'],
}));
// Subscribe requester to quota threshold alarm topic
const quotaAlarmTopic = new sns.Topic(this, 'QuotaAlarmTopic', {
topicName: 'alivemcp-quota-alarms',
});
quotaAlarmTopic.addSubscription(new subscriptions.LambdaSubscription(requesterLambda));
const pollerLambda = new lambda.Function(this, 'QuotaPoller', {
runtime: lambda.Runtime.NODEJS_22_X,
code: lambda.Code.fromAsset('quota-status-poller'),
handler: 'index.handler',
environment: {
QUOTA_REQUESTS_TABLE: quotaRequestsTable.tableName,
ALERT_TOPIC_ARN: alertTopic.topicArn,
},
});
quotaRequestsTable.grantReadWriteData(pollerLambda);
alertTopic.grantPublish(pollerLambda);
pollerLambda.addToRolePolicy(new iam.PolicyStatement({
actions: ['service-quotas:GetRequestedServiceQuotaChange'],
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)],
});
Common failures
| Symptom | Cause | Fix |
|---|---|---|
| Duplicate quota increase requests submitted in quick succession | Alarm fires multiple times before the first request is stored in DynamoDB | Add DynamoDB conditional write with attribute_not_exists(requestId); additionally, check for existing PENDING requests via ListRequestedServiceQuotaChangesByService before submitting |
Lambda throws ServiceQuotasException: This quota is not adjustable | The quota code exists but the quota is not adjustable via self-service API | Catch the exception, log the quota name, and send an SNS alert for manual Support case creation; some quotas require calling AWS Support directly |
Poller Lambda reports stale PENDING status for requests older than 10 days | Request was auto-closed by AWS after inactivity without a status change event | Add a staleness check in the poller: if submittedAt is older than 7 days and status is still PENDING, send an SNS alert and treat it as requiring manual investigation |
Requester Lambda IAM error: not authorized to call RequestServiceQuotaIncrease | Service Quotas requires servicequotas:RequestServiceQuotaIncrease (not service-quotas:) as the IAM action name | Use servicequotas:* as the IAM action namespace: servicequotas:RequestServiceQuotaIncrease, servicequotas:GetServiceQuota, etc. |