What it means
Throttling is a healthy response to load, not a fault: a service is protecting itself by refusing work it cannot absorb. The reason it is confusing on Lambda is that two entirely different throttles produce similar-looking messages, and only one of them leaves anything in your function's logs.
The first is a Lambda invocation throttle. The account's concurrency limit or the function's reserved concurrency is reached, and Lambda declines to start another execution. Your function does not run. There is no START, no REPORT, and nothing whatsoever in the log group — the evidence lives in the Throttles metric and in whatever called you. Notably this is not counted in the Errors metric either, since no invocation occurred, which is why a throttled function can look completely healthy on an errors dashboard while dropping traffic.
The second is a downstream throttle: your handler ran, called DynamoDB or KMS or STS, and that service refused. This one does appear in your logs, with a stack trace, inside a normal invocation. The fix is on your side of the call — fewer calls, better caching, or lower concurrency — rather than on Lambda's.
Two multiplication effects cause most downstream throttling. Concurrency multiplies call rate: a function making three API calls per invocation, running at 200 concurrent executions, is making six hundred concurrent calls, and a limit that was ample in testing is reached immediately at scale. And control-plane APIs have much lower limits than data-plane ones — sts:AssumeRole and GetSecretValue are the usual offenders — so calling them once per invocation rather than caching them at module scope reaches a quota long before your actual work does. The retry-then-fail sequence in the logs, and a duration inflated by backoff you never see, are what make that visible.
CloudWatch’s Errors metric: Whether CloudWatch’s Errors metric counts this depends on whether the error escaped your handler, which the log line alone does not settle. A Lambda invocation throttle is counted under the Throttles metric, not Errors — the invocation never happened. A throttle from a downstream service inside your handler counts under Errors only if it escapes. LogStitch reports this invocation's status as throttled rather than errored, which is the distinction the CloudWatch metrics also draw.
What it looks like in CloudWatch
This is the shape the failure arrives in: the lines of one invocation scattered among everything else the log group received at the same moment.
CloudWatch Logs Before
6 raw lines, in the order CloudWatch delivered them.
LogStitch After
The same lines, grouped into the invocation they belong to.
Throttled7d4e2b90-6c31-4f08-9a25-1e8b3d7c0f64
- 12:11:04.220PLAT
START RequestId: 7d4e2b90-6c31-4f08-9a25-1e8b3d7c0f64 Version: $LATEST
- 12:11:04.224INFO
INFO Batch received records=250
- 12:11:04.902WARN
WARN Retry 3/3 after backoff op=BatchWriteItem
- 12:11:05.118ERROR
ERROR ThrottlingException: Rate exceeded
- 12:11:05.402PLAT
END RequestId: 7d4e2b90-6c31-4f08-9a25-1e8b3d7c0f64
- 12:11:05.402PLAT
REPORT RequestId: 7d4e2b90-6c31-4f08-9a25-1e8b3d7c0f64 Duration: 1182.44 ms Billed Duration: 1183 ms Memory Size: 512 MB Max Memory Used: 118 MB Status: error
The panel on the right is generated by running the excerpt on the left through the same parser that powers the free web stitcher — it is what the tool actually produces for this input, not an illustration of it.
How to confirm it from the logs
Where the message appears settles which kind you have. A throttle inside an invocation — between START and REPORT, with an SDK stack trace — is a downstream service refusing your call. A Lambda invocation throttle produces no log lines at all, because the function never ran; it appears only in the Throttles metric and in the caller's own error.
For the downstream kind, look at whether retries are visible. Most AWS SDKs retry throttles automatically with backoff, so a single logged Rate exceeded usually means the SDK had already exhausted its attempts. Also check the duration on the REPORT line — invocations that spend seconds longer than usual are often paying for silent retries you never see.
Causes, most likely first
A downstream service's per-second limit was reached
Read the exception name and the stack trace to identify the service. DynamoDB, KMS, STS, SQS, Secrets Manager and CloudWatch Logs all have request-rate limits, and a function running at high concurrency multiplies its own per-invocation call rate by the number of concurrent executions.
The account's Lambda concurrency limit was hit
Check the Throttles metric for the function and the account's concurrent-execution quota. This produces no function logs at all, so an absence of evidence in CloudWatch Logs combined with a rise in Throttles is exactly the signature.
Reserved concurrency on this function is set too low
Look at whether the function has a reserved concurrency configured. Reserved concurrency is a ceiling as well as a guarantee — setting it to 10 means the eleventh concurrent invocation is throttled even when the account has thousands of executions free.
A burst arrived faster than concurrency could scale
Compare the timing of the throttles against traffic. Lambda scales up in increments rather than instantly, so a sharp spike from a queue drain or a scheduled fan-out can be throttled even well below the steady-state limit.
Every invocation calls a low-limit control-plane API
Look for sts:AssumeRole, GetFunctionConfiguration, or Secrets Manager GetSecretValue being called on each invocation. Control-plane and secrets APIs have far lower limits than data-plane ones, and calling them per request rather than caching at module scope reaches those limits quickly.
Fixes
Cache expensive and rate-limited calls outside the handler
Anything initialised at module scope is created once per execution environment rather than once per invocation. Moving secret fetches, role assumptions and client construction there often removes the throttling entirely, because it cuts the call rate by the number of invocations each container serves.
jsimport { SecretsManagerClient, GetSecretValueCommand }
from "@aws-sdk/client-secrets-manager";
const client = new SecretsManagerClient({});
// Fetched once per execution environment, not once per invocation.
let cached;
async function secret() {
cached ??= await client.send(
new GetSecretValueCommand({ SecretId: process.env.SECRET_ID })
);
return cached.SecretString;
}
Retry with exponential backoff and jitter
Throttles are transient by definition, and the AWS SDKs already retry them — but the default attempt count is low for a bursty workload. Raising it with the adaptive retry mode adds client-side rate limiting as well as backoff.
jsimport { DynamoDBClient } from "@aws-sdk/client-dynamodb";
const client = new DynamoDBClient({
maxAttempts: 5,
retryMode: "adaptive" // backoff plus client-side rate limiting
});
Reduce concurrency rather than raising the downstream limit
When the function itself is the source of the load, capping its concurrency protects the downstream service. This is the standard fix for a Lambda draining a queue faster than the database behind it can absorb.
yamlResources:
ProcessFunction:
Type: AWS::Serverless::Function
Properties:
# Cap the fan-out so the database is never asked for more than it can take.
ReservedConcurrentExecutions: 25
Raise the quota when the demand is legitimate
Account concurrency and most per-service rate limits are adjustable. Where the traffic is real and the architecture is sound, request an increase rather than engineering around a default.
bash# Current account concurrency quota.
aws service-quotas get-service-quota \
--service-code lambda --quota-code L-B99A9384
Also seen as
The same underlying failure, worded differently by a different runtime, SDK version, or logging layer. All of these land here — there is no separate page for each phrasing.
Related errors
Errors that show up alongside this one, or that people mistake for it.
References
- AWS Lambda Developer Guide — Lambda quotas
- AWS Lambda Developer Guide — Configuring reserved concurrency
LogStitch finds this automatically, across every invocation in your account.
Paste a log excerpt into the free web stitcher and see it grouped, classified, and measured in your browser — nothing is uploaded. Or run the Mac app against your own AWS profiles and get the same view over every function you own.