Back to Thread Pool: Sizing & Pitfalls
Q7Question 7 of 7

How to estimate target throughput?

Key Concepts
  • Little's Law: concurrency equals throughput (requests per second) multiplied by average latency (seconds).
  • 1000 TPS at 100 ms means 100 concurrent requests in flight at any instant.
  • Memory equals concurrency times per-request footprint plus the steady-state heap for caches and pools.
  • Set maximum heap to roughly two to three times the live set so the collector has room to work.
  • Set -Xms equal to -Xmx on servers to avoid runtime heap-resizing pauses.
  • Allocation rate equals per-request allocation times TPS; the GC must reclaim at that rate within the latency budget.
  • Validate with a load test at the real target and adjust from observed P99, GC pause, and allocation rate.

Definition

Concurrency equals throughput times latency, memory equals concurrency times per-request footprint plus steady state, and the garbage collector must clear that allocation inside the latency budget.
  • Little's Law – the average number of concurrent requests equals the arrival rate multiplied by the average time each request spends in the system (L = lambda x W).
  • Concurrency – for 1000 TPS at 100 ms each, concurrency is 1000 x 0.1 = 100 requests in flight at any instant.
  • Transient working set – concurrency times per-request retained memory gives the heap needed for in-flight requests.
  • Heap sizing – set -Xmx to two to three times the live set (transient plus steady-state) so the GC has headroom.
  • GC selection – match the collector's pause behaviour to the latency budget and its throughput to the allocation rate.

Imagine National Bank of India must serve 1000 customers every second, and each customer takes 100 milliseconds at a counter. Little's Law says that at any instant you will have 1000 times a tenth of a second, which is 100 customers standing at counters simultaneously, so you need 100 counters' worth of space and supplies. If each customer needs a certain amount of desk paper (per-request memory), then 100 concurrent customers need 100 times that paper on the desks at once, plus the branch's permanent filing cabinets (caches). You then make sure the cleaning crew (garbage collector) can tidy desks fast enough that no customer waits too long.

Understanding the Concepts

What is Little's Law and why does it matter?

Little's Law says L = lambda x W, where L is the average number of requests in the system, lambda is the throughput (requests per second), and W is the average time in the system (seconds). For 1000 TPS at 100 ms, L = 1000 x 0.1 = 100. On average 100 requests are being processed at any instant. This number drives thread pool size, memory, and GC tuning.

How do you convert concurrency to memory?

If one request retains about 2 MB at peak (request object, domain graph, JSON buffers), then 100 concurrent requests have a transient working set of 200 MB. Add the steady-state heap for caches, connection pools, and reference data, perhaps 300 MB, giving a live set around 500 MB.

JAVA
package com.bank.capacity;

import java.math.BigDecimal;

public class CapacityPlanner {

    public static void main(String[] args) {
        double tps = 1_000;
        double latencySeconds = 0.100;
        double concurrency = tps * latencySeconds;              // 100

        long perRequestBytes = 2L * 1024 * 1024;                // 2 MB measured
        long transientSet = (long) concurrency * perRequestBytes; // ~200 MB
        long steadyState = 300L * 1024 * 1024;                  // caches, pools
        long liveSet = transientSet + steadyState;               // ~500 MB

        System.out.println("Concurrency: " + (long) concurrency);
        System.out.println("Transient set: " + transientSet / (1024 * 1024) + " MB");
        System.out.println("Live set: " + liveSet / (1024 * 1024) + " MB");
        System.out.println("Recommended -Xmx: " + (liveSet * 3) / (1024 * 1024) + " MB");
    }
}
TEXT
Concurrency: 100
Transient set: 200 MB
Live set: 500 MB
Recommended -Xmx: 1500 MB

How do you size the heap?

Set maximum heap to two to three times the live set so the collector has room. With a 500 MB live set, choose -Xmx1500m to -Xmx2g. Set -Xms equal to -Xmx on servers to avoid runtime resizing pauses. Too small a heap means frequent collections; too large wastes memory and can lengthen individual pauses.

How do you compute the allocation rate?

Allocation is higher than retained because of intermediate objects. If each request allocates 4 MB, then at 1000 TPS the allocation rate is about 4 GB per second. The GC must reclaim that fast enough.

JAVA
double allocPerRequestMB = 4;
double allocationRateGBps = (tps * allocPerRequestMB) / 1024;   // ~3.9 GB/s
System.out.println("Allocation rate: " + allocationRateGBps + " GB/s");

How do you choose a garbage collector?

G1GC (default since Java 9) targets a pause-time goal via -XX:MaxGCPauseMillis. For a 100 ms latency budget, set the goal to about 50 ms.

BASH
-Xms2g -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=50 -XX:+ParallelRefProcEnabled

For very low pauses at large heaps, ZGC or Shenandoah keep pauses in the low single-digit milliseconds. The Parallel collector maximizes throughput but has longer stop-the-world pauses, suiting batch jobs rather than latency-sensitive APIs.

| Collector | Pause Target | Best For |
|-----------|-------------|----------|
| G1GC | Configurable (e.g. 50 ms) | General APIs |
| ZGC | Sub-millisecond | Large heaps, strict SLO |
| Shenandoah | Sub-millisecond | Low-latency services |
| Parallel | Maximise throughput | Batch processing |

What happens when latency rises under load?

Latency and throughput are coupled. If latency rises under load, Little's Law says concurrency rises too, which increases memory and can spiral. You protect the system with bounded thread pools and admission control.

What are common mistakes?

The wrong approach is guessing heap and pool sizes without measurement.

BASH
# WRONG: random heap, no GC tuning
java -Xmx512m -jar payment-service.jar

The correct approach derives every number from Little's Law and measured per-request footprint.

BASH
# CORRECT: derived from capacity plan
java -Xms2g -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=50 -jar payment-service.jar

How do you validate the estimate?

Run a load test at the target 1000 TPS, watch P99 latency, GC pause time, and allocation rate, and adjust heap and collector flags from real data. Plan headroom for bursts above the average.

What are common interview follow-up questions?

  • How does virtual thread scalability change the concurrency calculation?
  • What if the service is IO-bound versus CPU-bound?
  • How do you size the database connection pool alongside the thread pool?

Visual Overview

DIAGRAM
  Little's Law:  L = lambda x W

  lambda = 1000 TPS     W = 0.1 sec
       │                    │
       └───── L = 100 ──────┘
            concurrent
            requests
               │
       ┌───────┴────────┐
       ▼                ▼
  Thread pool       Memory sizing
  >= 100 threads    100 x 2 MB = 200 MB transient
                    + 300 MB steady state
                    ─────────────────────
                    500 MB live set
                    x 3 = 1500 MB -Xmx
               │
               ▼
  GC selection: G1GC -XX:MaxGCPauseMillis=50
  Allocation rate: 4 GB/s
               │
               ▼
  Load test → validate P99, GC pauses, alloc rate

Key Takeaway

Capacity planning for a throughput target starts with Little's Law, which says the average number of requests in the system equals the arrival rate multiplied by the time each request spends in the system. For a target of one thousand transactions per second at one hundred milliseconds each, that is one hundred concurrent requests.

From concurrency I size memory. I take the per-request footprint, say two megabytes retained at peak, and multiply by the hundred concurrent requests to get a two-hundred-megabyte transient working set. I add the steady-state heap for caches and pools, giving a live set around five hundred megabytes. I then set the maximum heap to roughly two to three times the live set.

The other number I always compute is the allocation rate. If each request allocates about four megabytes, then at one thousand TPS the collector must reclaim roughly four gigabytes per second. With a hundred-millisecond latency budget I choose G1GC and set an explicit pause-time goal of around fifty milliseconds.

At National Bank of India this exact method took our payments API launch from random guessing and constant garbage collection to a predictable, validated configuration that held one thousand TPS within budget.

Remember one sentence: concurrency equals throughput times latency, memory equals concurrency times per-request footprint plus steady state, and the GC must clear that allocation within the latency budget.

---

Share
Use ← → arrow keys to navigate