What 0.5 vCPU Actually Means — Your Container Gets Frozen, Not Slowed

·5 min read·Updated
cpuperformancecontainersthrottlingtroubleshooting

0.5 vCPU does not mean a CPU that runs at half speed. It means a time budget: your container may use 50 milliseconds of CPU time in every 100-millisecond window. Your code runs at the machine's full speed until that budget is gone — and then the kernel stops it dead until the next window opens. That difference matters, because it changes what the symptoms look like. CPU starvation here doesn't show up as everything being uniformly sluggish; it shows up as latency spikes, stalls of tens of milliseconds, and the counter-intuitive result that adding worker threads makes things worse.

The actual mechanic

Every app container is created with two values: a period of 100,000 microseconds (100ms) and a quota equal to your CPU share times that period.

Your allocation Budget per 100ms window
0.1 vCPU 10ms
0.15 vCPU 15ms
0.5 vCPU 50ms
1 vCPU 100ms
2 vCPU 200ms (across all threads)

The kernel's scheduler tracks how much CPU time your container's processes consume. When the total hits the quota, every one of them is descheduled — throttled — until the window rolls over. Then the budget resets and you run again.

Nothing about this is a slower core. It's full speed, then a hard stop.

Why that produces spikes, not slowness

Say a request needs 80ms of CPU and you have 0.5 vCPU:

  1. The request starts. It burns its first 50ms of CPU and consumes the whole window's budget.
  2. Your process is frozen for the remainder of that window — roughly 50ms of doing nothing.
  3. The next window opens. The remaining 30ms of work completes.

Wall-clock time: about 130ms for 80ms of work. Not "a bit slower" — paused mid-flight, and by an amount that depends on where in the window you happened to start.

For a Discord bot this is exactly how you miss the three-second interaction deadline on a command that normally answers instantly. For an HTTP service it's a p99 latency that looks nothing like your p50.

More threads make it worse

This is the part that surprises people. The quota is per container, not per thread. Four threads doing CPU work don't get four budgets — they share one, and they exhaust it four times faster.

So the standard scaling reflex backfires. Spawning more workers on a fractional CPU means the same total work, split into more pieces, with more context switching and more time frozen. You get lower throughput and worse tail latency than a single worker would have given you.

The rule of thumb: concurrency should match your quota, not your host's core count. On anything under 1 vCPU, one worker process is usually correct.

Runtimes that guess wrong

Several runtimes size their thread pools from the number of logical CPUs they can see — and a container sees the host's cores, not your quota. On a large host that means a runtime cheerfully creating dozens of threads to share 50ms.

Go fixed this recently. Per the Go 1.25 release notes: "On Linux, the runtime considers the CPU bandwidth limit of the cgroup containing the process, if any. If the CPU bandwidth limit is lower than the number of logical CPUs available, GOMAXPROCS will default to the lower limit." Before 1.25 you'd want automaxprocs or an explicit GOMAXPROCS.

Python's ThreadPoolExecutor defaults to min(32, (os.process_cpu_count() or 1) + 4). On a many-core host that's up to 32 threads contending for your quota. Pass max_workers explicitly rather than accepting the default, and don't size anything from os.cpu_count() — it reports the machine, not your allocation.

Node keeps a libuv thread pool of 4 by default for filesystem and crypto work, which is usually fine. What isn't fine on a fractional CPU is cluster — forking one worker per reported core creates several processes sharing a budget that barely fits one. Run a single process; don't add a process manager either.

How to tell if you're actually being throttled

You don't have to guess. The kernel counts it, and you can read the counter from the shell in the panel:

cat /sys/fs/cgroup/cpu.stat

On cgroup v2 you get nr_periods, nr_throttled, and throttled_usec. (On cgroup v1 the path is /sys/fs/cgroup/cpu/cpu.stat and the third field is throttled_time, in nanoseconds.)

Read it like this:

  • nr_throttled near zero — CPU isn't your problem. Look at blocking I/O instead.
  • nr_throttled a meaningful fraction of nr_periods — you're hitting the ceiling regularly, and your latency spikes are explained.
  • throttled_usec climbing fast between two reads a minute apart — you're spending real time frozen.

Take two samples a minute apart and compare. A single absolute number since container start tells you much less than a rate.

What to do about it

In the order I'd try them:

  1. Reduce concurrency to match the quota. One worker, an explicit small pool size, no cluster. Free and often enough.
  2. Get CPU work off the hot path. Move image resizing, large JSON parsing, or crypto out of the request or event handler and into a queue you drain slowly. Throttling hurts most when it lands mid-request.
  3. Fix the real hot spot. A tight loop, an unindexed scan repeated per event, or a synchronous call in an async handler will exhaust any budget. More CPU makes a wasteful process fail more slowly, not correctly.
  4. Then raise the preset. Presets go up to 2 vCPU, subject to your account limits, and the change takes effect on restart. Billing is per-minute on what you allocate, so this is the step worth taking on evidence rather than on suspicion.

FAQ

What does 0.5 vCPU mean?

Fifty milliseconds of CPU time per 100-millisecond window. Your code runs at full speed until the budget is gone, then it's frozen until the window resets.

Is a fractional vCPU just a slower processor?

No. It's full-speed execution plus enforced pauses. That's why the symptom is latency spikes rather than uniform slowness.

Why did adding more workers make my app slower?

The quota is shared across the whole container. More threads exhaust the same budget faster and add context-switching overhead.

How do I know if my container is CPU throttled?

Read cpu.stat from the shell and compare nr_throttled against nr_periods. Sample twice a minute apart to get a rate.

Why does my app think it has 8 CPUs?

Containers see the host's cores. Only some runtimes read the cgroup quota — Go does from 1.25 onward; most others need you to set pool sizes explicitly.

Will more CPU fix my slow bot?

Only if you've confirmed throttling. Blocking calls in an async handler look identical from the outside and don't improve with a bigger quota.

Wrapping up

Fractional CPU is a scheduling contract, not a smaller processor, and almost every confusing performance symptom on a small container follows from that one fact. Your process is fast right up until it's stopped. Size your concurrency to the budget instead of to the host, read cpu.stat before assuming, and reach for a bigger preset once you've seen nr_throttled climbing rather than because something felt slow.