ealeng

Episode 31 of 56 · Serving models

Self-hosting vs. an API: what one email really costs

Lab 11's fine-tuned 0.6B model, shrunk to 4 bits and served from a laptop, against gpt-5.4-nano and gpt-5.4-mini through OpenAI's API, on the same 100 emails. It tied mini on right answers (99 vs 97) and was 9× faster. Whether it's cheaper depends on how busy you keep the machine, on the API's cache discount, and on who runs it.

Lab 12 · Inference & Serving10 min read
Video transcript7:31

0:00Cold open

0:01

Every support email at our shop gets sorted by an AI model. Today, we pay OpenAI for that, per email.

0:09

In the fine-tuning series, I trained a small model to do the same job. Now it runs on my laptop.

0:17

Same hundred made-up test emails. OpenAI’s model, G P T five point four mini: ninety-seven right. About a second each. A dollar forty-six per thousand emails, at list price, before any discount.

0:32

Mine: ninety-nine right. A tenth of a second each. And nothing to pay per email.

0:39

So running it myself is free? Not quite. The laptop costs money every hour, busy or not.

0:47

So what does one email really cost?

0:50Intro

0:52

Hands-on AI engineering: serving models, part three.

0:56

Today: your own model against a paid one. Right answers, speed, and cost.

1:02Where we are

1:04

I’m Lea, an ML engineer at an outdoor gear shop.

1:08

In part one, I shrank my email sorter to four bits: a third of a gigabyte, and still ninety-nine percent right.

1:16

In part two, a server that lets new requests hop on at every step, so many people can share one model.

1:24

Today: should we run it ourselves, or keep paying OpenAI?

1:3001 · Same code

1:31

First, how the app talks to a model. It sends a request through an API: a standard way for one program to ask another for something, and get an answer back.

1:43

To serve my model, I start MLX’s own server on the laptop. It understands the same requests as OpenAI’s API. That’s called an OpenAI-compatible server.

1:56

So the app’s code barely changes. Same library, same call. Only the settings change: the address, the model name, the prompt, and a couple of call options.

2:08

Switching between the paid model and mine is a config change. That makes a fair race easy.

2:1602 · Right answers

2:17

The race: the same hundred made-up test emails, with known right answers, one at a time. For each, the model fills in four fields: what the customer wants, the order number, the product, and how urgent it is.

2:31

Three setups. My model, with a one-line prompt. gpt-5.4-nano, the cheapest of OpenAI’s five point four models, with a page of instructions. And mini, with the instructions plus ten worked examples.

2:48

Right answers, with all four fields correct: mine ninety-nine, mini ninety-seven.

2:54

Is two emails a real difference? The two disagreed on just four emails: three went my way, one went mini’s. That’s like three heads in four coin flips. Not a real difference, so it’s a tie.

3:08

Nano got seventy-five: clearly behind. And remember, mine trained on emails just like these.

3:1703 · Speed

3:18

Speed. The typical email: mine took a tenth of a second. Nano, eight tenths. Mini, one second.

3:26

Now the slow end: one email in twenty took longer than this. Mine, still about a tenth of a second. Mini, two seconds.

3:36

Why so much faster? Mine has no trip over the internet, a prompt fifteen times shorter than mini’s, and it’s almost certainly a much smaller model. I didn’t measure which of these matters most.

3:4904 · Many at once

3:51

So far, one email at a time. Now: how many emails a second can the laptop handle? With one simulated user sending, eight point six.

4:01

I simulated thirty-two users sending at once: twenty-four a second. The server works on many emails in one pass, like the hop-on bus in part two, where new requests join mid-way.

4:15

Each email waits longer. A typical one now takes about as long as mini’s.

4:21

Even so, its slow end, one and a half seconds, is still quicker than mini’s slow end with a single user.

4:29

At thirty-two at once, the most I tried, that’s eighty-seven thousand emails an hour, from one laptop.

4:3805 · The real cost

4:39

Now the money. OpenAI charges per email. My own machine costs the same every hour, busy or not.

4:47

Like a gym membership: the more you go, the less each visit costs.

4:53

So the cost of one email is the machine’s hourly cost, divided by the emails it handles in that hour.

5:01

Prices vary, so these hourly prices are what-ifs. Say two dollars an hour, kept busy all the time. Spread over eighty-seven thousand emails, that’s about two cents per thousand.

5:14

Handling only five percent of what it could: forty-six cents per thousand. The same machine, twenty times more per email, because it sits idle most of the hour.

5:26

Now say the machine costs five dollars an hour, and it’s busy five percent of the time. Is that still cheaper than mini’s dollar forty-six?

5:37

At list price, yes, just: a dollar fifteen.

5:42

But OpenAI charges less for the start of a prompt it has seen before. Mini’s long instructions are the same every time. My run didn’t record that discount.

5:54

From OpenAI’s prices, it could bring mini down to roughly forty to eighty cents per thousand, maybe less. Then the five-dollar machine, mostly idle, loses.

6:0706 · Left out

6:08

And there’s a cost the table doesn’t show: people. Someone has to run the server, watch it, and fix it when it breaks.

6:16

When the shop changes, with new products or new rules, the paid model just reads a new prompt. My model needs new training.

6:25

And volume. Say we get five thousand emails a day. That’s about three and a half minutes of work for this server. At two dollars an hour, kept on all day, that’s almost ten dollars per thousand emails: far more than mini.

6:4107 · What I do now

6:42

So here’s what I do now. I keep the same client code for both, so switching is always a config change.

6:50

I run the model myself when the job is narrow, the volume keeps a machine busy, or the data can’t leave the company.

6:58

Otherwise, I pay per call. And I compare right answers first. Nano costs eighteen cents per thousand, but gets a quarter wrong. That isn’t cheap.

7:09

That’s the series. Your own model can be faster and cheaper. As long as it stays busy, and someone owns it.Lea: Narrow. Busy. Owned.

7:16Summary

7:18

A paid API charges per email. Your own machine charges per hour.

7:23

So its cost per email depends on how busy you keep it, and on the people who run it.Lea: Thanks!

Key takeaways

  • ✓An OpenAI-compatible server (here `mlx_lm.server`) accepts the same requests as OpenAI's API, so the app keeps its code and changes settings: the address, the model name, the prompt and a couple of call options. Switching between the paid model and your own is a config change, which makes a fair comparison easy.
  • ✓On 100 made-up test emails, the 4-bit fine-tune got 99 fully right, gpt-5.4-mini 97 and gpt-5.4-nano 75. Mine and mini disagreed on only 4 emails, 3 to 1 (p = 0.62): a tie. Nano is clearly behind (p < 0.001). The fine-tune trained on emails like these.
  • ✓One email at a time, the fine-tune's round trip was 0.11 s typical and 0.13 s at the slow end, against mini's 1.02 s and 2.09 s. Its prompt was 115 tokens against mini's 1,766. No internet trip, a 15× shorter prompt and almost certainly a much smaller model all differ; the lab didn't measure which matters most.
  • ✓A self-hosted email costs the machine's hourly price ÷ the emails it handles that hour. The laptop handled 87,263 an hour at 32 simulated users. At a what-if $2/h that's 2.3¢ per 1,000 when always busy and 46¢ at 5%: the same machine, 20× more per email. A $5/h machine at 5% ($1.15) beats mini only at list price ($1.46); with mini's cache discount (estimated 42–77¢, not measured) it loses.
  • ✓The table leaves out people, change and volume: someone runs the server, a new product means retraining, and 5,000 emails a day is 3.4 minutes of this server's work. Kept on all day at $2/h, that's $9.60 per 1,000. Self-host when the job is narrow, the machine stays busy, or the data can't leave; otherwise pay per call, and compare right answers first.

The shop's support emails are sorted by an AI model, and today the shop pays OpenAI for every email. In the fine-tuning chapters, a small model learned to do the same job; in the last two chapters it was shrunk to 4 bits and served to many users at once. Now the question that started it: should the shop run that model itself, or keep paying per call? This chapter compares right answers, speed and cost, and then what the comparison leaves out.

The source is Lab 12 (Inference & Serving), script 12_self_host_vs_api.py, recorded in October 2026 on one laptop (Apple M4 Max). It serves Lab 11's fine-tuned Qwen3-0.6B, merged and quantized to 4 bits (0.34 GB, still 99% right; chapter 29), with mlx_lm.server, and runs it against two OpenAI models on the first 100 of Lab 11's test emails. The emails are made up, with known right answers, and the fine-tune trained on emails just like them. The hourly machine prices are what-ifs, not quotes. The lab's API half (streaming, prompt caching, the Batch API, semantic caching, cascades) isn't in the video; one of its results, the cache discount, matters here and is quoted below.

The same code, a different address

An app talks to a model through an API: a standard way for one program to send another a request and get an answer back. OpenAI's API is the one most client code is written for.

mlx_lm.server is an OpenAI-compatible server: it runs the model on the laptop and accepts the same requests as OpenAI's API. So the app keeps its library and its call. The lab's script starts the server like this:

python -m mlx_lm server --model data/models/ticket-0.6b-4bit --port 8090 \
  --chat-template-args '{"enable_thinking": false}' --max-tokens 60

and talks to both with the same openai client:

api = OpenAI()                                                         # OpenAI's servers
local = OpenAI(base_url="http://localhost:8090/v1", api_key="not-needed")  # this laptop

r = client.chat.completions.create(model=model, messages=msgs, **options)

Only settings change: the address, the model name, the prompt, and a couple of call options (the API calls use reasoning_effort="none" and max_completion_tokens, the local ones max_tokens and temperature=0). Switching between the paid model and your own is a config change, which also makes a fair race easy. A model gateway gives you the same thing across many providers.

Right answers: a tie, and a clear loser

The race: the same 100 emails, one at a time. For each, the model fills in four fields: what the customer wants, the order number, the product, and how urgent it is. An answer counts as right only if all four are.

Setup Prompt All four right
My fine-tune, 4-bit, on the laptop one line 99
gpt-5.4-nano, the cheapest of OpenAI's 5.4 models a page of instructions 75
gpt-5.4-mini the instructions plus 10 worked examples 97

Is 99 against 97 a real difference? A paired test (McNemar's) looks only at the emails where the two disagree. Mine and mini disagreed on just 4: three went my way, one went mini's. That's like three heads in four coin flips (p = 0.62), so it's a tie. Nano's 75 is clearly behind (p < 0.001).

Remember what the tie means: on emails like the ones it practised on, the narrow model matches the bigger one. It can't do anything else (the fine-tuning regressions chapter shows what it lost).

Speed: a tenth of a second

Time is the full round trip as the app sees it: localhost for mine, the internet for the API.

Setup Typical (median) Slow end (p95) Prompt tokens
My fine-tune 0.11 s 0.13 s 115
gpt-5.4-nano 0.80 s 1.81 s 655
gpt-5.4-mini 1.02 s 2.09 s 1,766

The slow end (95th percentile) is the time one email in twenty took longer than. Mini's typical email took 9× as long as mine, and mine barely has a slow end: 0.13 s against 0.11.

Why so much faster? Several things differ at once: no trip over the internet, a prompt 15× shorter than mini's (the fine-tune learned the rules, so it doesn't need them spelled out), and almost certainly a much smaller model (mini's size isn't published). The lab didn't measure which of these matters most.

Switch between the measures:

Try it · The race, three setupsLab 12 · script 12 · 100 made-up emails, one at a time
My fine-tune, 4-bit · this laptop, one-line prompt99 / 100
gpt-5.4-nano · OpenAI API, a page of instructions75 / 100
gpt-5.4-mini · OpenAI API, instructions + 10 examples97 / 100

All four fields correct. Mine and mini disagreed on just 4 emails: 3 went my way, 1 went mini's (p = 0.62). That's a tie. Nano is clearly behind (p < 0.001). Mine trained on emails just like these.

Many at once

So far, one email at a time. How many can the laptop handle? The lab sent 64 emails per setting from 1, 4, 16 and 32 simulated users (threads sending back to back):

Simulated users Emails a second Emails an hour Typical Slow end
1 8.6 30,975 0.12 s 0.13 s
4 17.0 61,286 0.23 s 0.32 s
16 22.8 82,088 0.67 s 0.85 s
32 24.2 87,263 1.14 s 1.52 s

The server works on many emails in one pass, letting new requests join mid-way (continuous batching, chapter 30). Throughput nearly triples, and each email waits longer: at 32 users a typical email takes about as long as mini's. Even so, the slow end, 1.52 s, is still under mini's slow end with a single user (2.09 s). 32 was the most the lab tried, and throughput was still rising.

Try it · Many at onceLab 12 · script 12 · 64 emails per setting, the local server

Simulated users sending at once

Emails a second
24.2
Emails an hour
87,120
Typical
1.14 s
Slow end
1.52 s

Mine, 32 at once

typical 1.14 s · slow end 1.52 s

gpt-5.4-mini, one at a time

typical 1.02 s · slow end 2.09 s

mlx_lm.server works on many emails in one pass, so throughput nearly triples from 1 to 32 users, and each email waits longer. At 32 the typical email takes about as long as mini's, but the slow end (1.52 s) is still under mini's one-user slow end (2.09 s). 32 was the most the lab tried, and throughput was still rising: the 87,263 an hour used below may not be the machine's limit.

What one email really costs

The API charges per email: at list price, $1.46 per 1,000 for mini and $0.18 for nano. Your own machine charges per hour, busy or not. Like a gym membership, the more you use it, the less each visit costs:

cost per 1,000 emails = machine cost per hour ÷ emails handled per hour × 1,000

With the measured 87,263 emails an hour at full use, and what-if hourly prices (a laptop you own costs its power and wear; a rented GPU its rental):

Machine cost per hour Used 100% Used 25% Used 5%
$0.10 $0.001 $0.005 $0.023
$0.50 $0.006 $0.023 $0.115
$2 $0.023 $0.092 $0.458
$5 $0.057 $0.229 $1.146

At $2 an hour, always busy, that's about 2¢ per 1,000. Handling only 5% of what it could, 46¢ per 1,000: the same machine, 20× more per email, because it sits idle most of the hour.

A $5-an-hour machine used 5% of the time comes to $1.15: under mini's $1.46, just. But $1.46 is list price. OpenAI charges a tenth of the input price for the start of a prompt it has seen recently, and mini's ~1,650 tokens of instructions and examples are the same on every call. The lab's own run didn't pass cached tokens to its cost function, and the discount couldn't be measured afterwards. An estimate from OpenAI's prices: if 1,024 to 1,536 of mini's 1,766 prompt tokens were cached on every call, mini would cost about 42–77¢ per 1,000, maybe less if more is cached. Then the mostly idle $5 machine loses, and the $2 machine at 5% (46¢) is inside the range. Nano's 655-token prompt is under the cache's 1,024-token minimum, so its 18¢ stands.

The cache discount is not a guess about whether it exists. In Lab 12's Part 1 (03_prompt_caching.py), from the second call on, 1,792 of about 2,860 input tokens came from the cache and the cost per call halved. It works in steps: the first 1,024 tokens, then 128 at a time. What isn't known is how much of mini's prompt hit the cache in this run.

Pick a machine price and how busy it is:

Try it · What one email costsLab 12 · script 12 · 87,263 emails an hour at full use

The machine costs, per hour (what-if)

How busy it is

5% of capacity ≈ 104,716 emails a day, kept on all day

Mine, per 1,000 emails
46¢
Formula
$2 ÷ 4,363 an hour × 1,000

Cheaper than mini at list price; may lose once the discount applies

0.1¢1¢10¢$1$10
Mine: 46¢ nano, list: 18¢ (75 of 100 right) mini, list: $1.46 mini with its cache discount: ~42¢–77¢ (estimate)

Hourly prices are what-ifs: a laptop you own costs power and wear, a rented GPU its rental. Capacity is the measured 24.2 emails a second at 32 simulated users. The discount range assumes 1,024 to 1,536 of mini's ~1,766 prompt tokens are cached on every call; the run didn't record it. Not in the number: the people who run the server.

Nano's 18¢ beats the $2 and $5 machines used 5% of the time, and it gets a quarter of the emails wrong. Compare right answers first; a cheap wrong answer isn't cheap.

What the table leaves out

  • People. Someone has to run the server, watch it, and fix it when it breaks. That's not in the hourly price.
  • Change. When the shop adds products or changes its rules, the paid model reads a new prompt. The fine-tune needs new training data and a new training run (Lab 11).
  • Volume. Say the shop gets 5,000 emails a day. That's 3.4 minutes of work for this server. Kept on all day at $2 an hour, it costs $9.60 per 1,000 emails, far more than mini. Unless the machine is free anyway, or privacy or speed forces it, paying per call is simpler at that volume, and nano or a cascade (a cheap model first, a bigger one when it's unsure) may be enough.

When to run it yourself

  • Keep the same client code for both, so switching is always a config change, and fallback to the other is easy.
  • Self-host when the job is narrow, the volume keeps a machine busy, or the data can't leave the company. The win here came from three things together: a narrow model small enough to be fast (Lab 11), quantized to fit (chapter 29), and kept busy by batching (chapter 30).
  • Otherwise, pay per call, and price the API with the cache discount you actually get, not list price.

Caveats

  • Made-up emails, and the fine-tune trained on emails like them. The 99% holds for this narrow task; it says nothing about anything else.
  • Hourly prices are what-ifs. Put in your own: hardware spread over its life plus power, or a rented GPU.
  • Mini's cost is at list price. The cache discount wasn't recorded; the 42–77¢ range is an estimate from OpenAI's prices, not a measurement.
  • One laptop, one run, one network. API times include the internet trip from one home connection on one day.
  • Capacity is a measured point, not a ceiling. 87,263 an hour is at 32 simulated users, the most tried, with throughput still rising. The simulated users are threads sending back to back, not real traffic.
  • The speed gap isn't split by cause. Network, prompt length and model size all differ at once.

The checklist

  1. Put both behind the same client: an OpenAI-compatible server or a gateway, so switching is a config change.
  2. Compare right answers first, with a paired test on the same items. A gap of 2 on 100 can be a tie.
  3. Measure latency as the app sees it: typical and slow end, at the concurrency you expect.
  4. Measure throughput under load, then price a self-hosted request as hourly cost ÷ requests per hour × 1,000.
  5. Use how busy the machine will really be, not its peak. At low volume an always-on machine costs more per request than the API.
  6. Price the API with its discounts: cached prompt tokens, batch pricing.
  7. Count the people and the retraining before you move a model in-house.

Practice: price the race

Write the lab's nearest-rank percentile and paired McNemar test, then both sides of the bill: an API call's cost per 1,000 (with and without cached tokens) and a self-hosted email's cost from the hourly price, the throughput and how busy the machine is. The tests use the recorded run: 0.11 s and 0.13 s for the fine-tune, p = 0.625 for the tie with mini, $1.46 at list price and 42–77¢ with the estimated discount, 46¢ at 5% busy, and $9.60 for 5,000 emails a day.

Try it · Practice in PythonRuns in your browser · ⌘/Ctrl + Enter to run
selfhost_practice.py57 lines

Quiz

Try it · Check your understanding0 of 6 answered
  1. 1.What does an OpenAI-compatible server change for the app that calls a model?

  2. 2.The fine-tune got 99 of 100 right and gpt-5.4-mini 97. What can you say?

  3. 3.Why was the fine-tune's round trip so much shorter than mini's (0.11 s vs 1.02 s)?

  4. 4.With 32 simulated users at once, what happened on the laptop server?

  5. 5.At a what-if $2 an hour, why does a self-hosted email cost 20× more when the machine is used 5% of the time than when it's always busy?

  6. 6.A $5/h machine used 5% of the time costs $1.15 per 1,000 emails, under mini's $1.46 list price. Why isn't that the end of it?

Check your understanding

Try each question before opening its answer.

Why is switching between a paid API and your own model "a config change"?

An OpenAI-compatible server such as mlx_lm.server accepts the same requests as OpenAI's API. The app keeps its library and its call and changes settings: the address, the model name, the prompt and a couple of call options.

Is 99 right against 97 a win for the fine-tune?

No, a tie. The two disagreed on only 4 of 100 emails, 3 one way and 1 the other, and a paired test gives p = 0.62. Nano's 75 is clearly behind (p < 0.001). And the fine-tune trained on emails like these.

What explains the 0.11 s vs 1.02 s round trip?

Several differences at once: no internet trip, a prompt 15× shorter (115 vs 1,766 tokens), and almost certainly a much smaller model. The lab didn't measure which matters most.

What happens to each email's time when 32 users share the laptop?

Throughput rises from 8.6 to 24.2 emails a second, and the typical email takes 1.14 s, about mini's. The slow end, 1.52 s, is still under mini's 2.09 s with one user.

How do you price a self-hosted email, and what moves it most?

Hourly cost ÷ emails handled per hour × 1,000. How busy the machine is moves it most: at $2/h, 2.3¢ per 1,000 always busy, 46¢ at 5%, and $9.60 if it's kept on all day for 5,000 emails.

Why compare against the API's discounted price, not its list price?

Mini's long, repeated instructions qualify for the prompt-cache discount. Estimated from OpenAI's prices, that brings mini from $1.46 to about 42–77¢ per 1,000, and a mostly idle $5/h machine ($1.15) goes from cheaper to dearer. The run didn't record it, so measure your cached tokens.

Next up: Chapter 32 starts on resilience: what to do when a model call fails, and how retries can make an outage worse unless they wait the right way.

Learning something useful?

New labs, articles, and videos on evaluating and testing AI systems come out regularly. Follow along so you don't miss the next one.