01 · INTRODUCTION
Suspension vs Blocking in Swift
The short answer
Blocking keeps a thread occupied while work waits or executes. Suspension pauses a Swift task without requiring that task to keep occupying its current thread.
Both can make one operation wait. The difference is what happens to the execution resource underneath it.
CODE EXAMPLE
BLOCKING
Task waits
Thread waits
SUSPENSION
Task waits
Thread can execute other eligible work
This distinction is one of the foundations of Swift Concurrency.
💡 Important Terminology
Blocking = preventing a thread from progressing to other work until the current synchronous operation returns.
Suspension = pausing a task at a potential suspension point so it can continue later.
Waiting = the broad condition in which an operation cannot yet continue. Waiting may be implemented by blocking or suspension.
02 · TUTORIAL
Begin With a Synchronous Wait
Consider a function that deliberately sleeps its current thread:
CODE EXAMPLE
func prepareLaunch() {
print("Preparing")
Thread.sleep(forTimeInterval: 2)
print("Ready")
}
The thread enters Thread.sleep and remains unavailable until the sleep finishes.
EXECUTION DIAGRAM
BLOCKED THREAD
TIME
│
▼
print("Preparing")
████
Thread.sleep(forTimeInterval: 2)
████████████████████████████
print("Ready")
████
The processor does not need to spend two seconds repeatedly executing this sleeping thread.
However, the thread remains tied to the synchronous call. It cannot use that time to run another task.
03 · TUTORIAL
Blocking the Main Thread Blocks the Interface
If the function executes on the main thread, important UI-dependent work must wait for the same execution stream.
CODE EXAMPLE
@MainActor
func refresh() {
status = "Loading"
Thread.sleep(forTimeInterval: 2)
status = "Finished"
}
EXECUTION DIAGRAM
MAIN THREAD
status = "Loading"
│
▼
████████ BLOCKED FOR TWO SECONDS ████████
│
│ touches, animations and other
│ main-thread-dependent work wait
▼
status = "Finished"
The state changed to "Loading", but the frameworks may not receive a suitable opportunity to present that change before the thread becomes blocked.
The user can experience a frozen interface followed by the final state.
The problem is not merely that the operation took two seconds. The problem is that it occupied an execution resource needed by other work for those two seconds.
04 · TUTORIAL
Now Suspend the Task Instead
Swift provides an asynchronous sleep that suspends the current task:
CODE EXAMPLE
@MainActor
func refresh() async throws {
status = "Loading"
try await Task.sleep(for: .seconds(2))
status = "Finished"
}
If the task must wait, it can suspend at await.
EXECUTION DIAGRAM
MAIN-ACTOR TASK
status = "Loading"
│
▼
Task.sleep
│
▼
TASK SUSPENDED FOR TWO SECONDS
│
│ main execution domain can run
│ other eligible work
▼
Task becomes ready
│
▼
status = "Finished"
The task still waits for two seconds.
The difference is that it does not reserve the main thread merely to preserve that wait.
💡 The Central Distinction
Blocking makes the thread wait with the operation.
Suspension lets the task wait without keeping its thread.
05 · TUTORIAL
A Network Request Is a Natural Suspension
Network code spends much of its lifetime waiting for systems outside the application.
CODE EXAMPLE
func loadRocketLaunches() async throws -> [Launch] {
let (data, _) = try await URLSession.shared.data(
from: launchesURL
)
return try JSONDecoder().decode([Launch].self, from: data)
}
While the network operation is incomplete, there are no response bytes for the task to decode.
Blocking a thread throughout that wait would not make the server reply sooner.
EXECUTION DIAGRAM
LOAD LAUNCHES TASK
Start request
████
│
▼
Suspended while the network operation proceeds
─────────────────────────────────────
│
▼
Decode response
███████
Suspension separates the logical lifetime of the request from the much shorter periods during which its Swift instructions actually need to execute.
06 · TUTORIAL
Blocking Work and CPU Work Are Not Identical
A sleeping thread is blocked because it cannot execute other work even though it is not performing useful computation.
A large calculation also occupies its thread, but for a different reason: the processor is actively executing its instructions.
CODE EXAMPLE
func calculateLaunchWindows() -> Int {
var result = 0
for value in 1...500_000_000 {
result &+= value
}
return result
}
Developers often describe both situations as “blocking the main thread” because their visible consequence is the same: other main-thread work cannot execute until the synchronous function returns.
CODE EXAMPLE
THREAD SLEEP
Thread occupied by a synchronous wait
Processor may run other threads
CPU CALCULATION
Thread occupied by executing instructions
Processor performs the calculation
SAME UI CONSEQUENCE ON MAIN THREAD
Other main-thread-dependent work is delayed
The solution for a wait is often suspension.
The solution for necessary CPU work is to keep it away from latency-sensitive execution domains and design its isolation carefully. Suspension does not perform the calculation for us.
07 · TUTORIAL
async Code Can Still Block
Adding async to a declaration does not convert synchronous work into suspension.
This is a deliberately bad example:
CODE EXAMPLE
@MainActor
func calculateLaunchWindows() async -> Int {
var result = 0
for value in 1...500_000_000 {
result &+= value
}
return result
}
The loop has no potential suspension point.
Once the main actor begins executing it, the task continues synchronously until the loop finishes.
EXECUTION DIAGRAM
ASYNC FUNCTION WITHOUT SUSPENSION
Enter function
│
▼
████████████ synchronous CPU work ████████████
│
▼
Return result
async means the function is permitted to participate in asynchronous calls. It does not make every instruction cooperative, interruptible or inexpensive.
08 · TUTORIAL
Task {} Does Not Automatically Remove Blocking
Wrapping synchronous work in a task can leave it on the same actor:
CODE EXAMPLE
@MainActor
func beginCalculation() {
Task {
let result = calculateLaunchWindows()
launchWindow = result
}
}
The task inherits the surrounding main-actor isolation.
The synchronous calculation still belongs to the main execution domain and can still delay the interface.
EXECUTION DIAGRAM
MAIN-ACTOR METHOD
│
▼
Create Task {}
│
▼
Task inherits MainActor
│
▼
Synchronous function occupies
the main execution domain
A Task is not automatically a background thread.
This is why understanding execution context matters more than surrounding a function with concurrency syntax.
09 · TUTORIAL
Moving Blocking Work Does Not Turn It Into Suspension
Older code often protects the main thread by sending a blocking operation to a worker queue:
CODE EXAMPLE
DispatchQueue.global().async {
let launches = loadLaunchesSynchronously()
DispatchQueue.main.async {
self.launches = launches
}
}
This can keep the main thread responsive.
However, loadLaunchesSynchronously() still occupies a worker thread until it returns.
CODE EXAMPLE
BEFORE
Main thread blocked
Main-thread work delayed
AFTER MOVING THE BLOCKER
Worker thread blocked
Main thread remains available
Moving a blocking operation can be a necessary compatibility technique when an API offers only a synchronous interface.
It changes which thread pays the cost. It does not change the blocking operation into a suspended task.
10 · TUTORIAL
Suspension Allows Interleaving
When one task suspends, another eligible task can use the released execution opportunity.
CODE EXAMPLE
Task { @MainActor in
print("Task A: waiting")
try await Task.sleep(for: .seconds(2))
print("Task A: finished")
}
Task { @MainActor in
print("Task B: updates the interface")
}
A possible output is:
CODE EXAMPLE
Task A: waiting
Task B: updates the interface
Task A: finished
EXECUTION DIAGRAM
MAIN ACTOR
Task A runs
██████
│
▼ suspends
Task B runs
█████████████
Task A continues later
███████
The tasks are concurrent because both lifetimes exist during the same period.
They do not execute simultaneously on the main actor. They take turns as suspension makes another task eligible to progress.
11 · TUTORIAL
Suspension Is Cooperative
Swift Concurrency cannot interrupt arbitrary synchronous code merely because other tasks would like to run.
CODE EXAMPLE
@MainActor
func monopoliseTheMainActor() {
for value in 1...500_000_000 {
performStep(value)
}
}
This function contains no asynchronous call and no suspension point.
The current job continues until the function returns.
EXECUTION DIAGRAM
COOPERATIVE PROGRESS
Task runs synchronous instructions
│
├── completes its current work
│
└── or reaches a suspension point
│
▼
another eligible task can run
The scheduler can select among eligible jobs. It cannot make badly placed synchronous work disappear.
💡 Important
Swift Concurrency is cooperative.
Tasks must reach genuine suspension points or finish their synchronous work promptly before the executor can make progress on other jobs.
12 · TUTORIAL
Suspension Does Not Guarantee Immediate UI Rendering
Suspending a main-actor task makes the main execution domain available for other eligible work.
It does not directly command SwiftUI or UIKit to render a frame.
CODE EXAMPLE
@MainActor
func refresh() async throws {
status = "Loading"
try await Task.sleep(for: .seconds(2))
status = "Finished"
}
The suspension gives event handling and UI coordination an opportunity to progress.
Exactly when a visible frame is prepared and presented remains the responsibility of the UI frameworks and display system.
The safe claim is:
💡 Reality Check
Suspension releases the execution opportunity.
It does not guarantee that a particular piece of work or a particular UI frame will execute next.
13 · TUTORIAL
A Complete Suspended Network Feature
The API uses an asynchronous network operation rather than a blocking request:
CODE EXAMPLE
struct LaunchAPI {
func loadLaunches() async throws -> [Launch] {
let (data, _) = try await URLSession.shared.data(
from: launchesURL
)
return try JSONDecoder().decode(
[Launch].self,
from: data
)
}
}
The main-actor feature can await it:
CODE EXAMPLE
@MainActor
final class LaunchListFeature: ObservableObject {
@Published private(set) var launches: [Launch] = []
@Published private(set) var isLoading = false
@Published private(set) var errorMessage: String?
private let api: LaunchAPI
init(api: LaunchAPI) {
self.api = api
}
func refresh() async {
isLoading = true
errorMessage = nil
defer { isLoading = false }
do {
launches = try await api.loadLaunches()
} catch {
errorMessage = error.localizedDescription
}
}
}
The feature changes its loading state synchronously on the main actor.
When the network operation must wait, the task can suspend. The main actor is then available for other eligible interface work.
When the response is ready, the task becomes eligible to continue through the main actor and update the feature state.
EXECUTION DIAGRAM
REFRESH TASK
Main actor: isLoading = true
│
▼
Await URLSession
│
▼
Task suspended
│
│ main actor remains available
▼
Response ready
│
▼
Main actor: update launches
│
▼
Main actor: isLoading = false
The operation still takes time.
Swift Concurrency changes how the application occupies its limited execution resources during that time.
14 · TUTORIAL
The Complete Mental Model
EXECUTION DIAGRAM
BLOCKING
Task cannot continue
│
▼
Thread remains occupied
│
▼
Other work needing that thread waits
SUSPENSION
Task cannot continue yet
│
▼
Task preserves its continuation
│
▼
Thread becomes available
│
▼
Other eligible work can execute
│
▼
Original task continues when ready
Blocking and suspension can both describe an operation that has not yet completed.
Blocking ties up the current thread until synchronous work returns.
Suspension preserves the task's logical lifetime while releasing its current execution resource.
This distinction allows many Swift tasks to remain in progress without requiring one blocked thread for every waiting operation.
That is the difference between suspension and blocking.
15 · TUTORIAL
What to Remember
💡 What to Remember
1. Blocking keeps a thread occupied until synchronous work returns.
2. Suspension pauses a task without requiring it to keep its current thread.
3. Waiting is not automatically blocking; an asynchronous wait can suspend.
4. Blocking the main thread delays important UI-dependent work.
5. CPU work and synchronous waiting occupy a thread for different reasons.
6. Declaring a function async does not convert its synchronous work into suspension.
7. Task {} does not automatically move blocking work away from the main actor.
8. Moving blocking work to another thread protects the caller but does not make the operation nonblocking.
9. Suspension allows other eligible tasks to make progress through cooperative scheduling.
10. Suspension creates an opportunity for UI work; it does not command a render.
16 · TUTORIAL
Frequently Asked Questions
What is the difference between suspension and blocking?
Blocking keeps the current thread occupied until an operation returns. Suspension pauses the task and allows its current thread to execute other eligible work until the task can continue.
Does await block the thread?
When an awaited asynchronous operation genuinely suspends, the task does not need to retain its current thread. A poorly implemented asynchronous function can still call blocking synchronous code before or after an await.
Does async prevent blocking?
No. An async function can contain long loops, synchronous file operations, locks or other blocking calls. Only genuine suspension releases the task's current execution resource.
Is Thread.sleep the same as Task.sleep?
No. Thread.sleep blocks the current thread. Task.sleep is asynchronous and suspends the current task, allowing the underlying execution resource to be used elsewhere.
Why does blocking the main thread freeze an app?
Important event handling and UI coordination depend on the main execution domain. If synchronous work occupies it for too long, those operations begin later and the interface appears unresponsive.
Does moving work to a background queue make it nonblocking?
No. It may stop the work from blocking the main thread, but the synchronous operation still occupies whichever worker thread executes it.
17 · TUTORIAL
Continue Learning Swift Concurrency
Read What Does await Mean in Swift? to revisit the potential suspension point that makes nonblocking waits possible.
Continue with What Is Cooperative Scheduling in Swift? to understand how executors make progress among eligible task jobs and why long synchronous work reduces that cooperation.
Apple's Understanding Hangs in Your App documentation connects unavailable main-thread time to visible application hangs.
18 · TUTORIAL
Download Xcode Playground
The accompanying Suspension vs Blocking in Swift Xcode playground can make both forms of waiting visible.
It should compare Thread.sleep with Task.sleep, run each on the main actor, observe a second task attempting to make progress and place a long synchronous loop inside an async function.
A final page should compare a blocking launch loader moved to a worker queue with a suspended URLSession implementation. Both can protect the main thread, but only one releases its thread while waiting.
The article explains the difference. The playground will make the occupied thread impossible to miss.
