Skip to content

9 min read

Unit cost is per accepted image, not per API call

On this page

Marketing wants a "generate a hero image" button, product wants it unlimited on the $29 plan, and the number on the whiteboard is $0.04 per image. Someone has already done the arithmetic that makes the meeting easy: prototype analytics say 6.2 generations per accepted result, six point two times four cents is twenty-five cents, and twenty-five cents against twenty-nine dollars is nothing. The feature ships unlimited.

That arithmetic is correct as far as it goes, and it is the reason the feature will be repriced within two quarters. Both halves of it — the multiplier and the mean — are doing more work than they can carry.

The multiplier is the number, and it is also a defect report

The first correction is the easy one and it is already in the whiteboard maths: $0.04 is the price of an API call, and users do not buy API calls. They accept an image, and behind each accepted image sit 6.2 calls, so the unit that appears in your cost model is $0.248 per delivered image. Every downstream figure — breakeven, gross margin, the price of a plan — has to be built on that number rather than on the one in the provider's pricing table.

What the whiteboard misses is that 6.2 is not a constant of nature. It is a measurement of how often the product gives someone what they asked for on the first try, and it moves. A user who accepts the first generation contributes 1.0. A user who cannot get the composition they want and gives up after eleven attempts contributes 11. The distribution behind a mean of 6.2 is usually bimodal, meaning it has two clumps rather than a single hump around the average: a large group at 1 or 2, and a tail of sessions in the teens where someone is fighting the model. Nobody generates 6.2 images; the mean is a point between two populations, and neither of them lives at it. Halving the multiplier to 3.1 halves the cost of the feature and, more valuably, means the tail sessions stopped happening, which is the same thing as the feature working.

This makes the multiplier the single most useful metric to instrument, and it has to be instrumented as a ratio over sessions rather than as a count of API calls. The denominator is an explicit acceptance event — the user inserted the image, downloaded it, or published it — and if your UI has no such event, you cannot compute the number that your cost model depends on.

The price sheet is not one price

The second correction is that $0.04 is one cell in a grid. Resolution and quality tier are independent knobs on current image APIs, and they move the price by more than most teams expect. Resolution is the pixel dimensions of the output and is self-explanatory; quality tier is the provider's name for how much computation it spends producing the picture, which in a diffusion model is mostly the number of denoising steps it runs and how large a model path it routes the request through. The tiers are usually named rather than numbered — low, medium, high, or draft and final — and the name is the only thing you can control, since the step counts behind it are not published and change without notice. A representative published price sheet for a current model looks like this, and the shape is what matters rather than the exact cents:

OutputLowMediumHigh
1024 × 1024 (square)$0.011$0.042$0.167
1024 × 1536 (portrait)$0.016$0.063$0.250
1536 × 1024 (landscape)$0.016$0.063$0.250

The span from the cheapest cell to the most expensive is a factor of 23. Within a single resolution, low to high is a factor of 15. The $0.04 on the whiteboard is the square medium cell, and a hero image on a marketing site is almost certainly the landscape high cell at $0.25 — which, multiplied by 6.2, is $1.55 per delivered image rather than $0.25. The prototype was priced in a different currency from the feature that shipped.

Latency moves with the same knob and in the same direction, which is why the two have to be measured together. On the same model family, a low-tier square generation lands near a 4-second p50, medium near 9, and high-tier landscape near 19 with a p95 past 45. A team optimising purely on cost will move traffic to the low tier and discover that the acceptance rate falls, pushing the multiplier up, which can leave total cost per accepted image flat or worse while the output is visibly poorer.

Measure the grid, not one happy request

The only way to know where your feature sits is to run representative prompts across the settings you might ship and record cost, latency and acceptance together. This is a half-day of work and it replaces a year of arguing:

python
SETTINGS = [
    ("1024x1024", "low"),    ("1024x1024", "medium"), ("1024x1024", "high"),
    ("1536x1024", "medium"), ("1536x1024", "high"),
]

def sweep(prompts: list[str], n_per_cell: int = 8) -> list[dict]:
    rows = []
    for size, quality in SETTINGS:
        for prompt in prompts:
            for _ in range(n_per_cell):
                t0 = time.perf_counter()
                try:
                    r = image_client.generate(prompt=prompt, size=size,
                                              quality=quality)
                    outcome = "ok"
                except Exception as exc:
                    r, outcome = None, classify(exc)   # chapter 3's classifier
                rows.append({
                    "size": size, "quality": quality, "prompt": prompt,
                    "latency_s": time.perf_counter() - t0,
                    "outcome": outcome,
                    "usd": PRICE[(size, quality)],
                    "asset": r and store_for_review(r),
                })
    return rows

Ten representative prompts across five cells with eight samples each is 400 generations. At an average of $0.09 that is $36 and about ninety minutes of wall-clock time with modest concurrency — a rounding error against the decision it informs. The asset column exists so that a human can then rate the outputs blind — shown the images in a shuffled order with no label saying which resolution or tier produced each one, so the rating is of the picture rather than of the price — which is where the acceptance rate per cell comes from; without that column you have a cost-and-latency table and no way to know which cell is actually cheapest per delivered image.

Run the sweep at your expected peak concurrency rather than serially, because the latency figures change under load and the change is not yours to control. Image providers queue, and the queue is shared: a sweep run at three in the morning with one request in flight measures the model, while a sweep run at 20 requests in flight during European working hours measures the model plus whatever else is happening on that provider. On the same account and the same settings, a p95 that reads 22 seconds serially can read 60 seconds at 20-way concurrency, and that second number is the one your worker timeout and your queue depth have to be sized against.

The output of the sweep is a per-cell effective cost:

CellPrice/callp50 latencyAccept rate (blind review)Effective cost per accepted image
1024², low$0.0114 s9%$0.122
1024², medium$0.0429 s22%$0.191
1536×1024, medium$0.06312 s31%$0.203
1536×1024, high$0.25019 s58%$0.431

The low tier is cheapest per accepted image on these figures and delivers a 9% acceptance rate, which means an eleven-attempt session for the median user and a feature nobody enjoys using. The medium landscape cell costs 66% more per call than square medium and is nearly identical per accepted image because the extra resolution is what a hero image needed. This is the table that decides your default setting, and it cannot be derived from the price sheet alone.

Why the mean does not settle the pricing question

Take the medium landscape cell at $0.203 per accepted image and the arithmetic against a $29 plan is still comfortable at the mean. Breakeven, ignoring every other cost, is 143 accepted images per user per month; at the whiteboard's $0.25 it is 116. If the average user accepts 9 images a month, the feature costs $1.83 per user against $29 of revenue and the meeting was right.

The mean is not what unlimited is priced against. Consumption of a generative feature is heavily skewed, and a realistic shape on 10,000 seats is a median of 4 accepted images a month, a p90 of 28, and a p99 of 210. The p99 cohort is 100 accounts consuming 21,000 accepted images a month, or $4,263, against $2,900 of revenue from those accounts — a negative-margin cohort inside an overall-profitable feature. That cohort is also the one most likely to be an agency running client work through a single seat, which means it grows rather than regresses to the mean, and it is the cohort that answers support tickets loudest when you later impose a cap.

Two further sensitivities push in the same direction. The multiplier is not stable across cohorts: users with a strict brand bar generate more attempts per accepted image, so the heaviest users often carry a higher multiplier as well as a higher acceptance count, and the two multiply. And the multiplier is not stable across time, because a provider's default model version changes and can move acceptance in either direction. A shift from 6.2 to 9 raises the p99 cohort's cost by 45% with no change in user behaviour and no announcement you controlled.

ScenarioMultiplierCost per acceptedp99 user's monthly cost
Prototype assumption6.2$0.25$52.50
Measured, medium landscape3.2$0.20$42.60
After a model version change9.0$0.57$119.70
Agency cohort, high tier7.5$1.88$394.80

The position to take in the room

Refusing the feature is not the position, and neither is quoting a mean back at the person who already quoted one. What the numbers support is a structure that keeps the promise finite while leaving the ordinary user unaware that a limit exists.

An included allowance sized well above the p90 does that. At a p90 of 28 accepted images, an allowance of 50 covers over 90% of users completely, costs at most $10.15 per month per user in the worst case for those it covers, and turns the p99 into a decision rather than an invoice. Beyond the allowance there are three honest options, and it is worth naming which one you are choosing: metered overage priced at a visible multiple of cost, degraded service where further generations drop to a cheaper tier or a slower queue, or a hard stop with an upgrade path. Metered is the cleanest to reason about and the hardest to sell; degraded preserves the "unlimited" word at the cost of a quality cliff that users notice; a hard stop is the easiest to build and the most likely to generate churn from your most engaged accounts.

Whichever is chosen, a per-account rate limit belongs underneath it regardless of pricing, because a stuck retry loop in someone's browser tab is not a pricing scenario. The limit exists to bound damage from a bug, and it should be high enough that no legitimate user meets it.

The honest limitation in all of this is that the numbers move under you. Image model prices have fallen substantially year over year while the tiers people actually ship have moved upward in quality, and those two effects partially cancel, so a cost model built on today's price sheet with no allowance for a version change is a model with one input pinned to someone else's roadmap. What is not in the model at all — storage and CDN egress, the per-gigabyte charge for bytes leaving your bucket or your cache toward a browser — is genuinely small at these volumes: a 1.5 MB PNG costs about $0.000035 a month to store, four orders of magnitude below the generation that produced it.

The largest lever in the whole calculation is the multiplier, and it is not a pricing lever. Moving 6.2 to 3.2 was worth more than every tier decision in the sweep, and it comes from the output matching what the person asking actually wanted — which, for anything with a brand attached to it, is not achieved by writing a better sentence.

Image generation and editing APIs — read the current version at https://daviddominguez.dev/learn/en/image-generation-and-editing-apis/unit-cost-is-per-accepted-image-not-per-api-call

Back to top