Lambda Concurrency Is a Throughput Problem, Not a Config Number
Lambda concurrency isn’t requests per second. Real throughput is concurrency ÷ average duration. A CLI demo of a deliberate throttle, and how to calculate your real ceiling
The Lambda console shows you a concurrency number and it's easy to read it as "this many requests are fine at once." That's not quite what it means. Concurrency alone doesn't tell you your real throughput you need one more number to get there, and skipping that step is how a function that looks fine on paper throttles hard under real traffic.
The number nobody multiplies out
Concurrency is how many invocations of your function can run at the same time. It is not requests-per-second. Those are only the same thing if each invocation takes exactly one second which almost nothing does.
The actual formula:
1
real max throughput (requests/sec) = concurrency ÷ average invocation duration (seconds)
A function with 1,000 concurrency and a 100ms average duration can sustain roughly 10,000 requests/sec. The same 1,000 concurrency with a 2-second average duration (an external API call, a cold dependency, a slow query) sustains roughly 500 requests/sec a 20x difference, from the exact same concurrency number, purely because of how long each call takes.
If you've only ever looked at the concurrency setting and never multiplied it against your real average duration, you don't actually know your ceiling yet.
Checking your real starting point
First, what your account actually has available this is a shared pool across every function in the account unless you've carved out reserved concurrency for specific ones:
1
2
aws lambda get-account-settings \
--query "AccountLimit.[UnreservedConcurrentExecutions]"
1
2
3
[
1000
]
1000 is AWS's standard default for a normal account. It's worth actually checking this rather than assuming it accounts can end up with a much lower ceiling than expected (a support-imposed limit, an org-level guardrail, a quota that was never raised from a lower starting point), and
1000 is a number you confirm, not one you assume.Next, set a deliberately small reserved concurrency on a test function so the throttle is easy to reproduce on purpose:
1
2
3
aws lambda put-function-concurrency \
--function-name my-test-function \
--reserved-concurrent-executions 10
Reproducing the throttle
A simple concurrent load test 50 invocations fired at once against a function reserved to 10 concurrency:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import boto3
import json
from concurrent.futures import ThreadPoolExecutor, as_completed
client = boto3.client("lambda")
def invoke():
try:
response = client.invoke(
FunctionName="my-test-function",
InvocationType="RequestResponse",
Payload=json.dumps({}),
)
return response["StatusCode"]
except client.exceptions.TooManyRequestsException as e:
return f"throttled: {e}"
with ThreadPoolExecutor(max_workers=50) as pool:
futures = [pool.submit(invoke) for _ in range(50)]
results = [f.result() for f in as_completed(futures)]
from collections import Counter
print(Counter(str(r)[:20] for r in results))
Illustrative output with reserved concurrency capped at 10, a real portion of 50 simultaneous invocations get throttled outright rather than queued:
1
Counter({'200': 11, "throttled: An error": 39})
Confirm it directly against CloudWatch rather than trusting the client-side count alone:
1
2
3
4
5
6
7
8
aws cloudwatch get-metric-statistics \
--namespace AWS/Lambda \
--metric-name Throttles \
--dimensions Name=FunctionName,Value=my-test-function \
--start-time 2026-09-13T10:00:00Z \
--end-time 2026-09-13T10:05:00Z \
--period 60 \
--statistics Sum
1
2
3
4
5
6
{
"Datapoints": [
{"Timestamp": "2026-09-13T10:01:00Z", "Sum": 39.0},
{"Timestamp": "2026-09-13T10:02:00Z", "Sum": 0.0}
]
}
That matches the client-side count almost exactly the throttles aren't intermittent noise, they're the direct, immediate consequence of asking for more concurrent executions than the reserved pool allows.
Doing the math with your own numbers
Pull your function's actual average duration straight from its own logs rather than guessing:
1
2
3
4
5
aws logs filter-log-events \
--log-group-name /aws/lambda/my-test-function \
--filter-pattern "REPORT" \
--limit 20 \
--query "events[].message"
Each
REPORT line includes a real Duration: X ms value. Average a recent sample of those, then apply the formula from the top of this post using your function's actual reserved (or account-level unreserved) concurrency. That number not the raw concurrency setting is what you should be comparing against your expected peak requests/sec.If your real ceiling comes out lower than expected peak demand, two honest options, not one default answer:
- Raise concurrency
put-function-concurrencyfor a per-function reservation, or a Service Quotas increase (lambda/L-B99A9384) for the account-wide unreserved pool if that's the actual bottleneck. - Lower average duration if the function is slow because of a dependency it's waiting on (a downstream API, a cold connection), fixing that raises your real ceiling without touching concurrency at all, and is often the cheaper fix.
Takeaway
A concurrency number by itself answers "how many at once," not "how many per second" and the second question is almost always the one that actually matters for whether your function keeps up with real traffic. The fix is one honest calculation, using your own function's real measured duration, not the number the console shows you by default.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article