top of page

01 · INTRODUCTION

How Many Threads Can Each iOS App Create?

64 threads is the number most iOS developers should know when discussing the system-managed worker threads used by Grand Central Dispatch's global concurrent queues.

If you submit enough blocking work to GCD's global queues, you can observe the system growing its worker pool until it reaches roughly this 64-thread ceiling. That is why the number 64 appears so often in discussions about iOS threading.

But there is an important distinction to make immediately.

64 is not a universal statement that an iOS process can never contain more than 64 threads. An application can create threads through lower-level threading APIs as well, and those are constrained by process limits, system resources and operating-system implementation details rather than by the GCD global workqueue ceiling alone.

So the useful answer is:

💡 The Numeric Answer

GCD global concurrent queues are commonly limited to approximately 64 worker threads.

An iOS process itself is not best understood as having a universal hard limit of exactly 64 total threads.

That distinction turns what looks like a simple numeric question into a much more useful concurrency lesson.

02 · TUTORIAL

Why Does the Number 64 Appear?

Grand Central Dispatch does not create a brand-new permanent thread every time you call:

CODE EXAMPLE

DispatchQueue.global().async {
    performWork()
}

You submit work to a system-managed concurrent queue.

GCD uses an underlying worker-thread system to execute that work. When existing worker threads are busy or blocked, the system can make additional worker threads available, up to limits imposed by the workqueue implementation.

Historically and in practical experiments on iOS, developers encounter a ceiling of approximately 64 worker threads for the global GCD workqueues.

This is the origin of the familiar number.

It is useful because it demonstrates something important: even Apple's managed concurrency system does not allow an unlimited number of worker threads to be created simply because an application keeps submitting work.

03 · TUTORIAL

64 GCD Workers Does Not Mean 64 Threads in the Entire Process

This is the distinction that is often lost when the number is repeated online.

Your application process already contains a main thread.

Frameworks may use additional threads.

Your own code can create threads using lower-level threading APIs.

GCD itself can manage worker threads.

Other runtime systems inside the process can also require execution contexts.

So this mental model is too simplistic:

EXECUTION DIAGRAM

MY IOS APP
     │
     ▼
Maximum possible threads = 64

A more accurate model is:

EXECUTION DIAGRAM

MY IOS APP PROCESS
        │
        ├── Main Thread
        ├── Framework Threads
        ├── Manually Created Threads
        ├── GCD Worker Threads
        └── Other Runtime Threads

The 64 figure is particularly associated with GCD's global worker-thread pool behavior.

It should not be mistaken for the total number of thread objects that can ever exist inside an iOS process.

04 · TUTORIAL

What Happens If I Create Threads Manually?

At a lower level, POSIX threads can be created using APIs such as pthread_create.

Apple's pthread documentation explicitly states that thread creation can fail when the system lacks resources or when the system-imposed per-process thread limit would be exceeded.

That tells us two useful things.

First, a process does have finite limits.

Second, those limits should not be confused with the GCD global workqueue's 64-worker behavior.

Conceptually:

EXECUTION DIAGRAM

pthread_create(...)
       │
       ▼
Can the process/system
support another thread?
       │
       ├── YES ──▶ create thread
       │
       └── NO  ──▶ creation fails

The limiting factor is ultimately the operating system and the resources available to the process and device.

05 · TUTORIAL

Start Again With Our Process

Return to the model we have built throughout this series.

EXECUTION DIAGRAM

┌─────────────────────────────────────────┐
│              MY APP PROCESS             │
│                                         │
│          Application Memory             │
│                                         │
│          Main Thread                    │
│                                         │
└─────────────────────────────────────────┘

Our application starts with a main thread.

Additional threads can exist inside the same process.

EXECUTION DIAGRAM

┌─────────────────────────────────────────┐
│              MY APP PROCESS             │
│                                         │
│          Application Memory             │
│                                         │
│          Main Thread                    │
│          Thread 2                       │
│          Thread 3                       │
│          Thread 4                       │
│          Thread 5                       │
│          ...                            │
│                                         │
└─────────────────────────────────────────┘

There is no architectural rule saying that every iOS application receives exactly 64 slots in that list.

The number 64 becomes relevant when we discuss one particular managed source of threads: GCD's global worker system.

06 · TUTORIAL

Why Does GCD Limit Its Worker Threads?

Because creating additional threads does not create additional processor cores.

Imagine that our simplified device contains four processor cores.

CODE EXAMPLE

CORE 1
CORE 2
CORE 3
CORE 4

Now imagine the application has four runnable worker threads.

EXECUTION DIAGRAM

Thread 1 ─────────▶ Core 1
Thread 2 ─────────▶ Core 2
Thread 3 ─────────▶ Core 3
Thread 4 ─────────▶ Core 4

Those four pieces of CPU-bound work could potentially execute in parallel.

Now create sixty more runnable threads.

EXECUTION DIAGRAM

64 runnable worker threads
            │
            ▼
       4 CPU cores

We still only have four cores.

The additional threads do not create additional hardware execution capacity.

Instead, the operating system has more runnable execution contexts to manage and schedule.

07 · TUTORIAL

A Thread Has a Cost

A thread is not merely an entry in an array.

Each thread requires operating-system resources and execution state. It has a stack, kernel bookkeeping, scheduling information and processor state that must be preserved and restored as execution moves between threads.

Conceptually:

EXECUTION DIAGRAM

THREAD

┌────────────────────────────┐
│ Stack                      │
│                            │
│ Execution state            │
│                            │
│ Register state             │
│                            │
│ Scheduling information     │
│                            │
│ Current execution location │
└────────────────────────────┘

Create another thread and the system must manage another execution context.

Create sixty-four worker threads and the system is managing sixty-four of those contexts in addition to the other threads already belonging to the process.

Create hundreds of manually managed threads and the costs continue to accumulate.

08 · TUTORIAL

Every Thread Needs a Stack

One particularly visible cost is the thread stack.

Consider:

CODE EXAMPLE

func startLaunch() {
    prepareRocket()
}

func prepareRocket() {
    loadConfiguration()
}

func loadConfiguration() {
    readSettings()
}

As functions call other functions, execution needs somewhere to maintain the active call state associated with that thread.

A simplified stack might look like:

EXECUTION DIAGRAM

THREAD STACK

┌─────────────────────────┐
│ readSettings()          │
├─────────────────────────┤
│ loadConfiguration()     │
├─────────────────────────┤
│ prepareRocket()         │
├─────────────────────────┤
│ startLaunch()           │
└─────────────────────────┘

Each thread needs its own stack because each thread can be at a completely different point in execution.

EXECUTION DIAGRAM

Thread 1             Thread 2             Thread 3

┌───────────┐        ┌───────────┐        ┌───────────┐
│   Stack   │        │   Stack   │        │   Stack   │
│           │        │           │        │           │
│           │        │           │        │           │
└───────────┘        └───────────┘        └───────────┘

This is another reason there cannot sensibly be an unlimited number of system threads.

09 · TUTORIAL

Now Imagine One Thousand Threads

Suppose an application needs to perform one thousand asynchronous operations.

A naive design might make this assumption:

CODE EXAMPLE

1 operation = 1 thread

1000 operations = 1000 threads

That would create an enormous number of execution contexts:

CODE EXAMPLE

Thread 1
Thread 2
Thread 3
Thread 4
Thread 5
...
Thread 996
Thread 997
Thread 998
Thread 999
Thread 1000

But the device might have only a handful of CPU cores capable of executing those instructions simultaneously.

The thread count has exploded.

The processing hardware has not.

10 · TUTORIAL

This Is Thread Explosion

When concurrency design causes excessive numbers of threads to be created, the problem is commonly described as thread explosion.

Imagine several independent features in a large commercial application.

The image system creates threads.

The networking system creates threads.

The database layer creates threads.

A third-party framework creates more.

Another developer decides their feature also needs several dedicated threads.

The process can quickly accumulate far more execution contexts than can usefully run in parallel.

EXECUTION DIAGRAM

MY APP PROCESS

T1   T2   T3   T4   T5   T6   T7   T8
T9   T10  T11  T12  T13  T14  T15  T16
T17  T18  T19  T20  T21  T22  T23  T24
T25  T26  T27  T28  T29  T30  T31  T32
...
T64
...
more manually-created threads
        │
        ▼
FINITE CPU CORES

At that point, more threads do not automatically mean more useful concurrency.

They can mean more memory pressure, more scheduling work and more context switching.

11 · TUTORIAL

More Threads Can Make Performance Worse

This is initially counterintuitive.

If threads make concurrent execution possible, then surely more threads must produce more concurrency.

Not necessarily.

The hardware is still finite.

Imagine a device with six CPU cores and sixty-four runnable threads.

EXECUTION DIAGRAM

64 RUNNABLE THREADS
        │
        ▼
6 CPU CORES

Only a limited number of those threads can physically execute at the same instant.

The others must wait for processor time.

Adding more runnable threads therefore creates more contenders for the same finite execution resources.

At some point, additional scheduling and context-switching overhead can reduce efficiency rather than improve it.

12 · TUTORIAL

Why GCD Manages the Threads for Us

This is exactly the kind of problem Grand Central Dispatch was designed to remove from ordinary application architecture.

Instead of manually creating:

CODE EXAMPLE

Thread 1
Thread 2
Thread 3
Thread 4
...

we submit work:

CODE EXAMPLE

DispatchQueue.global().async {
    processImage()
}

Our responsibility is to describe the work.

GCD manages the execution resources used to perform that work.

Apple's own concurrency guidance recommends this higher-level approach because the optimal number of threads can vary with device hardware and current system load. Letting the system manage threads gives applications a level of scalability that manually managed thread architectures struggle to achieve. citeturn143415search9

13 · TUTORIAL

Why Does GCD Sometimes Grow Toward 64 Threads?

This becomes particularly interesting when submitted work blocks.

Imagine several global-queue blocks begin executing and then block waiting for something.

EXECUTION DIAGRAM

Worker 1 ─────▶ BLOCKED
Worker 2 ─────▶ BLOCKED
Worker 3 ─────▶ BLOCKED
Worker 4 ─────▶ BLOCKED

There is still pending work waiting on the queue.

GCD can make additional worker threads available so that useful work can continue rather than leaving the entire pool stuck behind blocked execution contexts.

If enough submitted work keeps blocking, the number of worker threads can therefore grow substantially.

Eventually the system reaches its workqueue limits.

This is where developers often encounter the approximately 64-worker-thread ceiling associated with global GCD queues.

The number is therefore not an invitation to use sixty-four threads.

It is a protective boundary in a managed execution system.

14 · TUTORIAL

Blocking 64 Worker Threads Is Not a Goal

This distinction is important enough to make explicit.

When developers discover that GCD may grow toward sixty-four workers, the wrong conclusion is:

CODE EXAMPLE

Great.
I have 64 background threads available.

The better conclusion is:

CODE EXAMPLE

The system has limits because
blocking worker threads is expensive.

If your architecture routinely needs dozens of blocked worker threads merely to represent asynchronous work, the architecture itself deserves examination.

This is precisely where modern asynchronous programming becomes more interesting.

15 · TUTORIAL

Waiting Work Does Not Need to Own a Thread

Consider a network request.

CODE EXAMPLE

let data = try await URLSession.shared.data(from: url)

The request may take a second to complete.

But our processor does not need to continuously execute one second of machine instructions merely because the network operation has not finished.

Most of that lifetime represents waiting.

A primitive one-operation-one-thread model would look like:

EXECUTION DIAGRAM

Network Request
      │
      ▼
Dedicated Thread
      │
      ▼
WAIT...
WAIT...
WAIT...
WAIT...
      │
      ▼
Response arrives

That wastes an execution context simply to represent unfinished asynchronous work.

A better abstraction separates the lifetime of the operation from the lifetime of a thread.

16 · TUTORIAL

Work and Threads Are Different Things

This is the key transition.

Imagine your application has one thousand asynchronous operations.

CODE EXAMPLE

Operation 1
Operation 2
Operation 3
Operation 4
...
Operation 1000

That does not mean the application needs one thousand system threads.

We need to separate:

CODE EXAMPLE

WORK TO BE DONE

from:

CODE EXAMPLE

EXECUTION RESOURCES USED TO DO IT

This distinction is one of the foundations of modern concurrency.

17 · TUTORIAL

This Is Why Swift Tasks Are So Important

Swift Concurrency lets us represent asynchronous work using Task.

A Task is not a dedicated system thread.

That means we can represent many asynchronous operations without needing a permanent one-to-one relationship between tasks and threads.

EXECUTION DIAGRAM

Task 1
Task 2
Task 3
Task 4
Task 5
...
Task 1000

        │
        ▼

Swift Concurrency Runtime

        │
        ▼

Executors / Eligible Jobs

        │
        ▼

System Threads

        │
        ▼

CPU Cores

Now our architecture matches the actual requirement much better.

We may have many pieces of asynchronous work.

Only the work that is currently eligible to execute requires execution resources.

18 · TUTORIAL

Suspension Changes Everything

Suppose one task reaches:

CODE EXAMPLE

let result = try await loadData()

If the operation cannot complete immediately, the task may suspend.

The task still exists.

Its asynchronous operation has not disappeared.

But it does not need to keep a system thread blocked simply to represent the fact that it is waiting.

EXECUTION DIAGRAM

Task A
██████████
         │
         ▼
       await
         │
      suspend
         │
         │          Task B
         │          ██████████
         │
         │               Task C
         │               ██████████
         │
         ▼
Task A becomes eligible again
██████████

This is a much more scalable relationship between asynchronous work and underlying threads.

19 · TUTORIAL

So How Many Threads Can My App Create?

Now we can answer the original question precisely.

💡 Final Answer

For GCD's global concurrent worker system, approximately 64 worker threads is the commonly observed practical ceiling.

That does not mean an iOS application process can contain only 64 total threads. Threads created through other mechanisms are governed by process/system resource limits and can increase the total process thread count beyond the GCD worker pool.

The number is useful.

But the architecture behind the number is more useful.

GCD limits its workers because threads are expensive, processor cores are finite and allowing arbitrary thread growth would damage system efficiency.

Swift Concurrency then takes the next logical step by allowing us to represent asynchronous work without requiring each operation to own a thread.

20 · TUTORIAL

What to Remember

💡 What to Remember

64 is the important numeric value commonly associated with the maximum worker-thread growth of GCD global concurrent queues.

It is not a universal hard limit saying that an iOS application process can never contain more than 64 total threads.

Your process can contain its main thread, framework-created threads, manually created pthreads, GCD worker threads and other runtime threads.

Every thread consumes resources, including stack and kernel-management resources.

Creating more threads does not create more CPU cores.

When runnable threads outnumber available processor cores, the operating system must schedule those threads across the finite hardware.

Excessive thread creation can lead to thread explosion, increased memory usage, scheduling overhead and context switching.

GCD manages worker threads so application developers can submit work rather than manually owning execution resources.

Swift Concurrency goes further by representing asynchronous operations as Tasks that can suspend instead of requiring a dedicated thread while waiting.

The modern question is therefore not simply “How many threads can I create?”

It is “Why would my application need another thread at all?”

21 · TUTORIAL

Your Next Move

We now understand that our application can contain many threads, that GCD's global worker system commonly grows to a maximum of approximately 64 workers, and that threads are finite execution resources rather than unlimited units of additional processing power.

But every diagram in this series has contained one thread before we created any additional ones:

EXECUTION DIAGRAM

┌─────────────────────────────────────────┐
│              MY APP PROCESS             │
│                                         │
│              Main Thread                │
│                                         │
└─────────────────────────────────────────┘

Where did that thread come from?

Does iOS create it?

Does Swift create it?

Does UIKit create it?

Does every application process begin with one?

And why did that particular thread become so important that nearly every iOS developer has been told not to block it?

That is the next article:

Does My App Process Provide a Default Thread?

bottom of page