# Tuning AWS Lambda for Cost: The Memory Myth, Measured

- URL: https://karandeepsingh.ca/posts/lambda-cost-tuning-memory-graviton/
- Author: Karandeep Singh
- Published: 2026-08-10
- Series: Serverless on AWS
- Tags: aws, lambda, serverless, cost
- Summary: Most Lambda cost advice repeats folklore. This walks through what AWS actually documents and bills — GB-seconds, the 1,769 MB one-vCPU line, the exact 20% arm64 discount and where it does not apply — then shows how to measure your own function instead of guessing.

Most Lambda cost advice is folklore passed between blog posts. "Raise the memory, it gets
cheaper." "Switch to Graviton, save 20%." Both are repeated constantly, both are wrong as
stated, and AWS's own documentation contains the numbers that show why.

This is the version built from primary sources: what you are actually billed for, what
the memory slider really does, where the 20% is real and where it evaporates, and how to
find the right setting for *your* function instead of copying someone else's.

**info:**
**Prices in this article** are the us-east-1 rates from AWS's public Price List API as of
**10 August 2026**. Lambda pricing is per-Region and does change — check the
[pricing page](https://aws.amazon.com/lambda/pricing/) before you budget against these.
The mechanics matter more than the exact digits.

## Step 1: Know what you're actually paying for

Lambda bills two things, and they behave differently:

**Duration**, in GB-seconds — memory allocated × time running. In us-east-1 on x86 this is
**$0.0000166667 per GB-second** for the first 6 billion GB-seconds a month, dropping to
$0.0000150000 and then $0.0000133334 in higher tiers.

**Requests**, at **$0.0000002 each — $0.20 per million.** Flat. No tiers.

### So what *is* a GB-second?

It is the unit that trips people up, and it is simpler than it sounds:

> **One GB-second = one gigabyte of memory, held for one second.**

Picture a rectangle. Memory is the height, time is the width, and you pay for the area.
Halve either side and you halve the bill. That's the entire pricing model.

| Memory setting | Runs for | GB-seconds per invocation | Working |
|---|---|---|---|
| 1,024 MB | 1 s | 1.0 | 1 GB × 1 s |
| 512 MB | 1 s | 0.5 | 0.5 GB × 1 s |
| 512 MB | 2 s | 1.0 | 0.5 GB × 2 s |
| 128 MB | 500 ms | 0.0625 | 0.125 GB × 0.5 s |

At $0.0000166667 each, **a dollar buys you roughly 60,000 GB-seconds** — which is why single
invocations feel free and why the bill still surprises people at volume.

Now the part that actually costs teams money:

**warning:**
**You are billed for the memory you *allocated*, not the memory your code *used*.**

A function set to 1,024 MB that never touches more than 90 MB is billed for the full
gigabyte, every millisecond it runs. Lambda does not meter your actual heap. That slider
*is* the price tag — which is exactly why "just bump the memory up, it's fine" is not free
advice.

Put the two halves together and a real bill looks like this — one million invocations of a
512 MB function that runs for 200 ms:

- **Duration:** 0.5 GB × 0.2 s = 0.1 GB-s each → 100,000 GB-s → **$1.67**
- **Requests:** 1,000,000 × $0.0000002 → **$0.20**
- **Total: $1.87**

Duration is 89% of that bill. Tuning is worth your time. Change the numbers to a 40 ms
function and the ratio flips — and so does the advice.

### Two consequences

Two things fall straight out of that split, and most tuning advice ignores both:

- **Duration has volume discounts. Requests do not.** Growing traffic slowly improves your
  effective duration rate; it never improves your request rate.
- **A function's bill has a shape.** A 40 ms function invoked 100 million times is a
  *request-dominated* bill. A 4-second function invoked 100,000 times is *duration-dominated*.
  Almost every optimization below only moves the duration half.

Work out which half dominates before you touch anything. It decides whether the rest of
this article is worth your afternoon.

**success (take):**
**Take —** the first number I ask for is *what percentage of this function's cost is
duration?* Cost Explorer, grouped by usage type, answers it in about a minute.

If duration is under half the bill, stop. Every lever in this article moves the duration
half, so the best you can achieve is a fraction of a fraction. Go find the function where
duration is 90% — that's where the same afternoon actually pays.

**tip:**
Duration is billed **rounded up to the nearest 1 ms**. That has only been
true since December 2020 — before that Lambda rounded to 100 ms. It is why tuning advice
written before 2021 doesn't transfer: shaving a function from 90 ms to 30 ms used to save
you exactly nothing, because both rounded to 100 ms. Today it saves two-thirds.

## Step 2: The memory myth

Here is the claim you have read a hundred times: *increasing Lambda memory makes functions
cheaper, because they finish faster.*

AWS's own documentation puts it far more carefully — "in many cases, increasing the memory
available causes a decrease in the duration, and as a result, the overall cost increase may
be **negligible or may even decrease**." Note what that sentence actually concedes: the
baseline expectation is a cost *increase* that happens to be small.

And AWS's own worked example, a prime-number function, shows exactly that:

| Memory | Average duration | Cost per 1,000 invocations |
|---|---|---|
| 128 MB | 11.722 s | $0.024628 |
| 1,024 MB | 1.465 s | $0.024638 |

Read that table twice. Eight times the memory made it **8× faster** — and it cost
**one thousandth of a cent more**. Not cheaper. Fractionally more expensive.

_Diagram: Both scales start at zero, which is the whole point — the cost bars really are that close. **8× the memory, 8× faster, and a bill that does not move.** Figures are AWS's own worked example._

Run it back through the rectangle and you can see why. At 128 MB the area is
0.125 GB × 11.722 s = **1.465 GB-seconds**. At 1,024 MB it is 1 GB × 1.465 s =
**1.465 GB-seconds**. Identical. The speedup scaled exactly with the memory, so the
rectangle kept the same area — and the bill did not move.

That is the honest framing, and it is far more useful than the myth:

> **Memory buys you speed at roughly neutral cost. It is a latency dial that happens not to
> hurt your bill much — not a savings lever.**

The mechanism is simple arithmetic. You pay memory × time. Price scales linearly with
memory, so doubling memory only reduces cost if duration **more than halves**. That happens
for CPU-starved work at low memory settings. It does not happen for I/O-bound work, where
your function is waiting on a database or an API and extra CPU shortens nothing.

So: if your function spends its life waiting on network calls, raising memory raises your
bill and changes nothing else.

**success (take):**
**Take —** I don't let "increase the memory" be proposed as a *cost* fix in review
any more. It is a **latency** fix, and it should be justified as one: "this is CPU-bound,
here's the p95 we need, here's the sweep that shows 1,024 MB gets us there."

The reason this matters beyond pedantry: framed as a savings lever, people apply it to the
I/O-bound functions too — where it is pure loss. Framed as a latency dial, it lands only
where it works.

## Step 3: The 1,769 MB line

Lambda allocates CPU in proportion to memory — there is no separate CPU setting. The number
worth memorising is this one, and it is documented exactly:

> **At 1,769 MB, a function has the equivalent of one vCPU.**

Below 1,769 MB you are running on a *fraction* of a core, and CPU-bound work genuinely
speeds up as you add memory. Above it you are buying additional vCPUs — approximately 6 at
the 10,240 MB maximum. "Approximately" is AWS's own word, and the arithmetic explains it:
10,240 ÷ 1,769 is 5.79, not a round 6.

Which leads to the trap AWS states plainly: *"if your code runs sequentially, it will only
use one vCPU regardless of how many are available. The remaining vCPUs sit idle while you're
still paying for the full memory allocation."*

**If your handler is single-threaded — and most Node.js and Python handlers are — memory
above roughly 1,769 MB is close to pure waste.** You pay linearly for cores you cannot use.

**success (take):**
**Take —** a plain Node.js or Python handler sitting above 1,769 MB is a bug until
someone proves otherwise. Not a style preference — a defect, with a dollar figure attached.

It is worth auditing for directly, because it is almost always accidental: somebody hit a
timeout once, dragged the slider to 3,008 MB, the symptom went away, and nobody ever walked
it back. That function has been paying for idle cores ever since.

**warning:**
You will see "3,538 MB = 2 vCPUs" repeated in blog posts. AWS does not publish that number;
it is just 1,769 × 2. The documented facts are the 1,769 MB one-vCPU point and the 6 vCPUs
available at 10,240 MB. Treat anything in between as an inference, not a spec.

## Step 4: Measure, don't guess — Power Tuning

The cost/speed curve differs per function, so the only defensible way to pick a memory
setting is to measure. **AWS Lambda Power Tuning** does exactly that: it deploys a Step
Functions state machine that runs your function concurrently at several memory settings and
charts the result.

It is open source, written by AWS developer advocate Alex Casalboni, documented by AWS, and
distributed through the Serverless Application Repository:

```text
arn:aws:serverlessrepo:us-east-1:451282441545:applications/aws-lambda-power-tuning
```

Deploy it from SAR — but note that SAR materializes a **pinned** CloudFormation stack at
whichever semantic version you deploy, and stacks do not update themselves. If you keep it
around between audits, redeploy rather than assuming you are on the latest. Then execute
the state machine
against a function ARN with a list of memory values to try — typically something like
`128, 256, 512, 1024, 1536, 3008`.

It runs your **real function in your own account**, making real HTTP and SDK calls, so the
measurement reflects production behaviour rather than a synthetic benchmark. You get back a
recommended memory setting plus a graph of the cost/speed trade-off, and it is scriptable
enough to run from CI on every deploy.

The output is the point. AWS's own example shows a function whose **cheapest** setting is
1,024–1,536 MB while its **fastest** is 3,008 MB. For strongly CPU-bound work the cheapest
and fastest settings often land on the same value, and Power Tuning will emit a single
"balanced" recommendation when they do. The point is not that an optimum can never exist —
it is that the curve belongs to your function, and you have to measure it to find out
whether you are choosing or compromising.

**success (take):**
**Take —** the anti-pattern I see most is a team picking one memory value and
standardising it across every function in the account, usually 1,024 MB, usually because it
felt safe.

There is no fleet-wide right answer. The curve is a property of the individual function's
workload, and an image resizer and a webhook receiver do not share one. Standardise the
*process* — Power Tuning on significant functions, re-run when the workload changes — never
the number.

**tip:**
Don't want to deploy anything? **AWS Compute Optimizer** produces Lambda memory
recommendations natively. Less precise than a real measurement sweep, but it is free,
managed, and a reasonable first pass across a whole account.

## Step 5: Switch to arm64 — and know what the 20% covers

This one is real, and unusually exact. Computed from current us-east-1 rates, arm64 duration
costs **precisely 0.8× the x86 rate at every pricing tier**:

| Aspect | x86 (tier 1) | arm64 (tier 1) |
|---|---|---|
| Duration, per GB-second | $0.0000166667 | $0.0000133334 |
| Requests, per million | $0.20 | **$0.20** |

Look at the second row. **Requests get no Graviton discount at all.** So the headline "20%
cheaper" applies to the duration component only — and to provisioned concurrency — never to
your total bill.

Which means the saving you actually see depends entirely on the shape from Step 1. A
duration-dominated bill approaches the full 20%. A request-dominated one — short functions,
huge invocation counts — saves a small fraction of that. Same architecture switch, wildly
different outcome.

There is also a detail almost nobody knows: **the volume tiers are not at the same place on
both architectures.** x86 changes tier at 6 billion GB-seconds a month; arm64 at 7.5 billion.
That looks arbitrary until you multiply it out — 6B × $0.0000166667 and 7.5B × $0.0000133334
are both almost exactly **$100,000**. In us-east-1 the tiers line up on *dollars spent*
rather than volume — though treat that as an observation about this Region, not a stated
AWS design rule: it does not hold everywhere. In ap-east-1 the two first breaks fall about
$270 apart, and in eu-central-2 the arm64 break is actually the cheaper of the two.

**Migrating** is a one-line change in the console, CLI, or IaC — but only if your code is
portable. Anything with native binaries, container images, or compiled layers has to be
rebuilt for `arm64`. Pure Python and Node.js usually move without incident; anything with a
compiled dependency needs a real test pass.

**Decision:**
Switching a function's architecture from `x86_64` to `arm64` drops the **duration** rate to
exactly **0.8×**. Requests stay at **$0.20 per million on both architectures** — that part
of your bill does not move.

Native binaries, container images and compiled layers must be rebuilt before this takes
effect.

**Do you accept?**

**success (take):**
**Take —** arm64 is my default for *new* functions, and the 20% is not the reason.
The reason is that for pure-runtime code there is no trade-off to weigh: same behaviour,
same limits, lower rate. Defaults should be set where the decision is free.

Migrating *existing* functions is a different call. Anything with a compiled dependency
needs a genuine test pass, and if that function is request-dominated you may spend a
sprint's attention to save single-digit dollars. Set the default going forward; migrate the
back catalogue only where duration dominates.

## Step 6: Reserved vs provisioned concurrency

These two get confused constantly, and confusing them costs money or wastes effort in
opposite directions.

| Aspect | Reserved concurrency | Provisioned concurrency |
|---|---|---|
| What it does | Reserves a slice of account concurrency **and caps** the function at it | Keeps N environments **pre-initialized** |
| Cold starts | **No effect** | This is the lever |
| Cost | **Free** | Billed per GB-second, whether invoked or not |
| Use it for | Guaranteeing capacity; protecting a downstream database from too many connections | Latency-critical request paths |

The single most common mistake: **enabling reserved concurrency hoping to reduce cold
starts.** It does nothing for them. None of those environments are pre-initialized;
invocations still pay Init. It is a quota control, not a warmth control.

Provisioned concurrency is the real cold-start fix, and it bills continuously. At current
us-east-1 rates, holding **1 GB of provisioned concurrency for a 30-day month costs about
$10.80 on x86, or $8.64 on arm64** — before a single invocation. Multiply by your concurrency
target before switching it on.

One genuine consolation: invocations that run *inside* provisioned capacity are billed at a
discounted duration rate — $0.0000097222 per GB-second on x86, roughly **42% below** the
standard on-demand rate. Provisioned concurrency is expensive to hold and cheap to use, so
it pays off best when utilisation is high and steady, not spiky.

**success (take):**
**Take —** reserved concurrency's best use isn't guaranteeing capacity, it's
**protecting whatever sits behind the function.** A Lambda in front of an RDS instance will
happily scale to a thousand concurrent executions and open a thousand connections, and the
database falls over long before Lambda does. Reserved concurrency is the cheapest
back-pressure you will ever configure — and it costs nothing.

For provisioned concurrency, do the multiplication *before* the meeting, not after. Hold 10
concurrent 1 GB environments around the clock and you're at roughly $108/month on x86
before a single invocation. That number changes the conversation.

**tip:**
Reserved concurrency has one more trick: set it to **0** and the function stops processing
events entirely. It is the cleanest kill switch for a runaway function, and provisioned
concurrency has no equivalent.

## Step 7: Cold starts in 2026 — SnapStart has moved on

If your cold starts are the problem rather than your steady-state cost, the landscape has
changed and a lot of writing about it is stale.

**SnapStart is no longer Java-only.** It now supports **Java 11+, Python 3.12+, and .NET 8+**
— Python and .NET reached GA in November 2024. It works by running Init once when you publish
a version, snapshotting the initialized microVM, and restoring from that snapshot instead of
re-running Init.

Two things to know before reaching for it:

- **It is free on Java. It is not free on Python and .NET,** which add a snapshot cache
  charge per published version (minimum three hours) plus a restore charge per restore.
- **It cannot be combined with provisioned concurrency** on the same version, and does not
  support EFS or ephemeral storage above 512 MB.

And it changes assumptions your code may be making. Anything unique generated during Init —
IDs, secrets, seeded randomness — is now duplicated across every environment restored from
that snapshot, so it has to move into the handler or an after-restore hook. Lambda does
reseed `/dev/random` and `/dev/urandom` on restore, and the AWS SDKs already re-establish
connections; custom connection code does not.

**Node.js and Ruby still have no SnapStart.** For those, the levers remain package size and
memory. AWS is concrete here: importing only the DynamoDB client instead of the whole AWS SDK
measured **125 ms faster**. And a caveat worth knowing for Go and Rust — putting dependencies
in a layer can *increase* cold start time, because the function has to load additional
assemblies during Init.

Worth keeping in proportion, though: AWS states cold starts typically affect **under 1% of
invocations** in production, ranging from under 100 ms to over a second. Before you spend
money on provisioned concurrency, confirm cold starts are actually hurting your p99 rather
than just being visible in logs.

## What changed in March 2026

One update invalidates part of the model above. Lambda now supports up to **32 GB of memory
and 16 vCPUs** on **Lambda Managed Instances**, and — the real change — lets you configure
the memory-to-vCPU ratio explicitly at **2:1, 4:1, or 8:1**.

At 32 GB that means 16 vCPUs (2:1), 8 vCPUs (4:1), or 4 vCPUs (8:1), chosen according to
whether the workload is CPU-bound or memory-bound. For the first time you can **buy CPU
without buying proportional memory** — which is precisely the trap described in Step 3.

AWS frames the previous state as the limitation it was: functions were "limited to 10 GB of
memory and approximately 6 vCPUs, with no option to customize the memory-to-vCPU ratio."

The distinction matters, so be precise about it: **this applies to Lambda Managed Instances,
not standard on-demand functions.** Standard Lambda is still 128 MB–10,240 MB with CPU
proportional to memory, and everything above still holds. It is available in all Regions
where Managed Instances is GA, and configurable from the console, CLI, CloudFormation, CDK
or SAM.

If you have a CPU-bound function that has been buying memory purely to get cores, that
workaround finally has a real alternative.

**success (take):**
**Take —** resist the urge to move everything. Standard on-demand Lambda is still
the right answer for the overwhelming majority of functions, and the March 2026 change does
not alter a single number in Steps 1 through 6 for them.

The specific workload worth moving is the one you can name: CPU-bound, currently parked at
some large memory setting purely to get cores, with most of that memory idle. That function
has been paying a real premium and now has an exit. Everything else should stay where it is
until it has a reason.

## The checklist

1. **Determine the shape of the bill** — duration-dominated or request-dominated. Everything
   else follows from this.
2. **Don't raise memory hoping to save money.** You are billed for memory *allocated*, not
   used. Raise it to buy speed at roughly neutral cost, and only if the work is CPU-bound.
3. **Measure with Power Tuning** rather than copying a number from a blog. Cheapest and
   fastest are different settings.
4. **Watch the 1,769 MB line.** Past it, single-threaded code pays for idle cores.
5. **Move to arm64** — exactly 20% off duration, nothing off requests, and rebuild anything
   with native dependencies.
6. **Don't use reserved concurrency for cold starts.** Use provisioned, and price it first.
7. **Check SnapStart** if you're on Java, Python 3.12+ or .NET 8+ — free on Java, not on the
   others.
8. **Trim the deployment package.** Import the client you need, not the whole SDK.

The theme running through all of it: Lambda's cost model is simple enough to reason about
from first principles, and almost all the popular advice fails because it skips that step.
Work out what you're billed for, measure your own function, and ignore the folklore.

If you want the settings themselves explained rather than the economics, see
[AWS Lambda Configuration Explained](https://karandeepsingh.ca/posts/lambda-configuration-explained/); for what breaks
at scale, [Handling a Million Users an Hour with AWS Lambda](https://karandeepsingh.ca/posts/lambda-million-users-per-hour/).

## References and Further Reading

- Amazon Web Services. [Configuring Lambda function memory](https://docs.aws.amazon.com/lambda/latest/dg/configuration-memory.html).
- Amazon Web Services. [AWS Lambda pricing](https://aws.amazon.com/lambda/pricing/).
- Amazon Web Services. [Profiling functions with AWS Lambda Power Tuning](https://docs.aws.amazon.com/lambda/latest/dg/profile-functions.html).
- Amazon Web Services. [Lambda function scaling and concurrency](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html).
- Amazon Web Services. [Improving startup performance with Lambda SnapStart](https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html).
- Amazon Web Services. [New for AWS Lambda: 1ms billing granularity](https://aws.amazon.com/blogs/aws/new-for-aws-lambda-1ms-billing-granularity-adds-cost-savings/).
- Amazon Web Services. [AWS Lambda Managed Instances now supports up to 32 GB of memory and 16 vCPUs](https://aws.amazon.com/about-aws/whats-new/2026/03/lambda-32-gb-memory-16-vcpus/).

**Question:** Have you actually measured your Lambda memory setting with Power Tuning, or is it still whatever the template defaulted to?

## Quiz

1. What exactly is a "GB-second" on your AWS Lambda bill?
   a) One gigabyte of data transferred in one second
   b) One gigabyte of memory allocated for one second — allocated, not used
   c) One gigabyte of memory your code actually consumed for one second
   d) One gigabyte of storage held for one second
   Answer: b. A GB-second is memory × time. Crucially it is the memory you *allocated* with the slider, not the memory your code touched. A 1,024 MB function that only ever uses 90 MB is still billed for the full gigabyte, which is why the memory setting is the single most expensive number in your function config.

2. At what memory setting does an AWS Lambda function get the equivalent of one full vCPU?
   a) 1,024 MB
   b) 1,536 MB
   c) 1,769 MB
   d) 3,008 MB
   Answer: c. AWS documents that "at 1,769 MB, a function has the equivalent of one vCPU". Below that you have a fraction of a core, so CPU-bound work speeds up as you add memory. Above it, single-threaded code gains nothing from more memory.

3. Lambda on arm64 (Graviton) is often described as "20% cheaper". What does that discount actually apply to?
   a) The entire Lambda bill
   b) Only the GB-second duration charge — requests cost the same $0.20 per million on both architectures
   c) Only requests
   d) Only functions above 1 GB of memory
   Answer: b. The arm64 rate is exactly 0.8x the x86 rate for duration (and for provisioned concurrency), but request charges are identical on both. A request-heavy workload with very short durations therefore saves far less than 20% overall.

4. You enable reserved concurrency on a latency-sensitive function hoping to reduce cold starts. What happens?
   a) Cold starts drop, because environments are kept warm
   b) Nothing improves — reserved concurrency does not pre-initialize environments, so invocations still pay Init
   c) Cold starts drop only on arm64
   d) The function is billed for idle capacity
   Answer: b. Reserved concurrency reserves a slice of your account's concurrency and caps the function, at no extra charge, but none of those environments are pre-initialized. Provisioned concurrency is the cold-start lever, and it does cost money.


