Build1 publisher3 min readPublished
One SQS consumer can empty a default Lambda concurrency pool in under four minutes
A standard SQS event source mapping climbs toward 1,250 concurrent instances while the default account pool holds 1,000. The setting that would cap it sits on function configuration, at one API call per function to read.
The Engineer · Build desk

What happened
- Lambda defaults to 1,000 concurrent executions per Region shared on demand by every function without a reservation, and 100 of those units can never be reserved, capping any single reservation at 900.
- A standard SQS event source mapping starts at five concurrent invocations, adds up to 300 more per minute while messages keep arriving, and tops out at 1,250 concurrent instances.
- The check for this sat unbuilt for months because GetFunctionConcurrency returns one function per call while ListFunctions returns whole pages of them.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- exposure The blast radius is every unreserved function in the Region, so the first symptom lands on services whose owners have no idea another team's queue backed up.
- constraint Code review cannot catch this, and neither can an assistant generating code from the repository, because the setting that prevents it is not a line in the repository.
- cost Visibility costs one API call per Lambda on every analysis. That price is what kept the check out of the tool for months.
- decision Capping the consumer means reserving concurrency for it, and reserved units leave the pool the unreserved functions draw on, so somebody has to decide which functions get a guaranteed floor.
Climbing from five concurrent invocations to 1,000 at 300 a minute takes about three minutes and twenty seconds [1]. On a default account the shared pool is therefore gone before the event source mapping gets near its own ceiling of 1,250 concurrent instances [4]. The dev.to post puts the time to that ceiling at a little over four minutes [5]. That is 1,245 units of headroom divided by 300 a minute [4].
The 250-unit gap between the mapping ceiling and the default account limit is what makes a limit increase a partial fix [2]. One mapping cannot go past 1,250 [4]. Two unbounded poll consumers in the same Region need an account limit above 2,500 before anything is guaranteed for everything else [5].
The second failure does not need account-level scarcity at all. The database client is initialized outside the handler so it survives between invocations, which makes the connection count track the environment count: 1,250 environments hold 1,250 connections against a Postgres instance whose max_connections is 100 [7]. That is 12.5 times what the server will accept [3]. The queue drains for a few seconds, invocations fail on connection exhaustion, the batches go back to the queue, the backlog deepens, and Lambda scales harder [8].
Nothing in the handler explains any of it. The setting that would cap the scaling lives on the function's configuration and on the event source mapping [9], and the post reports that the handler and the batch handling can both be correct while the failure stands [19]. A reviewer reading the diff has nothing to object to. The post's framing is two contracts, trigger shape first and delivery and retry semantics second, and it argues a code assistant asked for a queue consumer gets `event.Records[0].body`, the loop and the error handling right while having no way to know the second contract, because the second contract is not in the repository [10].
Reading the reservation costs one API call per function, either `GetFunctionConcurrency` or the `Concurrency` block from `GetFunction` [18]. `ListFunctions` returns many functions per page [12]. A naive implementation turns a handful of list calls into one call per Lambda in the account, on every analysis [12]. The commit that finally closed the issue said so: "GetFunctionConcurrency is one call per function, which is why this sat unbuilt" [11].
The narrowing is the good engineering here. Unbounded scaling only has teeth on poll-based sources, so the filter runs before any per-function call: `const POLL_TRIGGERS = new Set(['sqs', 'kinesis', 'dynamodb', 'msk'])` [14]. A queue hands a function as much work as the source contains and there is no backpressure from the caller, because there is no caller; an API-triggered function's concurrency is bounded by whoever is calling the API [15].
Collecting the field is now cheap. Grading it is the unsolved half: the author wrote, "I could not justify the cost for a check I could not even grade properly once I had the data" [17], and reports that the cost problem had a clean answer while the grading problem did not, calling the grading problem the more important of the two [16]. Whether a missing reservation is a defect depends on the account limit and on what the downstream will accept. The scaling numbers are Lambda's defaults for a standard queue [3], so for the four-minute figure to describe a given account, the 1,000-execution limit has to be untouched [1].
What to watch
- Whether the tool ships a grading rule for a missing reservation on a poll-triggered function, or leaves the finding unscored.
- Section 3 of issue #87, still sitting in the tracker now that the reserved-concurrency section is closed.
- Any change to the SQS scaling defaults of five invocations, up to 300 more a minute and a 1,250 ceiling, or to the 1,000-execution account default.