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:

  1. Quota threshold alarm — CloudWatch alarm on AWS/Usage ResourceCount at 80% of quota fires when usage approaches the limit. The alarm sends to an SNS topic.
  2. 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 RequestServiceQuotaIncrease with the desired value and stores the request ID in DynamoDB.
  3. 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

SymptomCauseFix
Duplicate quota increase requests submitted in quick successionAlarm fires multiple times before the first request is stored in DynamoDBAdd 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 adjustableThe quota code exists but the quota is not adjustable via self-service APICatch 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 daysRequest was auto-closed by AWS after inactivity without a status change eventAdd 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 RequestServiceQuotaIncreaseService Quotas requires servicequotas:RequestServiceQuotaIncrease (not service-quotas:) as the IAM action nameUse servicequotas:* as the IAM action namespace: servicequotas:RequestServiceQuotaIncrease, servicequotas:GetServiceQuota, etc.