Chatty function chains: what waiting costs in serverless
When one function calls another synchronously, you pay for both at once. Chains of waiting functions are the most expensive common anti-pattern in serverless, and they are easy to spot.
Quick answer
A function that synchronously invokes another and waits pays for both simultaneously. If function A at 1024 MB waits 400 ms for function B at 1024 MB, you are billed 0.8 GB-seconds for 400 ms of work, exactly double. A chain of four functions each waiting on the next can bill 4 to 10 times the compute of the work performed, because outer functions idle while inner ones run. At 10 million requests a month, a 3-deep chain of 1 GB functions doing 200 ms of real work each costs about $200 instead of $100. The fixes are asynchronous invocation, merging functions that always run together, or a workflow service that does not bill for waiting.
There is one serverless pattern that reliably doubles or triples a bill without adding any capability, and it is the easiest one to write: function A calls function B and waits for the response. Every millisecond B spends working, A is also billed, sitting idle. The platform has no way to know A is doing nothing useful, so it charges for the memory it reserved.
The double billing arithmetic
| Depth | Real work | Billed GB-seconds (1 GB each) | Multiplier |
|---|---|---|---|
| 1 function, 200 ms | 200 ms | 0.20 | 1.0x |
| 2 chained, 200 ms each | 400 ms | 0.60 | 1.5x |
| 3 chained, 200 ms each | 600 ms | 1.20 | 2.0x |
| 4 chained, 200 ms each | 800 ms | 2.00 | 2.5x |
| 5 chained, 200 ms each | 1,000 ms | 3.00 | 3.0x |
The outermost function stays alive for the full chain duration, the second for everything below it, and so on. The billed total is the sum of a triangular series, so cost grows quadratically with chain depth while useful work grows linearly. At 10 million requests a month, the 3-deep chain costs $200 in duration against $100 for the same work in one function, plus $0.60 in extra request charges.
Where it gets worse
Two factors amplify the problem. First, memory mismatch: a thin orchestrator function often gets provisioned at the same memory as the heavy worker it calls, so the idle waiting happens at the expensive rate. An orchestrator at 2048 MB waiting on a worker at 2048 MB burns 2 GB-seconds per second of wait.
Second, cold starts compound down the chain. If each hop has a 10 percent chance of a 300 ms cold start, a 4-deep chain has a 34 percent chance of at least one, and the cold start of the innermost function is paid for by every function above it in the stack. Tail latency degrades, timeouts get raised to compensate, and raised timeouts make failed invocations more expensive.
| Chain configuration | Cost per 10M requests |
|---|---|
| 1 function, 1 GB, 600 ms | $102.00 |
| 3 chained, 1 GB each, 200 ms each | $200.60 |
| 3 chained, orchestrators at 2 GB | $366.60 |
| 3 chained with 10% cold starts adding 300 ms | $230.00 |
Fixing it
The first question is whether the call needs to be synchronous. If the caller does not use the response, use asynchronous invocation or an event, and the caller returns immediately. A function that fires an event and exits in 20 ms instead of waiting 400 ms for a response cuts its own duration by 95 percent.
The second question is whether the split is earning anything. Two functions that are always called together, deployed together, and owned by the same team are one function with extra billing. Merging them removes the double billing, the invocation charge, and one potential cold start. Splitting is worth paying for when the pieces scale differently, need different memory, have different failure domains, or are owned by different teams. It is not worth paying for as a matter of style.
Where a genuine synchronous dependency exists across a real service boundary, a workflow service is often cheaper than a waiting function. Step Functions does not bill for wait time, so a state machine coordinating three tasks pays transitions plus the tasks themselves, with no idle duration. At 10 million executions a month, Standard workflow transitions would dominate, but Express at $1.00 per million plus duration frequently beats a chain of waiting functions outright.
Concurrency is consumed too
There is a second, less obvious cost to waiting functions: they occupy concurrency. An account has a regional concurrent execution limit, commonly 1,000 by default, and every function sitting idle in a chain holds a slot. A 3-deep chain serving 300 requests per second with 200 ms per hop keeps roughly 180 executions busy instead of 60, so the same traffic consumes three times the concurrency headroom. When the limit is reached, new invocations are throttled, synchronous callers see errors, and asynchronous ones retry, which adds cost while delivering nothing. Raising the account limit is free but takes a support request, and it does not remove the underlying waste of paying for memory that is doing nothing.
Spotting chains in an existing system
Look for functions whose average duration is close to the sum of their downstream calls, which means they are mostly waiting. Compare a function's billed duration against its own CPU time if you have that instrumentation. Trace a request and count how many functions are simultaneously in flight for a single user action. And look for the SDK Lambda invoke call in your codebase, which is the direct signature of the pattern.
Chain depth is a cost metric as much as a latency metric, and it deserves a place in design review alongside the per-function cost model. Price the full call graph against the resource catalog rather than the individual functions.
FAQ
Does one Lambda calling another cost double?
Yes, while it waits. If function A synchronously invokes function B and waits 400 ms for the response, both functions are billed for those 400 ms. At 1024 MB each, that is 0.8 GB-seconds for 400 ms of work, exactly twice the cost. The platform cannot tell that the outer function is idle, so it bills the memory it reserved.
How does cost grow with function chain depth?
Quadratically, while useful work grows linearly. The outermost function stays alive for the whole chain, the next for everything below it, and so on, making the billed total a triangular series. A 3-deep chain of 200 ms functions bills 1.2 GB-seconds for 600 ms of work, a 2x multiplier, and a 5-deep chain reaches 3x.
When is splitting functions worth the extra cost?
When the pieces genuinely scale differently, need different memory sizes, have different failure domains, or are owned by different teams. Two functions always called together, deployed together, and owned by the same team are one function with extra billing, an extra invocation charge, and an extra potential cold start.
Do cold starts make function chains worse?
Yes, twice over. The inner function's cold start is paid for by every function waiting above it in the stack. And the probability compounds: if each hop has a 10 percent chance of a 300 ms cold start, a 4-deep chain has about a 34 percent chance of at least one, which degrades tail latency and pushes teams to raise timeouts.
Is Step Functions cheaper than chained function calls?
Often, because workflow services do not bill for wait time. A state machine coordinating three tasks pays for transitions plus the tasks themselves, with no idle duration charged for outer functions. Express workflows at $1.00 per million requests plus duration frequently beat a chain of synchronously waiting functions at the same volume.
How does C3X help with function chain cost?
C3X prices all the functions in a design from Terraform, so the compute cost of a call graph is visible as a whole rather than per function. Since splitting a function into a chain is a Terraform change, the added cost of a new hop shows up at review time, when the design is still a code change rather than a refactor.
What to do next
Price the call graph, not the function. C3X reads your Terraform and prices your resources against a live catalog. Start with the quickstart.
Share this post
Try C3X on your own Terraform
Free and open source. No API key required. One command to install, one command to estimate.