Bonus: How Do Java Threads, CompletableFuture and CPU Cores Really Work?

One question comes up almost every time developers start deploying Java applications on Kubernetes.

“If my application uses CompletableFuture to upload five files in parallel, do I need five CPU cores?”

Or another variation:

“If my container has a CPU limit of 500m, does that mean it can only create 500 threads?”

The short answer is No.

Threads, CPU cores and Kubernetes CPU limits are related, but they are not the same thing.

Let’s understand why.


A Simple Analogy

Imagine a bank with customers waiting in line.

Customers = Tasks

Cashiers = CPU Cores

Queue = Threads

Suppose the bank has only one cashier.

             Bank

        +------------+
        | Cashier 1  |
        +------------+

Waiting Queue

Customer 1

Customer 2

Customer 3

Customer 4

Customer 5

Even though five customers are waiting, only one customer is being served at any given moment.

The same thing happens inside your computer.


CPU Cores Execute Work

A CPU core is the hardware that actually performs computations.

A thread is simply a unit of work waiting to be executed.

Think of it this way:

CPU Core

↓

Executes

↓

One Runnable Thread

↓

For a Very Small Time Slice

After a few milliseconds, the operating system may pause that thread and run another one.

This process is called context switching.

Because these switches happen thousands of times per second, it appears as though many threads are running simultaneously—even on a single CPU core.


Threads Are Not CPU Cores

This is one of the biggest misconceptions.

Consider the following configuration.

ExecutorService executor =
Executors.newFixedThreadPool(20);

Does this mean your application now needs twenty CPU cores?

Absolutely not.

It simply means Java can schedule up to twenty worker threads.

Those twenty threads will compete for the available CPU time.

For example:

20 Java Threads

↓

Linux Scheduler

↓

2 CPU Cores

The operating system continuously decides which thread gets CPU time next.


What Does 500m CPU Actually Mean?

Another common misunderstanding is that Kubernetes somehow limits the number of threads.

It doesn’t.

Suppose your Deployment specifies:

resources:
  requests:
    cpu: 500m
  limits:
    cpu: "1"

This configuration does not mean:

  • 500 threads
  • 500 requests
  • 500 concurrent users

Instead it means:

Guaranteed CPU Time

↓

Half of One CPU Core

Maximum CPU Time

↓

One CPU Core

Your application can still create hundreds of threads if it wants to.

Kubernetes only controls how much processor time those threads receive.


Let’s Apply This to File Uploads

Imagine your application receives five files.

File 1 : 50 MB

File 2 : 50 MB

File 3 : 50 MB

File 4 : 50 MB

File 5 : 50 MB

You decide to upload them in parallel.

CompletableFuture.allOf(
    upload(file1),
    upload(file2),
    upload(file3),
    upload(file4),
    upload(file5)
);

Internally, five worker threads begin processing.

Upload Thread 1

Upload Thread 2

Upload Thread 3

Upload Thread 4

Upload Thread 5

Does this mean you need five CPU cores?

Not necessarily.

The answer depends on what those threads are doing.


Scenario 1 – Uploading Files to Object Storage

Suppose each thread simply streams data to Amazon S3.

Read File

↓

Send Network Packet

↓

Wait

↓

Send Next Packet

↓

Wait

↓

Complete Upload

Notice something interesting.

Most of the time, the thread isn’t performing calculations.

It’s waiting for:

  • Network latency
  • Disk I/O
  • Storage acknowledgement

This type of workload is called I/O-bound.

Even with five concurrent uploads, CPU utilization may remain surprisingly low because the processor spends very little time performing actual computation.


Scenario 2 – Processing Every File

Now consider a different application.

Each uploaded image is:

  • Virus scanned
  • Watermarked
  • Resized
  • Compressed
  • OCR processed
  • Encrypted

The flow now becomes:

Upload

↓

Virus Scan

↓

OCR

↓

Image Resize

↓

Compression

↓

Encryption

↓

Store File

This workload is very different.

Here, every thread continuously performs CPU-intensive calculations.

This is called a CPU-bound workload.

In this case, adding more CPU cores often improves throughput because multiple calculations can execute simultaneously.


I/O-Bound vs CPU-Bound

Understanding this distinction is one of the most important skills in performance tuning.

I/O-Bound WorkloadsCPU-Bound Workloads
REST API CallsImage Processing
Oracle QueriesOCR
Redis AccessPDF Generation
Kafka/Solace PublishEncryption
S3 UploadCompression
File DownloadVideo Transcoding

I/O-bound applications typically benefit from additional threads because many of them spend their time waiting.

CPU-bound applications benefit from additional CPU cores because the processor is continuously busy.


So How Many Threads Should You Create?

There is no universal answer.

For CPU-intensive workloads, creating hundreds of threads rarely improves performance because only a limited number of CPU cores can execute simultaneously.

For I/O-intensive workloads such as REST APIs, database access, or cloud storage uploads, using more threads often improves throughput because most threads spend their time waiting rather than computing.

The optimal thread pool size depends on:

  • CPU availability
  • Network latency
  • Database response time
  • Storage performance
  • External service latency
  • Amount of work performed per request

The correct value should always be determined through performance testing rather than guesswork.


Applying This to Our File Upload Service

Let’s revisit the document upload service used throughout this series.

Client

↓

Spring Boot

↓

Upload to Object Storage

↓

Save Metadata in Oracle

↓

Publish Solace Event

↓

Return Response

Most of the processing time is spent waiting for external systems:

  • Object Storage
  • Oracle Database
  • Messaging Broker

This means the service is primarily I/O-bound.

A modest CPU allocation—combined with an appropriately sized thread pool—can often support many concurrent uploads.

If the service later adds OCR, image processing, encryption, or virus scanning, CPU demand will increase significantly, and infrastructure sizing should be revisited.


Key Takeaways

✔ Threads do not map one-to-one with CPU cores.

✔ Kubernetes CPU limits control processor time, not the number of Java threads.

CompletableFuture creates concurrent work but does not automatically require additional CPU cores.

✔ File upload services are usually I/O-bound unless they perform heavy processing such as OCR, image manipulation, compression, or encryption.

✔ The best infrastructure sizing comes from performance testing under realistic production workloads—not from simply counting threads or files.

Leave a Reply

Your email address will not be published. Required fields are marked *