← Back to list

Lambda or Fargate: a decision built from numbers

Two ways to run a Rust HTTP service on AWS: Lambda functions behind API Gateway, or a container on Fargate behind an ALB. Both work. The…

Illya Yalovoy in AWS in Plain English · 2026-06-06 05:57 · 0 claps · 17.0 min read paywalled
#aws #aws-lambda #aws-fargate #cost-optimization #performance
Open on Medium ↗
Wiki topics: ☁️ · DevOps & Cloud

Lambda or Fargate: a decision built from numbers

Two ways to run a Rust HTTP service on AWS: Lambda functions behind API Gateway, or a container on Fargate behind an ALB. Both work. The internet is full of opinions about which one is “right” — almost all unsupported, almost none calibrated to a real service shape.

This post is the calibration. The same Rust service — a small JSON store on top of DynamoDB, 25 KB payloads, three operations — deployed on both runtimes in the same account, in the same region, hit by the same client. Throughput, latency distributions, cost crossover, and operational shape, all measured. Then a decision framework that points at a single answer for each operating regime.

The short version.

  • Latency-bound services use Fargate. Across the entire distribution (p50, p99, p99.9, max) Fargate beats Lambda by measurable, repeatable factors for this Rust workload — p50 by ~4×, p99.9 by ~10×, max by ~10×. Lambda’s tail above p99.9 lives in the 400–1000 ms band; Fargate’s stays under 100 ms. If you need stable, predictable latency, the data is decisive.
  • Cost-bound services with sustained traffic above ~30 RPS use Fargate. Below 30 RPS Lambda wins, often by an order of magnitude (at 1 RPS, Lambda is 30× cheaper). Above 30 RPS the gap inverts and widens linearly: at 100 RPS Fargate is ~70% cheaper; at 500 RPS ~92% cheaper. Over 3 years at 100 RPS Fargate saves ~$8,400.
  • Genuinely unpredictable traffic that can 5× in 90 seconds uses Lambda. Fargate’s autoscaler reacts in ~7 minutes; Lambda spawns concurrent executions per request. That is the one runtime behaviour Lambda still wins on, and the gap is large.
  • Services that grow past ~5–6 features use Fargate even when Lambda is cheaper. Lambda’s per-function infrastructure grows faster than its application code; Fargate’s stays flat.
  • Provisioned Concurrency does not fix this. It removes Lambda’s cold tail at a cost essentially equal to plain Lambda+API Gateway (the duration discount cancels the PC fee), while leaving Lambda’s 4× warm-path latency disadvantage in place. At 100 RPS, Lambda+PC is 3.5× more expensive than Fargate; at 500 RPS, 12×; at 1000 RPS, 20× — and Fargate is still faster.

The rest of the post derives every one of these numbers from measurement.

1. The service under test

A single Rust HTTP service, three operations:

The business logic — router, service, repository, telemetry — lives in a library crate with no HTTP framework and no Lambda runtime. Two binary front doors depend on it: one wraps it in Axum on Tokio for Fargate, the other wraps it in the AWS Lambda runtime. Same data layer: DynamoDB with a payload table and a key-index table for the list operation. Same code path serves every request; only the front door differs.

Test configuration:

  • Region us-west-2, architecture x86_64.
  • Lambda: 512 MB memory, provided.al2023 runtime, direct boto3.invoke() for the latency measurements.
  • Fargate: c6i-class ECS tasks behind an internet-facing ALB, in a dedicated VPC with private task subnets, single NAT Gateway, and a DynamoDB Gateway VPC endpoint.
  • Load generator: c6i.xlarge in the same region as the targets, using open-loop scheduling (requests dispatched at scheduled wall-clock times regardless of response latency) so a slow tail does not deform the traffic shape.

2. Latency: the structural difference

The structural shape of the two distributions: Fargate’s is essentially unimodal; Lambda’s has a clean cold-start cluster above p99.5.

Two patterns of traffic were run against each runtime from the same generator host:

  • Sustained 10 RPS for 8 minutes (4,800 requests of get)
  • Intermittent: four 30-second bursts at 20 RPS, separated by 6-minute idle gaps (2,400 requests)

The same get operation in both. Full client-observed distribution captured per request, plus Lambda’s Init Duration parsed from each REPORT log to flag cold invocations.

Measured latency distributions

(All values in ms, client-observed wall-clock.)

Three observations land directly from this table.

Fargate’s distribution is tight; Lambda’s has a heavy tail

Fargate’s worst observed request across 7,200 calls was 94 ms. Lambda’s worst was 1,038 ms — 11× higher. The ratio of max / p50 is 16× for Fargate and 42× for Lambda. Fargate’s latency is substantially more stable, and that stability comes for free with the runtime model: a long-lived task has no cold/warm bimodality, no per-request execution-environment churn, no init duration. Once the task is running, request latency is just request latency.

Lambda’s distribution stays tight up to about p99.5 — the warm cluster runs all the way to ~55–90 ms. Then it jumps cleanly into the cold-start cluster: every observation above ~400 ms in the data is a cold invocation, no exceptions. The Init Duration on cold-flagged requests is 150–320 ms, and the cold billed duration on the Rust handler (which has to run the AWS SDK client warm-up on its first invocation) is another 490–660 ms, putting cold-path client latency in the 600–1,000 ms band.

Fargate is faster at every percentile, not just the tail

This is not a cold-start story. The 4× gap at p50 is the warm path on both runtimes: Lambda’s invoke pathway (the boto3 API call, the Lambda service routing, the execution-environment dispatch) adds ~17 ms over a direct ALB hit, every request, every time. Replace boto3.invoke() with API Gateway in production and that overhead shifts but does not disappear (API Gateway HTTP API adds 10-30 ms on its own).

For a service with a p99 latency budget of, say, 100 ms total including downstream calls, Lambda spends 24–38 ms of that budget on the runtime alone before any business logic. Fargate spends 6–9 ms. The 18 ms difference is recoverable headroom on Fargate that you do not get on Lambda.

Cold-start probability is lower than the textbook story suggests

The measured cold-share across both patterns was under 0.2%. Lambda’s keep-warm policy is more aggressive than commonly believed: 6-minute idle gaps in the intermittent pattern did not evict execution environments, and the 4,800-request sustained run produced exactly 8 cold invocations — all of them in the first 700 ms of the run, as Lambda spawned 8 concurrent environments to absorb the initial parallelism. After that initial warmup, zero cold-starts in the next 8 minutes.

This shifts the operational meaning of cold-start: it is not the dominant contributor to p99 for steady-state traffic shapes. It is the dominant contributor to p99.9 and the long tail. Production traffic patterns that would raise cold-share above ~0.2% — longer idle gaps (15+ minutes), deploys that evict all environments, sudden scale-up events that require fresh execution environments — were not measured here, and would push cold-start contamination further down the percentile curve toward p99 and below.

What SLOs Lambda and Fargate can support

Drawing the lines from measured data:

The structural conclusion: Lambda is competitive at p99 SLOs in the 50–200 ms band, and uncompetitive at p99.9 or for variance-bound SLOs. If your SLO talks about p99.9, max, or “stable” latency, the data picks Fargate decisively.

3. Cost crossover and long-term spend

Lambda’s pay-per-use line crosses Fargate’s flat floor at ~30 RPS sustained; above that, the gap grows linearly with traffic and is 12–20× by 1,000 RPS.

Both stacks pay DynamoDB and ECR identically (same tables, same operations, same payload sizes). DynamoDB cost cancels for the comparison; what differs is compute plus the front-door infrastructure.

Cost models (us-west-2, x86_64, list prices)

Lambda + API Gateway HTTP API, per million requests:

Lambda has no fixed monthly floor — you pay per request, period.

Fargate behind an internet-facing ALB, per month:

Plus compute: a 1 vCPU / 2 GB Fargate task costs $36.04/month. Measured single-task get ceiling is ~700 RPS at full CPU; running the autoscaler at 60% CPU target means each task carries ~420 sustained RPS at equilibrium.

Monthly cost at sustained RPS

The crossover lands cleanly around 30 RPS sustained. Below that Lambda wins decisively; above it Fargate wins by a multiple that grows linearly with RPS.

Three-year cost

Engineers often pick on the monthly bill and miss the multi-year picture. Same workload, sustained for 36 months:

At any traffic above ~50 RPS sustained, the multi-year savings on Fargate are non-trivial; at 100+ RPS they are large enough that picking Lambda on cost is a measurable mistake.

What moves the crossover

These numbers assume API Gateway HTTP API in front of Lambda — the standard production HTTP front door. Three knobs change the picture non-trivially:

  • Function URLs instead of API Gateway eliminate the $1.00/M Gateway charge. Lambda cost drops to ~$0.27/M. The crossover shifts from ~30 RPS to ~140 RPS sustained — meaning Lambda stays cost- competitive much further if you can live without API Gateway’s features (custom domains, auth, throttling, etc.).
  • ARM (Graviton) is ~20% cheaper on both runtimes. Crossover RPS is approximately preserved; both curves shift down proportionally.
  • Multi-AZ NAT (the standard production pattern, not the cheap benchmark configuration) adds another ~$33/mo per additional NAT. Fargate’s floor moves to ~$95/mo and the crossover drops from ~30 RPS to ~21 RPS sustained.

For a service that actually needs the API Gateway features and is running multi-AZ NAT (the real-world default), the crossover is closer to ~20–25 RPS sustained.

4. Provisioned Concurrency: the trap

PC removes the deep tail (p99.9: 648 ms → 69 ms; max: 1,038 ms → 92 ms) while leaving Lambda’s warm-path disadvantage untouched (p50 stays at 23 ms vs Fargate’s 6 ms). The PC profile is the Fargate profile shifted up by 17–20 ms — at multiple times the cost.

Lambda’s official answer to the cold-start tail is Provisioned Concurrency (PC): pay an hourly fee to keep N execution environments pre-warmed at all times, eliminating the Init Duration tax on requests that land on a PC instance, and getting a 20% discount on the per-invocation Duration rate in exchange.

It is the official answer, and it is also the trap. The data shows why on both axes — cost and latency.

What PC costs

Provisioned Concurrency pricing (us-west-2, x86, list, 2026):

For a 512 MB Rust function, one always-on PC instance costs $5.48/month. You need enough PC instances to cover your peak concurrency, or any traffic above the PC ceiling falls back to non-PC execution environments and pays the full cold-start tax. For a service at 100 RPS with ~7 ms warm Duration, mean concurrency is 0.7 and the p99 concurrency lands around 3–4; in practice you provision 4–6 PC instances to cover normal peaks with a buffer.

Concrete cost at the regimes where PC actually matters

Two things to read off this table.

First, PC adds roughly $20/month to plain Lambda at every traffic level above ~10 RPS. The 20% Duration discount on PC-served requests roughly cancels the PC fixed fee; the cost shape of Lambda+PC is within 5–10% of plain Lambda+APIGW. PC is not a cost optimization — it is a pure latency patch with negligible cost impact in either direction.

Second, the multiple by which Fargate is cheaper than Lambda+PC grows linearly with RPS, exactly the same as plain Lambda+APIGW does. At 100 RPS, Lambda+PC costs $251/month extra per month above Fargate; at 500 RPS it costs $1,559/month extra, or $18,708 per year, or $56,124 over three years — buying nothing but parity on the deep tail with what Fargate gives you for free.

What PC actually fixes — and what it does not

PC removes Init Duration from the latency profile of any request that lands on a PC instance. It does not remove Lambda’s warm-path disadvantage. The ~17 ms gap between Lambda warm p50 (23 ms) and Fargate p50 (6 ms) measured in this study is the boto3 invoke pathway plus Lambda service dispatch — paid on every request whether the environment is provisioned or not.

The inferred Lambda + PC profile (Init Duration removed, warm-path unchanged), compared against Fargate measured:

PC buys you the deep tail. PC does not buy you the warm path. The “fixed” Lambda profile with full PC coverage is roughly the Fargate profile shifted up by 17–20 ms across every percentile — at a 3.5× to 20× cost premium depending on RPS.

The mechanic of the trap

Real services arrive at this configuration not by choice but by accumulation. The recurring pattern:

  1. Service ships on Lambda small and cheap. 1–5 RPS sustained, ~$5–20/month, ~30 ms p99. Excellent fit. Nobody is reading this post yet.
  2. Service grows over months. Traffic crosses 30 RPS sustained. Monthly bill flips above Fargate’s floor. A few customer complaints arrive about occasional second-long responses (scale-up events spawning cold environments). Nobody reaches for migration — the platform decision is two years old and “works.”
  3. Engineering reaches for Provisioned Concurrency. Adds $20–100/month. The cold tail shrinks. The monthly bill is now triple-digit and climbing with traffic. P50 is still 23 ms; the team is not benchmarking p50, only the cold-tail incident that triggered the PC decision.
  4. Service hits 200–500 RPS. Monthly bill is now $1,000–2,000. A finance review surfaces the spend. Someone computes Fargate’s cost for the same workload: $99–135/month. The annualized gap is $10,000–25,000. A migration proposal lands on the table.
  5. Migration estimate comes back in person-weeks. Container build pipeline, Dockerfile, Terraform for VPC + NAT + ALB + ECS
  • autoscaling, IAM redesign, observability rewiring, deploy pipeline rework, on-call playbook updates, knowledge transfer to engineers who have only ever operated Lambda. The decision collapses to “spend N person-weeks now to save $X per month later,” and N person-weeks usually win.

The end state: the service runs on Lambda+PC, costs many times what Fargate would, performs worse at p50 and p95, and the team has made the “don’t migrate yet” decision so many times that the question is no longer revisited.

When PC is the right answer

Two narrow cases:

  1. You genuinely cannot leave Lambda. A regulatory requirement, a mandated platform, a hard dependency on a Lambda-specific feature (e.g., LambdaAuthorizer on API Gateway, a built-in Lambda destination, a tightly-coupled CodeDeploy hook), or an organizational mandate. PC is the best available patch.
  2. Your traffic is genuinely low and stays low. Below ~25 RPS sustained, Lambda+PC remains cheaper than Fargate while delivering acceptable tail latency. This is the only “natural fit” regime for PC.

If neither applies, and the post-mortem on a Lambda deployment ends in “we should add Provisioned Concurrency,” the structurally correct move is to migrate to Fargate before adding it. Migration cost is a one-time spend; PC cost is a multi-year-recurring spend that buys nothing but parity on the deep tail with what Fargate provides for free, while still paying more per request and serving p50 traffic 4× slower.

5. Scaling reaction time

The autoscaler does not react in seconds. ~7 minutes pass before the first new task launches; doubling continues every 2–3 minutes after that. This is the only runtime behaviour Lambda still wins on outright.

This is the one runtime behaviour where Lambda’s model wins, and the gap is wide.

Lambda scales per-invocation. 200 concurrent requests spawn up to 200 concurrent execution environments (subject to account/region concurrency limits, default 1000). The reaction time to a 10× traffic spike is essentially the cold-start cost of the new environments — sub-second on Rust.

Fargate scales via CPU target tracking on a CloudWatch metric. The measured reaction time from sustained 3× overload on a 1-vCPU cell:

overload start         1 task    (CPU pegged ~100%)
+~6.8 min              → 2 tasks   (first autoscaler action)
+~9 min                → 4 tasks
+~11.5 min             → 6 tasks

First scale-out action ~7 minutes after the overload begins, then the autoscaler roughly doubles every 2–3 minutes. This is not tunable into the seconds range — it is the CPU metric resolution (1 min) plus the alarm requirement (3 datapoints) plus the new task’s cold-start. You can mitigate by raising min_capacity, switching to request-count target tracking, or step scaling, but you cannot get under ~3-5 minutes for the first reaction with CPU-driven autoscaling.

The decision rule from this: if your traffic can 5× in under 90 seconds and you cannot tolerate 5–10 minutes of degraded p99 during the transition, Fargate is the wrong runtime regardless of what every other section of this post says. Lambda’s per-invoke scaling is the right architecture for spike traffic.

6. Operations over years

Cost and latency are first-order arguments. The second-order argument is what happens to the codebase and its infrastructure surface as the service grows over multiple years and multiple engineers.

Infrastructure surface

Lambda: one IAM role, one Lambda function per language artifact, two DynamoDB tables. Call it ~10 resources for the runtime study’s single-function configuration. Each new feature added as a new Lambda function brings: a function resource, a function IAM role, a CloudWatch log group, an event source mapping or trigger configuration, and any shared-layer wiring. A 6-feature service running 10 functions arrives at roughly 40–60 IAM/event/log resources plus the original DynamoDB tables.

Fargate: 35 Terraform resources at week one — VPC, four subnets, IGW, NAT, route tables, DynamoDB Gateway endpoint, two security groups, IAM exec role + task role + instance profile, ECR repository + lifecycle policy, CloudWatch Logs group, ALB + target group + listener, ECS cluster + task definition + service, autoscaling target + policy, two DynamoDB tables. A 6-feature service adds routes inside the same binary — no new infrastructure. The 35-resource count is roughly constant for the life of the service.

The crossover point on infrastructure complexity is around feature 5–6. Past that, Fargate’s surface is smaller in absolute terms than Lambda’s, despite starting four times larger.

Deploy iteration loop

Lambda Rust: cargo lambda build --release, zip the binary, aws lambda update-function-code. Source-to-live in ~30 seconds. This is genuinely excellent for fast iteration.

Fargate Rust: cargo build --release inside a multi-stage Dockerfile, ECR login, docker push, terraform apply with the new image URI, ECS rolling deployment with zero-downtime continuity (the old task serves until the new one is healthy). Source-to-live: 5-10 minutes for code-only changes, ~15 minutes for the first deploy.

Fargate’s iteration loop is slower but produces the prod artifact. The same container that ran on your laptop with docker run is the one serving traffic. Lambda’s local-dev story (SAM, LocalStack, cargo lambda watch) approximates the runtime; none of them reproduces it.

Production debugging

Lambda gives you CloudWatch Logs. That is the story. No shell, no attach, no live profiling, no live introspection. Every production incident is a forensic exercise on log data, possibly enriched with X-Ray traces if you wired them up.

Fargate with a distroless image is similar — no shell — but the production debug pattern is well-understood: debug sidecar. Ship a secondary container with busybox plus debug tools, deploy it alongside the runtime container, ECS Exec into the sidecar when you need to inspect live state. The runtime image stays minimal and locked down; debugging is opt-in and capability-bounded. Plus you can take CPU/heap profiles from the live process via any of the standard external tools (no Lambda equivalent exists).

Warm in-process state

Lambda rebuilds connection pools, JWKS caches, prepared statements, and lazy-initialized clients on every cold start. Provisioned Concurrency mitigates this — at a cost equal to running Fargate. Without Provisioned Concurrency, services that depend on warm connection state pay the cost on every cold environment.

Fargate warms once per task and stays warm for hours or days. Connection caches, JIT, JWKS, prepared statements — all reach steady state and remain there. Downstream connection pressure is predictable from task count.

Vendor lock-in

Lambda is the deepest lock-in in the AWS catalog: handler signatures, event shapes, runtime contract, cold-start characteristics, and deployment tooling are all AWS-specific. Migrating a Lambda service to anything else is a rewrite.

Fargate locks you into ECS, not into a programming model. The container runs on EKS, on a laptop, on a different cloud, on a Pi. The orchestrator changes; the program does not.

What this adds up to over years

Lambda has lower upfront ops cost and higher per-feature friction over time. Each new capability adds infrastructure faster than it adds application code; debugging is forensic-only; lock-in is total; the runtime is awkward to run locally.

Fargate has higher upfront ops cost and lower per-feature friction over time. Initial 35-resource infrastructure stays constant as features accumulate; debugging is interactive when you need it; lock- in is to an orchestrator, not a programming model; the artifact is portable.

The operational crossover — the feature count at which Fargate becomes easier to maintain than Lambda — is around 5–6 features in our direct experience building both. Past that, the Fargate codebase is structurally simpler than the Lambda codebase doing the same work.

7. The decision

Walk these four questions in order. Stop at the first one that decides for you.

Step 1 — What is your latency SLO?

If any cell here picks Fargate, you are done. Latency disqualification is not overridden by cost or operational considerations — those are “how to spend the budget” questions; latency is “can the budget be met at all.”

Step 2 — Is your traffic genuinely unpredictable?

If your traffic can 5× in under 90 seconds and a 5–10-minute window of degraded p99 during scale-out is unacceptable, Lambda. Fargate’s autoscaler reaction is structurally too slow. This is the only place Lambda still wins on runtime characteristics; the gap is large.

Otherwise proceed.

Step 3 — Where does your sustained traffic sit on the cost curve?

Function URL instead of API Gateway moves the crossover up to ~140 RPS; multi-AZ NAT moves it down to ~20 RPS. Recalibrate for your config.

Step 4 — How long will the service live, and how fast will it grow?

Where neither is right

  • Multi-minute compute: Step Functions, Batch, or plain EC2.
  • Anything event-driven with native AWS triggers (SQS, S3, EventBridge, DDB streams) where you would write a single function whose lifetime is one event — Lambda is what this is for; do not contort it into Fargate.
  • Workloads where the data layer (DynamoDB at 25 KB items, RDS, S3) dominates the bill so completely that compute is a rounding error — fix the data layer first; the runtime choice does not matter.

8. What this study does not cover

Honesty about scope:

  • Provisioned Concurrency latency was not directly benchmarked. §4 uses measured warm-path Lambda numbers and infers the PC profile by removing the cold contribution. A direct measurement is the only thing missing to make the §4 latency table fully first-party rather than inferred-from-first-party.
  • Cold-share under longer idle gaps and deploy events. The measured cold share (~0.2%) is for 6-minute idle gaps in a single test session. Longer gaps (15+ min), deploys that evict all environments, and traffic spikes requiring scale-up will all raise cold share. The structural conclusion (Lambda p99.9 lives in the cold band) still holds; the exact percentile at which cold-starts begin to show up is traffic-shape dependent.
  • Fargate large-cell ceilings (2 vCPU and 4 vCPU) were load-generator-bound in our throughput tests; the per-task RPS ceilings cited for cost modelling come from the 1-vCPU cell where CPU was the binding constraint. Larger tasks would handle more RPS per task than the cost table assumes, marginally improving the Fargate side of the crossover.
  • Spot Fargate lowers the compute side of the Fargate cost significantly at the price of interruption handling. Not measured.
  • ARM (Graviton) is ~20% cheaper on both sides and shifts the crossover only slightly.

The thesis is simple and the data backs every claim in it:

Fargate is the structurally correct runtime for latency-bound, sustained, or growing services. Lambda is the structurally correct runtime for cheap-and-bursty, small-and-young, or event-driven services. The crossover lines are ~30 RPS sustained on cost and ~100 ms p99.9 on latency — for this service shape, on this hardware, in 2026. Calibrate to yours.


메타데이터
post_id
b6fd8fcd7bce
slug
lambda-or-fargate-a-decision-built-from-numbers-b6fd8fcd7bce
url
https://medium.com/@yalovoy/lambda-or-fargate-a-decision-built-from-numbers-b6fd8fcd7bce
canonical_url
https://medium.com/@yalovoy/lambda-or-fargate-a-decision-built-from-numbers-b6fd8fcd7bce
author_url
https://medium.com/@yalovoy
status
ok
fetched_at
2026-06-16 19:09:56