How Lambda, Fargate and Cloud Run bill for compute
The three platforms bill different units, and those units decide how much idle time costs you.
AWS Lambda charges a fee per request plus a duration charge equal to execution time multiplied by allocated memory (GB-seconds). CPU scales with the memory setting, so memory is effectively your compute size. When no requests arrive, the compute bill is zero.
AWS Fargate charges per vCPU-second and per GB-second for as long as a task is running. The task's sizing sets the rate, and the meter runs whether the task serves a thousand requests or none. An API that needs one task available at all times pays for every hour of the month.
Google Cloud Run has two CPU allocation modes. With request-based billing, CPU is allocated only while requests are being processed, and you pay per request plus the instance time spent handling them. With instance-based billing, CPU stays allocated for the instance's whole lifetime and you pay for that time, much like Fargate. In either mode, Cloud Run can scale to zero instances when traffic stops.
| Platform | Billing unit | Idle cost | Scales to zero |
|---|---|---|---|
| Lambda | Requests + GB-seconds of execution | None | Yes |
| Fargate | vCPU-seconds + GB-seconds of task runtime | Full task rate | No, if a task is kept running |
| Cloud Run (request-based) | Requests + instance time during requests | None | Yes |
| Cloud Run (instance-based) | Instance lifetime | Charged while instances exist | Yes, when instances shut down |
Request-priced billing usually wins at low traffic because a low-traffic API is idle most of the time. Lambda and request-based Cloud Run charge nothing for that idle time, while a continuously running Fargate task charges for all of it. This comparison is aimed at teams running APIs with modest or bursty request volume, where idle hours make up most of the month.
Who should use this break-even model
The model is for engineers and tech leads who own the hosting decision for a small HTTP API and also see its bill. Typical cases:
- Teams launching a new internal or low-volume public API who need to pick between functions and containers before the first deploy.
- Teams already on Lambda or request-based Cloud Run who are seeing traffic grow and want to know whether an always-on container would now be cheaper.
- Teams already on Fargate with a mostly idle service, who suspect they are paying for capacity nobody uses.
- Platform or FinOps engineers who review many small services and want one repeatable calculation instead of a case-by-case argument.
It is less useful if your API already runs at sustained high load across many tasks. At that point per-hour container pricing is usually the baseline, and the more relevant work is right-sizing and autoscaling rather than a serverless-versus-container comparison.
When to use it
Run the model at the points where the answer can change:
- Before choosing a platform for a new service, using expected request volume and a duration estimate from a prototype or a similar existing service.
- When traffic shifts noticeably, either up toward steady load or down toward long idle periods.
- When you change memory, task size or minimum capacity, such as enabling provisioned concurrency or Cloud Run minimum instances to fix cold starts.
- When provider pricing for your region changes, or when you move to a different region or CPU architecture.
- During periodic cost reviews, so the decision is rechecked against current metrics rather than the assumptions made at launch.
How the model works
The model reduces each platform to the shape of its cost curve. Request-billed serverless is a line through zero: cost rises in proportion to request count, and zero traffic costs zero compute. An always-on container is a flat line: it costs the same every month regardless of traffic, until load forces a larger or additional task.
Where the rising line crosses the flat line is the break-even request volume. Below it, serverless is cheaper. Above it, the container is. The inputs that set the slope of the serverless line are the per-request fee, average billed duration and configured memory. The inputs that set the height of the container line are task size, task count and hours in the month.
The rest of the article builds on that core. The formula section makes it explicit, the worked example shows how each input moves the result, and the later sections add the costs that sit outside compute: load balancers, NAT gateways, logging and warm capacity. Each of those is either another fixed monthly term or another per-request term, so they slot into the same two expressions.
A break-even formula you can fill in with your own numbers
The model needs only two cost expressions and a handful of rate variables. Write them out explicitly so every assumption is visible and can be challenged.
Serverless monthly cost ≈ R × (P_req + D × M × P_gbs), where:
Ris requests per monthP_reqis the per-request feeDis average billed duration in secondsMis configured memory in GBP_gbsis the per-GB-second compute rate
Always-on container cost ≈ N × H × (vCPU × P_vcpu_hr + GB × P_gb_hr), where:
Nis the minimum task count you keep runningHis hours in the month (about 730)vCPUandGBdescribe the task sizeP_vcpu_hrandP_gb_hrare the hourly rates
Set the two equal and solve for requests:
R_break_even = container_cost / (P_req + D × M × P_gbs)
Below that volume, serverless costs less. Above it, the always-on task does. In a spreadsheet, put each rate in its own named cell and compute R_break_even from them. Fill every rate from your provider's current pricing page for your region and architecture. Rates differ by region and change over time, so a value copied from an old blog post can move the break-even point considerably. For Cloud Run, use request-based billing for the serverless side and instance-based billing for the always-on side.
Measure D from real latency data, not from a guess. It is usually the most uncertain input.
The value of the model is that it settles the decision with numbers. When traffic, memory settings or prices change, the team updates a few cells and reruns the calculation, instead of revisiting the same debate from intuition.
P_REQ = 0.0000002 # placeholder: fee per request
P_GBS = 0.0000166667 # placeholder: rate per GB-second
D = 0.120 # average billed duration in seconds (measure from real latency data)
M = 0.5 # configured memory in GB
P_VCPU_HR = 0.04 # placeholder: rate per vCPU-hour
P_GB_HR = 0.0045 # placeholder: rate per GB-hour
VCPU = 0.25 # task vCPU
GB = 0.5 # task memory in GB
N = 1 # minimum task count kept running
H = 730 # hours in a month (approximate)
def serverless_cost(requests_per_month: float) -> float:
# Cost scales linearly with traffic: per-request fee plus compute time.
return requests_per_month * (P_REQ + D * M * P_GBS)
def container_cost() -> float:
# Fixed monthly cost: you pay for the task whether or not traffic arrives.
return N * H * (VCPU * P_VCPU_HR + GB * P_GB_HR)
cost_per_request = P_REQ + D * M * P_GBS
r_break_even = container_cost() / cost_per_request
print(f"Container monthly cost: {container_cost():.2f}")
print(f"Serverless cost per request: {cost_per_request:.10f}")
print(f"Break-even volume: {r_break_even:,.0f} requests/month")
for r in (100_000, 1_000_000, 10_000_000):
print(f"{r:>12,} req: serverless={serverless_cost(r):.2f} container={container_cost():.2f}")
A Python script that computes the serverless-versus-container break-even request volume from placeholder rate variables, which you replace with your provider's current regional prices and rerun whenever inputs change.
Illustrative estimate: a few hundred thousand requests a month
Illustrative estimate (assumed numbers, not a benchmark). The rates below are placeholders chosen for easy arithmetic. They are not current prices, and free tiers are ignored. Replace them with the rates for your region before drawing any conclusion.
Assumed inputs:
- Lambda: 300,000 requests per month, 100 ms average duration, 128 MB (0.125 GB) of memory.
- Lambda rates: $0.00002 per GB-second and $0.20 per million requests.
- Fargate: one always-on task with 0.25 vCPU and 0.5 GB, running 730 hours per month.
- Fargate rates: $0.04 per vCPU-hour and $0.005 per GB-hour.
The formulas:
- Lambda = requests × duration (s) × memory (GB) × GB-second rate + (requests ÷ 1,000,000) × request rate
- Fargate = tasks × hours × (vCPU × vCPU rate + GB × GB rate)
| Scenario | Lambda GB-seconds | Lambda monthly | Fargate monthly |
|---|---|---|---|
| Baseline (100 ms, 128 MB) | 3,750 | ~$0.14 | ~$9.13 |
| Duration doubled (200 ms) | 7,500 | ~$0.21 | ~$9.13 |
| Memory doubled (256 MB) | 7,500 | ~$0.21 | ~$9.13 |
| Both doubled | 15,000 | ~$0.36 | ~$9.13 |
Doubling either duration or memory doubles the compute portion of the Lambda bill, because you pay for GB-seconds actually consumed. The per-request charge does not change, so the total rises by less than 2×. The Fargate figure stays the same in every row. You pay for reserved capacity whether it is used or not, and at this volume the task is busy for roughly 30,000 of the month's ~2.6 million seconds, which is about 1% utilization.
The container line only changes when load forces a second task or a larger task size. At that point it moves up in a single step rather than gradually.
The fixed costs around the compute
…but the compute line is rarely the largest one on a small API's bill. The components around it are.
Lambda path:
client -> API gateway [per use] -> Lambda [per use] -> NAT gateway [hourly + per GB] -> internet
\-> log ingestion + retention [per GB]
Fargate path:
client -> ALB [hourly + usage units] -> Fargate task [hourly] -> NAT gateway [hourly + per GB] -> internet
\-> log ingestion + retention [per GB]
The Lambda path can run close to zero when idle. The Fargate path cannot, because the load balancer bills every hour whether or not traffic arrives. A NAT gateway removes the idle advantage from both paths. You need one when a function or task in a private subnet calls an external API, and each one you deploy for availability adds another hourly charge. Data transfer out to the internet bills per GB on either path.
Logging is the cost that is easiest to miss. Many frameworks log every request at default verbosity, and some log full payloads.
Illustrative estimate (assumed numbers, not a benchmark):
- Monthly log cost ≈ requests × bytes logged per request × ingestion price per GB, plus retained GB × storage price.
- With assumed inputs of 300,000 requests at 4 KB each, ingestion is about 1.2 GB a month.
- At that volume, ingestion can exceed the Lambda compute charge for the same traffic.
- Retention has no expiry by default, so the stored volume grows every month.
Leave free tiers out of the model. They apply per account, not per service. The provider can change them. Other workloads in the same account use up the same allowance. A design that only breaks even inside the free tier does not break even.
Legend: [H] = hourly, bills when idle [U] = per use (request, unit, GB)
LAMBDA PATH
+--------+ +-------------+ +----------+ +-------------+ +----------+
| client |-->| API gateway |-->| Lambda |-->| NAT gateway |-->| internet |
+--------+ | [U] | | [U] | | [H] + [U] | +----------+
+-------------+ +----------+ +-------------+
| data transfer out [U]
v
+------------------+
| log ingestion |
| + retention [U] |
+------------------+
FARGATE PATH
+--------+ +-------------+ +----------+ +-------------+ +----------+
| client |-->| ALB |-->| Fargate |-->| NAT gateway |-->| internet |
+--------+ | [H] + [U] | | task [H] | | [H] + [U] | +----------+
+-------------+ +----------+ +-------------+
| data transfer out [U]
v
+------------------+
| log ingestion |
| + retention [U] |
+------------------+
Idle floor: Lambda path = NAT [H] only; Fargate path = ALB + task + NAT [H]
Both request paths with each component marked as hourly [H] or per-use [U], showing that the NAT gateway sets an idle cost floor on both paths and that the Fargate path also carries hourly ALB and task charges.
When serverless stops being cheaper
Request pricing is cheap because idle time costs nothing. Any feature that bills you for idle capacity moves serverless back toward the container cost model.
Minimum capacity is a fixed cost
Lambda provisioned concurrency and Cloud Run minimum instances keep environments warm and bill for them whether or not requests arrive. In the break-even formula, move them out of the per-request term and into the fixed monthly term. The cost is warm units × configured memory (or vCPU) × hours in the month × the provider's rate for that reserved capacity. Requests served by warm capacity are still billed, sometimes at a different rate, so keep that term as well. Once the fixed term approaches the cost of a small always-on Fargate task, the main financial reason for choosing serverless is gone.
Cold starts push you there
Low traffic is when cold starts happen most. Long gaps between requests let the platform reclaim idle environments, and the next caller pays the startup latency. If that latency breaks a latency target, the usual fix is minimum capacity, which brings back the idle cost the model was meant to avoid. Price that fix before you choose the platform, not after.
Steady traffic and migration cost
When traffic becomes sustained, instances are rarely idle, and per-request pricing loses its advantage over per-hour container pricing. Migrating at that point costs engineering time. Handler-shaped code, provider-specific event formats and IAM wiring rarely port directly to a long-running server. Keep business logic behind a plain HTTP or function interface so that a later move stays cheap.
HOURS_PER_MONTH = 730
warm_units = 2 # environments kept warm at all times
memory_gb = 1.0 # configured memory per environment
warm_rate_gb_hour = 0.015 # placeholder: price per GB-hour of reserved capacity
per_request_rate = 0.0000004 # placeholder: request + duration cost per call on warm capacity
task_vcpu, task_gb = 0.25, 0.5
vcpu_rate_hour = 0.04 # placeholder: price per vCPU-hour
gb_rate_hour = 0.0045 # placeholder: price per GB-hour
def serverless_monthly(requests):
# Fixed: billed whether or not any request arrives.
fixed = warm_units * memory_gb * HOURS_PER_MONTH * warm_rate_gb_hour
# Variable: requests on warm capacity are still billed.
return fixed + requests * per_request_rate
def container_monthly():
# Flat cost, independent of request volume (until you need a second task).
return (task_vcpu * vcpu_rate_hour + task_gb * gb_rate_hour) * HOURS_PER_MONTH
fixed_only = serverless_monthly(0)
container = container_monthly()
print(f"serverless fixed term at zero traffic: ${fixed_only:.2f}")
print(f"always-on container task: ${container:.2f}")
if fixed_only >= container:
print("Warm capacity alone costs at least as much as the container.")
else:
crossover = (container - fixed_only) / per_request_rate
print(f"Container is cheaper above ~{crossover:,.0f} requests/month")
An illustrative estimate, using placeholder rates, that prices provisioned concurrency or minimum instances as a fixed monthly term and finds the request volume at which a small always-on container task becomes cheaper.
Runtime, concurrency and portability trade-offs
Price is rarely the deciding factor. The platform constraints below cost engineering time instead of invoice dollars.
| Constraint | Lambda | Fargate | Cloud Run |
|---|---|---|---|
| Requests per instance | One per execution environment | Many, limited by your server | Many, up to a concurrency setting |
| Request duration | Hard platform maximum | No platform cap | Configurable timeout |
| WebSockets, long jobs | Needs a separate gateway or a job service | Native to your server | Possible, bounded by timeout |
| In-process cache | Per environment, discarded when recycled | Lives as long as the task | Lives with the instance; lost when it scales to zero |
| Deployable artifact | Handler plus provider event format | Container image | Container image |
Lambda's payload caps and execution limits push large uploads and slow work into other services, such as object storage or queues. Each of those services needs its own wiring.
The concurrency model matters most at the database. Each Lambda environment holds its own connections, so a burst scales connections linearly. On containers, many requests share a single pool.
Illustrative estimate (assumed numbers, not a benchmark): connections = concurrent instances × connections per instance. Take a burst of 200 concurrent requests:
- Lambda: 200 environments × 1 connection = 200 connections.
- Containers: 4 instances × a pool of 10 = 40 connections.
If the database allows 100 connections, Lambda needs a managed connection proxy, which carries its own charge.
Reaching a private database means attaching Lambda or Fargate to VPC subnets, or configuring VPC egress on Cloud Run. Outbound internet calls then need a NAT path.
Container images move between Fargate, Cloud Run and Kubernetes with configuration changes. Handlers need an adapter or a rewrite.
How the model saves engineering time
The direct saving is in decision time. Without a shared model, platform choice for a small API tends to be argued from preference and remembered anecdotes, and the same argument comes back at every cost review. With the two formulas in a script or spreadsheet, the discussion becomes a check of the inputs: is the request count right, is the measured duration right, are the rates current for this region. Disagreements turn into questions that metrics can answer.
It also reduces rework:
- Avoiding a premature migration. If the model shows a service is still well below break-even, there is no reason to spend time porting handlers to a long-running server.
- Avoiding a late migration. Rerunning the model as traffic grows shows the crossover coming, so a move can be planned rather than done under cost pressure.
- Pricing fixes before building them. Cold-start mitigations, NAT gateways, connection proxies and log retention can be added as terms before anyone configures them, which avoids building a setup and then tearing it down when the bill arrives.
- Reusing one calculation across services. A platform team can apply the same script to every small API, changing only the inputs, instead of producing a separate analysis each time.
The model does not account for engineering effort on its own. Add an estimate of recurring operational work, such as image patching or autoscaling tuning, alongside the compute figures when the cost difference between options is small.
Decision checklist and alternatives
Work through these questions in order. Each one tends to rule an option out rather than pick a winner.
- Is traffic spiky or steady? Spiky traffic with long idle gaps favours per-request billing, because idle time costs nothing. Steady traffic around the clock wastes that advantage and pays a higher unit price for it.
- Can clients tolerate cold starts? If a slow first request after idle breaks an SLO, you need something warm. That means minimum instances or an always-on task, and you pay for idle capacity either way.
- Do you need long-lived connections or background work? WebSockets, streaming, and in-process queue consumers fit poorly with function execution limits and request-scoped CPU. A container that owns its process lifetime fits better.
- Must it reach private resources in a VPC? This adds networking cost and setup on every option. Factor NAT into the fixed baseline before comparing compute prices.
- How much is operational simplicity worth? One engineer-hour a month spent patching images or tuning autoscaling can outweigh the compute savings at low volume.
| Profile | Fits best | Why |
|---|---|---|
| Spiky, latency-tolerant, short handlers | Lambda | Zero idle cost, minimal operations |
| Spiky, container-packaged, occasional cold start acceptable | Cloud Run, scale-to-zero | Per-request billing with portable images |
| Low but latency-sensitive | Cloud Run, minimum instances | Warm capacity without managing a cluster |
| Steady load, long-lived connections, background work | Right-sized Fargate task | Predictable flat cost and full process control |
Treat every figure in this article as an illustrative estimate. Before deciding, rerun the break-even model with current rates for your region and with real request counts, durations, and memory usage taken from your own metrics.
How OneEnv can help
Disclosure: OneEnv is the product behind this blog.
OneEnv (oneenv.io) is a workspace-based governance platform for the things services share — environment variables, database structure, API routes and validation rules. A change to a shared value is staged on a branch, reviewed once, and then every affected service explicitly approves or rejects it before it goes live, so you can see who a change touches before it ships instead of finding out in production.
Applied to lambda vs fargate cost low traffic api, this is where it helps:
Saves time
- Every service that inherits a shared value is listed in one place, so nobody searches repos and chats to find out who is affected.
- One review per branch replaces coordinating each team separately; each service still keeps its own approve/reject.
- Changing a shared variable is one edit and one review instead of editing every service's
.envby hand.
Saves cost
- Fewer production incidents and rollbacks caused by a shared value, route or column changing under a consumer unannounced.
- Less duplicated configuration to maintain: one shared definition inherited by many services.
- A wrong or stale value is caught at review time by the services it affects, not by a failed deploy.
- OneEnv has a free plan (1 workspace, unlimited services and members, full governance), so trying it costs nothing.
These are qualitative benefits, not measured results — how much you save depends on how many services share the value.
Want to try it? Start at oneenv.io.