kenny
Published on 2026-09-23 / 30 Visits
1
0

How to Calculate Compute for High-Concurrency Systems

Intro

Every time I sit down to size a server, I ask myself one question: "How many CPU cores does this thing actually need?"

You can't just eyeball it. I've seen people divide thread count by two, I've seen people buy the biggest instance they can afford "just in case," and I've seen people match CPU cores to Tomcat's threads.max—then wonder why utilization never breaks 20%.

The root of this problem is simple: thread count and CPU core count are not the same thing. A Tomcat instance with 800 worker threads will run just fine on 4 cores. Why? Because most of those threads are waiting, not computing. I'll share the capacity planning method I use myself to make the relationship between threading models and compute requirements clear.

1. The Counterintuitive Truth: More Threads ≠ More CPU

In the JVM, a thread spends most of its life in one of these states:

State

What it means

Burns CPU?

RUNNABLE

Running or ready to run

Yes

WAITING

Waiting for another thread

No

TIMED_WAITING

Waiting with a timeout

No

BLOCKED

Waiting on a lock

No

In a typical Spring Boot web app, threads spend the vast majority of their time in WAITING—waiting for the database, waiting for disk I/O, waiting for a downstream API. The time actually spent RUNNABLE might be 10–20%.

What this means: Of those 800 threads, only a few dozen are competing for CPU at any given moment. The rest are parked. CPU core count should match the number of simultaneously runnable threads, not the total thread pool size.

Think of it like a restaurant. 800 waiters sounds like a lot, but most of them are standing around waiting for the kitchen or taking orders. Only a handful are actually running food at any given second. How many cooks (CPU cores) you need depends on how many dishes are being prepared at once—not how many waiters you employ.

2. IO-Bound vs CPU-Bound: Two Completely Different Calculations

IO-Bound (most Spring Boot apps)

CPU spends most of its time waiting on IO.

Thread count can far exceed core count. The classic formula:

Optimal threads ≈ cores × (1 + wait time / compute time)

Example: a request takes 100ms total, 80ms waiting on the database, 20ms computing.

  • wait/compute = 4

  • On 4 cores: 4 × (1 + 4) = 20 threads

In practice, Tomcat often runs 200–800 threads because context switching is cheap and more threads improve throughput. The key point: core count doesn't need to scale with thread count.

CPU-Bound (encryption, image processing, heavy computation)

CPU is always busy. Thread count should equal core count, maybe +1. More threads just add context-switching overhead.

Type

Thread count

Relationship to cores

IO-bound

Can far exceed cores

Size cores by compute load

CPU-bound

≈ cores

Size cores by parallel tasks

3. Reverse-Engineering Compute Needs from Tomcat Config

Tomcat's three core parameters:

server:
  tomcat:
    max-connections: 10000   # max simultaneous connections
    threads:
      max: 800               # max worker threads
      min-spare: 100         # minimum idle threads
    accept-count: 200        # backlog queue length

What they actually mean:

  • max-connections: how many connections can be open at once (including queued)

  • threads.max: how many requests can be processed concurrently

  • accept-count: how long the queue gets once connections are maxed

The real question: how many cores do 800 threads need?

Answer: it depends on the compute ratio per request.

Assume:

  • Peak QPS: 1000

  • Average response time: 100ms

  • Compute time per request: 20ms

Then the number of requests simultaneously computing ≈ 1000 × 0.02 = 20. In theory, 20 cores would suffice—but accounting for spikes and GC, 8–16 cores is a safer bet.

If compute time is higher (say 50ms), the simultaneous compute count rises, and so does your core requirement.

4. A Practical Sizing Formula

Here's a simplified formula:

Cores needed ≈ peak QPS × CPU time per request (seconds) ÷ target utilization

Example:

  • Peak QPS = 500

  • CPU time per request = 20ms = 0.02s

  • Target utilization = 70%

500 × 0.02 ÷ 0.7 ≈ 14.3 cores

Round up: 16 cores. If budget is tight, start with 8 and monitor. Scale when CPU utilization consistently exceeds 70%.

Caveats: this gives you the average requirement. You still need headroom for:

  • GC pauses (non-negotiable for JVM apps)

  • Traffic spikes (30–50% buffer)

  • Other processes (system, monitoring, logging)

5. Connection Pools and Async Thread Pools Also Eat CPU

Two more places where CPU gets consumed indirectly:

1. HikariCP connection pool

The pool itself doesn't burn CPU, but too many connections cause:

  • Frequent database context switches

  • Lock contention

  • GC pressure (every connection holds objects)

Rule of thumb: 30–50 connections on 4 cores, 50–80 on 8 cores. Don't just max it out.

2. Async thread pool (@Async)

spring:
  task:
    execution:
      pool:
        core-size: 8
        max-size: 16
        queue-capacity: 100

Async pool sizing should also respect core count. IO-bound async tasks can be over-provisioned; CPU-bound async tasks should equal core count.

6. Cloud vs Bare Metal: The Compute Stability Gap

Same core count, very different real-world behavior.

Dimension

Cloud VM

Bare Metal

CPU performance

Can be affected by noisy neighbors (oversubscription)

Dedicated, consistent

Memory bandwidth

Shared, can fluctuate

Dedicated

Disk IO

Often network storage, higher latency

Local NVMe, low latency

Network

Shared bandwidth

Dedicated port

The thing high-concurrency systems fear most isn't low average performance—it's P99 jitter. When a cloud neighbor gets busy, CPU steal time rises and tail latency spikes. Bare metal doesn't have this problem. Compute is predictable.

If your system is P99-sensitive, dedicated bare metal compute is worth more than a few extra cloud cores.

7. When Does GPU Enter the Picture?

Not every high-concurrency workload needs a GPU. GPUs make sense for:

  • AI inference (model forward passes)

  • Video transcoding

  • Image processing

  • Batch scientific computing

Decision rule: if your workload is "lots of simple parallel operations," a GPU wins. If it's "complex logic with lots of branching," CPU is the better fit.

For AI inference in production, GPU bare metal guarantees dedicated VRAM and stable compute—exactly what production workloads need.

8. TL;DR

Back to the original question: how much compute does a high-concurrency system actually need?

Three rules:

  1. Thread count ≠ core count. IO-bound apps can run hundreds of threads on a handful of cores.

  2. Size cores by compute load. Peak QPS × CPU time per request ÷ target utilization.

  3. Stability beats peak performance. P99 jitter hurts user experience more than average latency.

Thank you for reading to the end!



Comment