top of page

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.

bottom of page