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. citeturn143415search9
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?
