A photograph of an empty underground car park lit in deep teal, its bare concrete floor and yellow-banded pillars receding towards a dark opening at the centre, darkened behind the title “Nobody chose 4 GB. It was the default.”, which is set across it in white, with the label COST ENGINEERING above and the byline Alan Salomon below.

Cover photograph by Petar Avramoski on Pexels.

← Insights & Articles
Cost Engineering

Nobody chose 4 GB. It was the default.

Three memory numbers, all wrong, in three different directions.

Aug 26, 2026 · 9 min read

Here are two numbers from the same job.

peak memory used        66 MB
memory allocated     4,096 MB

The job validates email addresses. It is almost entirely DNS-bound: it waits on network answers, holds a few hundred small records while it does, writes them out and exits. Sixty-six megabytes is a generous reading of what it needs. It was allocated four gigabytes, which is 62 times the workload, and it was billed for four gigabytes, because compute is billed per gigabyte-hour and the meter does not care whether you touched the memory.

The interesting part is not the waste. It is that nobody chose the number. Nobody sat down, considered a DNS-bound workload, and concluded it wanted four gigabytes. The field was absent, and this is what an absent field meant.

It was found while checking something else. The run was inspected to verify that charging worked correctly before the listing went public, and the memory figure was sitting in the run options next to the thing being checked. Nothing in the code hinted at it, nothing in the listing hinted at it, and no amount of reasoning about the program would have produced it. Only looking at a real run did.


The default was the ceiling

On this platform a run with no memory setting does not get a small sensible allocation. It gets the maximum the account permits. The manifest field is literally named maxMemoryMbytes, and leaving it out is not a request for a default: it is a declaration that the run may take everything.

That is worth pausing on, because it is the opposite of the shape most people carry in their heads. The mental model built by a decade of serverless is that a default allocation is a floor you tune upwards when something runs slowly. AWS Lambda starts you at 128 MB and makes you ask for more. You learn to treat the default as stingy, and you learn that the failure mode of ignoring it is a slow function, not a large invoice.

Invert that and the failure mode inverts with it. A default that is a ceiling fails silently and in the direction of money. Nothing runs slowly. Nothing errors. No log line says the run reserved sixty times what it touched. The only place it shows up is the bill, and on a small account the bill is small enough that it does not demand an explanation.

The first rule that came out of this is not about memory at all:

Find out whether your platform's default is a floor or a ceiling. It is one or the other, and the two fail in opposite directions.

Capping the run at a measured value cut what it costs to run by 73%, on identical input. That is two lines in a manifest, and it is the largest single cost change made to any of these programs. Compute is the overwhelming majority of what a run of this kind costs to operate, so memory is not one lever among several. It is close to the only one.

The second bill, and it was the customer's

The compute multiple was the part I expected once I saw the allocation. The other consequence I did not, and it is the worse of the two, because it was not my money.

The platform's start event charges one event per gigabyte of allocated memory, with a minimum of one. That is a defensible design. A run holding sixteen gigabytes genuinely does cost more to start than one holding a single gigabyte, and a price that reflects it is fair. But it means an allocation nobody chose was not only multiplying my costs, it was multiplying the customer's.

At four gigabytes, a customer running this tool paid four start events per run instead of one. They received nothing for the other three: no faster run, no larger capacity, no better result. They paid four times over for a number nobody had picked, on a workload that fitted in a rounding error of it.

Nobody complained, because nobody could see it. The pricing page correctly said what a start event costs. The invoice correctly said how many were charged. Working out that the count should have been one requires knowing the per-gigabyte rule, knowing the allocation, and knowing that the allocation was an accident rather than a decision. A customer has access to none of those three.

A default you never chose can arrive on somebody else's invoice. That makes reading your own run options a duty rather than an optimisation.


The laptop was wrong by 2.4x

Having decided to measure rather than guess, the obvious move is to run the thing locally and watch what it takes. That produced the second wrong number.

peak on macOS        620 MB
peak on Linux        258 MB

Same program, same input, same run. The laptop reported roughly 2.4 times what the platform reported. Sizing from the local figure would have shipped a 1 GB cap on a job whose real ceiling was comfortably inside 512 MB, on an Actor whose bill is about 95% compute. Having just cut 73% by measuring, we would have given a chunk of it back by measuring in the wrong place.

I am not going to give you a confident account of why the two disagree. Operating systems account for a process's memory differently, and the words they use for it are not the same words: what one tool calls footprint, another calls resident set, and neither is quite the number a billing system uses. The honest version is that I measured both and they disagreed, and only one of them was the environment that produces the invoice.

Measure on the platform that bills you. Every other measurement is a rehearsal.

This is the part that generalises furthest past scrapers. Anything billed as memory multiplied by time has this property, and so does anything scheduled by a requested reservation. A Kubernetes memory request does not have to be touched to be spent: it is removed from what the scheduler believes it has, whether or not the process ever reaches it. A container sized from a developer's laptop is a container sized from the wrong machine, and the error is invisible until you are counting nodes.


The test run was wrong in the other direction

Two numbers wrong, both too high. The third was too low, which is the one that actually takes a service down.

A scraper that visits websites was measured on the platform, properly, in the environment that bills. It was measured twice:

4 sites                  105 MB
20 sites, full concurrency   192 MB

The small run is the one you naturally reach for. It is quick, it is what you already had open from testing, and it produces a clean number. Size the cap from it with a comfortable margin and you ship 256 MB, which is nearly double what you measured and sounds cautious.

It is not cautious. The input schema permits twenty sites, and twenty sites at full concurrency is 192 MB before any margin at all. The cap would have been fine in every test anyone ran and would have died on input the product openly advertises. That failure arrives as an out-of-memory kill in a customer's run, on the day a customer first uses the tool the way the schema says they may.

Measure at the top of what your input schema allows, not at the top of what you happened to test.

The schema is a promise. If it accepts twenty, twenty is a supported workload, and the memory cap has to be sized against the promise rather than against the habit. The final cap on that program is 512 MB, which is now a measurement rather than a round number that felt safe.


Three numbers, three directions

Set them side by side, because the pattern is the point:

Where the number came fromDirection of the errorWhat it costs you
The platform default62x too highYour compute bill, and your customer's invoice
The development laptop~2.4x too highMoney, quietly, after you thought you had fixed it
The convenient test run~2x too lowAn out-of-memory kill in front of a customer

Every one of the three was easy to trust. The default came from the platform, which presumably knows what it is doing. The laptop measurement was a real measurement of a real run, not an estimate. The small test run was also a real measurement, and it was of the exact program that shipped. None of them was a guess, and all three were wrong.

What they have in common is that each was taken in whatever environment was most convenient at the time. Convenience is the variable. It is not that measurement is unreliable, it is that a measurement carries the conditions it was taken under, and those conditions get dropped the moment the number is written down.

So the working version of all three rules is one rule:

A resource number is only meaningful with its conditions attached. On this machine, at this input size, under this scheduler. Drop the conditions and you have a number that will be wrong somewhere you are not looking.


What this does not tell you

The 4 GB figure belongs to one program at one moment, before any of these caps existed. It is not a standing description of the platform, and it is not what any of these programs does today: all five now carry an explicit cap in their manifest, either 512 MB or 1024 MB, so there is no longer a run anywhere in the set that takes the ceiling by default. If you go and inspect them expecting to find 4,096, you will not.

The 73% is our bill, not a general rate. It is what happens when a 4 GB allocation meets a workload that needed 66 MB, and your ratio depends entirely on your own two numbers. A job that genuinely needs 3 GB saves nothing by capping, and the exercise is a waste of an afternoon.

The 2.4x is one comparison on one pair of machines. It establishes that the two environments disagree and that the local one was not conservative, which is the actionable part. It does not establish a conversion factor, and anybody who uses it as one has made exactly the mistake the article is about.

None of these numbers is large in absolute terms. This is a small operation and the amounts involved are tiny, so 73% of very little is still very little. I am publishing them anyway, because the ratios do not care about the scale, and the same three mistakes on a fleet of containers that bills monthly is a real number with a real signature on it. The mechanism is the transferable part. The amount is an accident of how small this operation currently is.


The checklist that replaced the guessing

Four lines, in the order they have to happen:

  1. Establish whether the platform default is a floor or a ceiling. Read the field name; here it was the answer.
  2. Measure on the platform that produces the invoice, never on the machine you develop on.
  3. Measure at the largest input the schema permits, at whatever concurrency the program actually uses.
  4. Set the cap explicitly from that measurement, and write the measurement down next to it so the next person knows what it was sized against.

The fourth line is the one that decays first and it is the one that matters in a year. A cap of 512 with no note beside it is indistinguishable from a cap of 512 someone picked because it is a power of two, and the next person to touch it will treat it as arbitrary, because they have no way to tell that it is not.


The programs these measurements came from are documented at Website Email & Phone Finder, YouTube Channel Analyzer and Company Enrichment, each with what it does not do written on the same page. The measurements above are the part worth taking somewhere else.