What a 3:1 CPU oversell costs a Minecraft server
One question: if three servers are each promised a CPU thread and all three get the same one, what happens to the tick? We ran PAPER 1.21.4 pinned to a single logical CPU, once alone and once with three servers sharing it, under a load held identical and verified at three checkpoints. The average tick went from 41.65 ms to 137.28 ms — 3.30× — and the server stopped keeping up: 20 TPS alone, 7.3 TPS shared.
The numbers
300 seconds sampled every 5 s, after a 60 s warm-up. A tick has a 50 ms budget: spend more and the server runs below 20 TPS.
| Arm | MSPT avg | p50 | p95 | TPS avg | Tick budget used |
|---|---|---|---|---|---|
| 1 server on the pinned thread | 41.65 ms | 41.3 ms | 43 ms | 20 | 83% |
| 3 servers on the same thread | 137.28 ms | 136.1 ms | 151.2 ms | 7.3 | 275% |
Alone, the server spent 83% of its tick budget and still held 20 TPS — busy, never behind. Sharing the thread three ways, each tick cost 3.30× as much: close to the 3× you would predict if three equally loaded servers really did split one thread evenly, which is the point — the arithmetic of overselling is not hidden, it lands directly on the tick.
Everything needed to run it again
Machine
AMD Ryzen 7 9700X 8-Core Processor
16 logical CPUs · kernel 6.8.0-124-generic
host truetick-na-1 (prod)
Server
PAPER 1.21.4 · heap 3072 MB
seed 888777666 · cpuset 6
itzg/minecraft-server@sha256:ddc48d6eff894f88bdc19728363aab40d8557b1b115ef10657bb07c61728e0c7
Load
2000 villagers per server, radius 48
PersistenceRequired · Invulnerable
time midnight · daylight false · spawning false
randomTickSpeed 3 · maxEntityCramming 0
forceload -112, -112, 112, 112
The load is data, not prose: the same manifest the harness reads is published as raw JSON below. Mobs are summoned persistent so they don't despawn, invulnerable so they don't drown or fall, and the world is pinned to night with spawning off so nothing wanders into the measurement.
Proof the load was real — and stayed real
A benchmark that doesn't check its own load can measure anything. Entity counts were read over RCON on every container at three points: after seeding, after warm-up, and after sampling. A run aborts if any container holds less than half its seeded count.
| Container | After seed | After warm-up | After sampling |
|---|---|---|---|
| bench-dedicated-1 | 2000 | 2000 | 2000 |
| bench-oversubscribed-1 | 2000 | 2000 | 2000 |
| bench-oversubscribed-2 | 2000 | 2000 | 2000 |
| bench-oversubscribed-3 | 2000 | 2000 | 2000 |
All four containers held exactly 2000 through the whole run, so both arms were measuring one constant, identical load — not a population quietly draining away.
Three runs that didn't survive review
These produced clean-looking numbers. They were wrong, and each was caught by checking the measurement instead of the conclusion.
Empty load dressed up as a stress test
First recipe summoned 300 zombies in a tight ring. They killed each other off — 109 of 300 alive after 20 seconds — and the tick cost 1.8 ms of a 50 ms budget. We were measuring an idle server with extra steps.
A quarter of the budget, and double the CPU we meant to give
The fixed zombie recipe still only used ~12 ms of the tick budget, so three servers had nothing to contend over. Worse, pinning cpuset 6,14 handed the containers both SMT threads of one core while their quota stayed at one CPU — a 1.5:1 oversell, not the 3:1 we were claiming to test.
A load that drained away while being measured
Villagers fixed the tick cost, but entity counts fell 49% (dedicated) and 39% (oversubscribed) during sampling, so the printed averages described a load that no longer existed — and the two arms were no longer running the same load as each other. Disabling entity cramming didn't stop it; summoning the villagers invulnerable did.
What this does not show
- One run, one machine. A single AMD Ryzen 7 9700X 8-Core Processor on PAPER 1.21.4. Different hardware, versions or server software will land somewhere else.
- The 3:1 arm is ours, not anyone else's. We built the oversell to measure it. This is not a measurement of any other host, and we make no claim about what anyone else sells.
--cpus=1is the bench's choice, not our pricing. Both arms use it so the only variable is how many servers share the thread. Our own servers get a quota by RAM tier, not a flat one CPU, and we don't pin cores at all — see pricing for what a plan actually includes.- Entity load is one kind of load. Mob AI and pathfinding stress the main tick thread; redstone, chunk generation and plugin work stress it differently.
Take the numbers, not our word
Every sample from the run — 60 points per container, both arms, plus the full manifest — as one JSON file. It is the same file this page reads.
Captured 2026-07-25T12:33:58Z · CC BY 4.0 — reuse it, cite the page.