What a 3:1 CPU oversell costs a Minecraft server
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, but never behind. Sharing the thread three ways, each tick cost 3.30× as much. That is close to the 3× you would predict if three equally loaded servers split one thread evenly, which shows the cost of overselling landing 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 harness reads the load from a manifest, and that same manifest 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.
Checking that the load held for the whole run
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 the same load, and it stayed constant.
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
The 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. That made it 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. 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 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.
The data behind this page
Every sample from the run is in one JSON file: 60 points per container for both arms, plus the full manifest. This page reads the same file.
Captured 2026-07-25T12:33:58Z · CC BY 4.0. Reuse it and cite this page.