Lambda reserved concurrency set to zero means the function can’t run at all: Lambda throttles every invocation until you remove or raise the setting. To find those functions, call GetFunctionConcurrency for each function from ListFunctions, compare the total with GetAccountSettings, and check CloudWatch ConcurrentExecutions and Throttles to see which reservations are too small or unused.
Reserved concurrency is easy to set and easy to forget. Someone sets a function to 0 to stop a runaway loop during an incident, or reserves 200 units for a batch job that later moved elsewhere, and the setting outlives everyone’s memory of it. This example is for platform and backend engineers who want one report per Region: which functions have Lambda reserved concurrency of zero, which reservations throttle, which sit idle, and how much of the account limit is still free to reserve.
The script is report only. It reads settings and metrics and never calls PutFunctionConcurrency or DeleteFunctionConcurrency; you make changes yourself after reading it.
What does reserved concurrency actually do?
By default, all functions in a Region share one account concurrency limit, 1,000 by default. Reserved concurrency carves out a slice of that pool for one function, and it works in both directions: the function is guaranteed that many concurrent executions, and it can never use more. There’s no charge for reserving it. The Lambda guide to function scaling and concurrency covers the model in detail. Three rules shape this report:
- Zero is a kill switch. AWS documents setting reserved concurrency to 0 as the way to intentionally throttle a function: it stops processing events until you remove the limit.
- 100 units are always unreserved. You can reserve concurrency for as many functions as you like, as long as at least 100 units stay free for functions without their own setting. With the default 1,000 limit, the most you can reserve in total is 900.
- Reserved but unused is still reserved. Other functions can’t borrow it. A 200-unit reservation for a job that peaks at 30 shrinks the pool everyone else shares by 200.
Reserved concurrency isn’t provisioned concurrency. Provisioned concurrency keeps pre-initialized environments warm and is billed; to audit that side, use the script to find unused Lambda provisioned concurrency and what it costs.
What happens to requests when a function has zero reserved concurrency?
It depends on how the function is invoked. For a synchronous Invoke, the Lambda API reference says the call fails with TooManyRequestsException; for a function-level limit the reason is ReservedFunctionConcurrentInvocationLimitExceeded (the account-level reason is ConcurrentInvocationLimitExceeded). Callers such as API Gateway see an error on every request.
For asynchronous invocations, Lambda keeps throttled events in its queue and retries them for up to 6 hours by default, with backoff up to 5 minutes between attempts, then discards them. Events you care about should have somewhere to go: the script to find Lambda functions without an async failure destination shows which don’t. Queue-based triggers such as SQS behave differently again; messages stay in the queue and are retried by the event source mapping, which is where SQS triggers without ReportBatchItemFailures can make retries more painful.
Note: A zero reservation doesn’t show up as an error in the function’s own logs, because the code never runs. The Throttles metric is the only signal, which is why the script reads it.
What does the script do?
- Reads the account limit
GetAccountSettingsreturnsConcurrentExecutions(the Regional limit) andUnreservedConcurrentExecutions(the limit minus everything reserved). - Finds every reservation
ListFunctionsdoesn’t include the setting, so the script callsGetFunctionConcurrencyfor each function, five at a time. An empty response means the function uses the shared pool. - Pulls metricsOne
GetMetricDatacall per 500 queries: the maximumConcurrentExecutionsand summedThrottlesper reserved function, plus the account-wideClaimedAccountConcurrencyandUnreservedConcurrentExecutions, over the last 14 days by default. - Classifies each reservationReserved at 0, hit its reservation and throttled, throttled below it, never invoked, or peaking under half of it.
- Prints headroomHow much more you can reserve before reaching the 100-unit floor, and how close the claimed concurrency came to the limit.
Prerequisites
- Node.js 18 or later with
tsx,@aws-sdk/client-lambdaand@aws-sdk/client-cloudwatch. - A read-only AWS profile. Setting up AWS profiles, SSO and assumed roles covers the options.
- The Regions you deploy to: concurrency limits and reservations are per Region.
Which IAM permissions does it need?
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadAccountAndFunctionList",
"Effect": "Allow",
"Action": ["lambda:GetAccountSettings", "lambda:ListFunctions"],
"Resource": "*"
},
{
"Sid": "ReadReservedConcurrency",
"Effect": "Allow",
"Action": "lambda:GetFunctionConcurrency",
"Resource": "arn:aws:lambda:*:123456789012:function:*"
},
{
"Sid": "ReadConcurrencyMetrics",
"Effect": "Allow",
"Action": "cloudwatch:GetMetricData",
"Resource": "*"
}
]
}
Replace the account ID. GetAccountSettings, ListFunctions and GetMetricData don’t support resource-level restrictions, so they stay on "*". The policy has no write actions. If you extend the script, the IAM policy generator for TypeScript SDK code drafts the new statement from your code.
The script to check Lambda reserved concurrency
// check-lambda-reserved-concurrency.ts
// Reports Lambda reserved concurrency per Region: the account limit, how much is reserved,
// functions reserved at 0 (every invocation is throttled), functions that hit their reservation,
// and reservations that sit mostly unused. Report only: it never changes a setting.
// Usage: npx tsx check-lambda-reserved-concurrency.ts [--regions us-east-1,eu-west-1] [--days 14]
import {
LambdaClient,
GetAccountSettingsCommand,
GetFunctionConcurrencyCommand,
paginateListFunctions,
} from "@aws-sdk/client-lambda";
import { CloudWatchClient, paginateGetMetricData, type MetricDataQuery } from "@aws-sdk/client-cloudwatch";
const args = process.argv.slice(2);
const flag = (name: string): string | undefined => {
const i = args.indexOf(name);
return i >= 0 ? args[i + 1] : undefined;
};
const regions = (flag("--regions") ?? process.env.AWS_REGION ?? "us-east-1").split(",").map((r) => r.trim()).filter(Boolean);
const days = Math.min(Math.max(Number(flag("--days") ?? 14), 1), 90);
const UNRESERVED_MINIMUM = 100; // Lambda always keeps 100 units for functions without a reservation
interface Reserved {
name: string;
reserved: number;
}
interface Row {
Function: string;
Reserved: number;
PeakConcurrency: number | string;
Throttles: number | string;
Finding: string;
}
const errorText = (err: unknown): string => (err instanceof Error ? `${err.name}: ${err.message}` : String(err));
/** GetFunctionConcurrency for every function, a few at a time. ListFunctions doesn't return the setting. */
async function reservations(lambda: LambdaClient): Promise<{ total: number; reserved: Reserved[] }> {
const names: string[] = [];
for await (const page of paginateListFunctions({ client: lambda }, {})) {
for (const fn of page.Functions ?? []) if (fn.FunctionName) names.push(fn.FunctionName);
}
const reserved: Reserved[] = [];
for (let i = 0; i < names.length; i += 5) {
const batch = names.slice(i, i + 5);
const results = await Promise.all(
batch.map((name) => lambda.send(new GetFunctionConcurrencyCommand({ FunctionName: name }))),
);
results.forEach((res, j) => {
// No ReservedConcurrentExecutions in the response means the function uses the unreserved pool.
if (res.ReservedConcurrentExecutions !== undefined) reserved.push({ name: batch[j], reserved: res.ReservedConcurrentExecutions });
});
}
return { total: names.length, reserved };
}
/** Peak ConcurrentExecutions and summed Throttles per function, plus account-level peaks. */
async function metrics(cw: CloudWatchClient, fns: Reserved[]): Promise<Map<string, number>> {
const end = new Date();
const start = new Date(end.getTime() - days * 86_400_000);
const queries: MetricDataQuery[] = [
{ Id: "claimed", MetricStat: { Metric: { Namespace: "AWS/Lambda", MetricName: "ClaimedAccountConcurrency" }, Period: 3600, Stat: "Maximum" } },
{ Id: "unreserved", MetricStat: { Metric: { Namespace: "AWS/Lambda", MetricName: "UnreservedConcurrentExecutions" }, Period: 3600, Stat: "Maximum" } },
];
fns.forEach((fn, i) => {
const dims = [{ Name: "FunctionName", Value: fn.name }];
queries.push(
{ Id: `c${i}`, MetricStat: { Metric: { Namespace: "AWS/Lambda", MetricName: "ConcurrentExecutions", Dimensions: dims }, Period: 3600, Stat: "Maximum" } },
{ Id: `t${i}`, MetricStat: { Metric: { Namespace: "AWS/Lambda", MetricName: "Throttles", Dimensions: dims }, Period: 3600, Stat: "Sum" } },
);
});
const out = new Map<string, number>();
for (let i = 0; i < queries.length; i += 500) {
// GetMetricData accepts up to 500 queries per request; the paginator follows NextToken.
const pages = paginateGetMetricData({ client: cw }, { MetricDataQueries: queries.slice(i, i + 500), StartTime: start, EndTime: end });
for await (const page of pages) {
for (const r of page.MetricDataResults ?? []) {
if (!r.Id) continue;
const values = r.Values ?? [];
if (values.length === 0) continue; // no data points: the function wasn't invoked in the window
const prev = out.get(r.Id) ?? 0;
out.set(r.Id, r.Id.startsWith("t") ? prev + values.reduce((a, b) => a + b, 0) : Math.max(prev, ...values));
}
}
}
return out;
}
async function scanRegion(region: string): Promise<void> {
const lambda = new LambdaClient({ region });
const settings = await lambda.send(new GetAccountSettingsCommand({}));
const limit = settings.AccountLimit?.ConcurrentExecutions ?? 0;
const unreservedPool = settings.AccountLimit?.UnreservedConcurrentExecutions ?? 0;
const { total, reserved } = await reservations(lambda);
const sumReserved = reserved.reduce((a, r) => a + r.reserved, 0);
const m = await metrics(new CloudWatchClient({ region }), reserved);
console.log(`\n${region}: account limit ${limit}, reserved ${sumReserved} across ${reserved.length} of ${total} functions`);
console.log(` unreserved pool ${unreservedPool}; you can still reserve ${Math.max(unreservedPool - UNRESERVED_MINIMUM, 0)}`);
if (m.has("claimed")) console.log(` peak ClaimedAccountConcurrency over ${days} days: ${m.get("claimed")} (${limit ? Math.round(((m.get("claimed") ?? 0) / limit) * 100) : 0}% of the limit)`);
if (m.has("unreserved")) console.log(` peak UnreservedConcurrentExecutions: ${m.get("unreserved")}`);
const rows: Row[] = reserved.map((fn, i) => {
const peak = m.get(`c${i}`);
const throttles = m.get(`t${i}`);
let finding = "ok";
if (fn.reserved === 0) finding = "reserved at 0: every invocation is throttled";
else if ((throttles ?? 0) > 0 && (peak ?? 0) >= fn.reserved) finding = "hit its reservation and throttled";
else if ((throttles ?? 0) > 0) finding = "throttled below its reservation (check the account limit)";
else if (peak === undefined) finding = `no invocations in ${days} days, reservation unused`;
else if (peak < fn.reserved / 2) finding = "peak under half the reservation";
return { Function: fn.name, Reserved: fn.reserved, PeakConcurrency: peak ?? "-", Throttles: throttles ?? "-", Finding: finding };
});
rows.sort((a, b) => a.Reserved - b.Reserved || a.Function.localeCompare(b.Function));
if (rows.length) console.table(rows);
if (unreservedPool - UNRESERVED_MINIMUM <= 0 && reserved.length) {
console.log(" warning: no reservable concurrency left; functions without a reservation share only 100 units");
}
}
async function main(): Promise<void> {
for (const region of regions) {
try {
await scanRegion(region);
} catch (err) {
console.error(`${region}: ${errorText(err)}`);
}
}
}
main().catch((err) => {
console.error(errorText(err));
process.exit(1);
});
paginateListFunctions and paginateGetMetricData follow the continuation tokens for you; the guide to AWS SDK v3 paginators explains how they work. Metrics use hourly periods, so the peak is the highest per-minute maximum in any hour of the window.
How do you run it?
npm install @aws-sdk/client-lambda @aws-sdk/client-cloudwatch
npm install --save-dev tsx typescript @types/node
# Last 14 days in two Regions
AWS_PROFILE=readonly npx tsx check-lambda-reserved-concurrency.ts --regions us-east-1,eu-west-1
# A longer window catches monthly jobs
AWS_PROFILE=readonly npx tsx check-lambda-reserved-concurrency.ts --days 45
Sample output
us-east-1: account limit 1000, reserved 670 across 5 of 7 functions
unreserved pool 330; you can still reserve 230
peak ClaimedAccountConcurrency over 14 days: 712 (71% of the limit)
peak UnreservedConcurrentExecutions: 142
┌─────────┬────────────────────┬──────────┬─────────────────┬───────────┬─────────────────────────────────────────────────┐
│ (index) │ Function │ Reserved │ PeakConcurrency │ Throttles │ Finding │
├─────────┼────────────────────┼──────────┼─────────────────┼───────────┼─────────────────────────────────────────────────┤
│ 0 │ 'legacy-sync' │ 0 │ '-' │ 37 │ 'reserved at 0: every invocation is throttled' │
│ 1 │ 'reports-worker' │ 20 │ '-' │ '-' │ 'no invocations in 14 days, reservation unused' │
│ 2 │ 'payments-webhook' │ 50 │ 50 │ 141 │ 'hit its reservation and throttled' │
│ 3 │ 'nightly-export' │ 200 │ 31 │ 0 │ 'peak under half the reservation' │
│ 4 │ 'orders-api' │ 400 │ 212 │ 0 │ 'ok' │
└─────────┴────────────────────┴──────────┴─────────────────┴───────────┴─────────────────────────────────────────────────┘
This run used mocked Lambda and CloudWatch responses for an account with seven functions. legacy-sync has Lambda reserved concurrency of zero and 37 throttled invocations: something still calls it. payments-webhook reached its 50-unit cap and was throttled 141 times, so the reservation is too small. nightly-export holds 200 units and peaked at 31, and reports-worker holds 20 that nothing used; together they keep 189 units away from the shared pool for no benefit.
How should you fix each finding?
- Reserved at 0. Find out why before you remove it; it may be an intentional stop. If it’s a leftover, run
aws lambda delete-function-concurrency --function-name legacy-syncto return the function to the shared pool, or put back a positive number withput-function-concurrency. If nothing should call it, the script to find unused Lambda functions with no invocations helps decide whether to delete it instead. - Hit its reservation and throttled. Raise the reservation, or remove it if the cap was never meant as a limit. If it protects a downstream database, keep the cap and fix the caller’s retries.
- Throttled below its reservation. The account limit was the constraint, not the function’s setting. Compare peak
ClaimedAccountConcurrencywith the limit and see monitoring AWS service quota usage and alerts for requesting and watching a higher quota. - Unused or under half. Lower it toward the observed peak plus a margin, so other functions get the units back. Check a month-end or seasonal window with
--daysfirst.
Throttling and timeouts often travel together; the report to find Lambda functions with a timeout too high for their runtime is a good next check, since long timeouts hold concurrency for longer. For request-level error patterns, see investigating Lambda errors with CloudWatch.
Troubleshooting
TooManyRequestsExceptionfrom the Lambda API. Control-plane calls have their own rate limits. Lower the batch of five inreservations()or run one Region at a time; the SDK already retries with backoff.- Peak shows “-” for a busy function. CloudWatch has no data points for that function name in the window, often because it’s invoked only through an alias or version. Metrics for versions and aliases carry extra dimensions.
- The unreserved pool looks wrong.
UnreservedConcurrentExecutionsinGetAccountSettingsis the limit minus reservations, not live usage. The CloudWatch metric with the same name is live usage. - Access denied. Compare the role with the policy above, then follow troubleshooting AWS IAM access denied errors step by step.
Ask ChatWithCloud instead
For a one-off answer, ask ChatWithCloud “Which Lambda functions in us-east-1 have reserved concurrency, and are any set to 0?” It writes AWS SDK for JavaScript v2 code, runs it on your machine with your profile and summarizes the result; how ChatWithCloud runs AWS queries locally explains the loop. It uses one profile and Region per session, can be wrong, and runs changes without asking for confirmation, so use a read-only profile and change concurrency yourself. For more questions to try, see asking AI about Lambda errors in your AWS account, and browse the library of practical AWS examples.
Frequently asked questions
What does Lambda reserved concurrency of zero mean?
The function is throttled on every invocation and runs no code until you delete the setting or set a positive number. AWS documents it as the way to intentionally stop a function.
Does reserved concurrency cost money?
No. Configuring reserved concurrency has no charge. Provisioned concurrency does, and it’s a separate setting.
How much Lambda concurrency can I reserve?
Up to your account limit minus 100 per Region, because Lambda keeps 100 units for functions without a reservation. With the default limit of 1,000, that’s 900 in total.
How do I see a function’s reserved concurrency with the CLI?
Run aws lambda get-function-concurrency --function-name my-function. An empty response means it has no reservation and uses the shared pool.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud
