top of page

01 · INTRODUCTION

What Actually Happens When a Swift Task Suspends?

When a Swift Task suspends, the Task temporarily stops executing without needing to keep a thread blocked while it waits to continue. Its continuation can later become eligible to execute again when the asynchronous operation it is waiting for allows it to proceed.

That definition is technically useful, but it does not yet explain why suspension matters.

To really understand suspension, we need to think about concurrency differently.

Concurrency is not simply about making one operation finish as quickly as possible. It is about allowing multiple pieces of work to make progress together without one unnecessarily preventing the others from progressing.

We already understand this idea at the operating-system level. We would never want iOS to let one running application monopolise the processor until that application had finished everything it wanted to do. The operating system schedules work so that many running applications can continue making progress over the same period of time.

Swift Concurrency asks us to begin thinking about the work inside our own applications with a similar mindset.

💡 The Mental Model

Concurrency is about progress.

Instead of asking how much work one feature can execute right now, begin asking how all of the important work in your application can continue making progress together.

02 · TUTORIAL

Imagine Processing 1,000 Items

Suppose our application has an array containing 1,000 values and we need to perform some computation on every item.

CODE EXAMPLE

for item in items {
    process(item)
}

There is nothing inherently wrong with this code.

The loop is continuously making progress. It processes the first item, then the second, then the third, and continues until all 1,000 items have been processed.

If the computation takes 200 milliseconds, the processor may be doing useful work throughout those entire 200 milliseconds.

But there is another question that matters enormously to an iOS developer:

Does all 1,000-item work actually need to finish right now before anything else is allowed to make progress?

If this synchronous computation occupies the main execution context for 200 milliseconds, the calculation may be progressing beautifully while the application experience is terrible.

The interface may stop responding smoothly.

Animations can miss frames.

Scrolling can become jerky.

The user experiences a frozen application even though the processor itself is extremely busy.

03 · TUTORIAL

Fast Code Can Still Produce a Slow Application

This distinction is extremely useful.

Imagine our 1,000-item computation takes only:

CODE EXAMPLE

0.2 seconds

Two tenths of a second sounds fast.

As a standalone benchmark, we might even be pleased with it.

But 200 milliseconds is a very long period of time when we are trying to maintain a responsive graphical interface.

At 60 frames per second, a new frame opportunity occurs roughly every:

CODE EXAMPLE

16.7 milliseconds

Two hundred milliseconds therefore spans roughly twelve 60 Hz frame intervals.

If our main execution context is monopolised throughout that period, the user does not think:

CODE EXAMPLE

Excellent.

That algorithm completed
in only 0.2 seconds.

They see the interface stop moving.

This gives us a much more useful definition of performance.

💡 Important Idea

The goal is not always to finish one piece of work in the shortest possible wall-clock time.

For interactive software, it is often more important that the application as a whole remains responsive while useful work continues to make progress.

04 · TUTORIAL

Think in Frames of Animation

iOS applications are visual, interactive programs.

Our users scroll lists, tap buttons, drag controls, watch transitions and expect animations to remain smooth while the application is doing other work.

This means it can be useful to think about our application over time.

EXECUTION DIAGRAM

TIME ─────────────────────────────────────────────▶

Frame        Frame        Frame        Frame
  │            │            │            │
  ▼            ▼            ▼            ▼

UI work      UI work      UI work      UI work
██           ██           ██           ██

Other work
   ███          ███          ███          ███

This diagram is not intended to describe the exact internal scheduling algorithm used by Swift or iOS.

It is a mental model for responsiveness.

We want our application to continue presenting frames, responding to the user and progressing other important work rather than allowing one long-running operation to monopolise the execution context required by everything else.

05 · TUTORIAL

Perhaps We Do Not Need to Finish Everything Right Now

Return to our 1,000 items.

Our first mental model was:

EXECUTION DIAGRAM

Process all 1,000 items

████████████████████████████████████████

DONE

The operation begins and attempts to finish all of its synchronous work before returning.

But imagine that our application did not actually require all 1,000 items to be processed during one uninterrupted period of execution.

Conceptually, the work could instead make progress in smaller units:

EXECUTION DIAGRAM

Items 1–100

████

Other eligible work

    ██

Items 101–200

      ████

Other eligible work

          ██

Items 201–300

            ████

Other eligible work

                ██

The total computation may still require roughly the same amount of useful CPU work.

It might even finish slightly later in wall-clock time.

But the application can remain responsive because the computation is no longer imagined as the only work that deserves to make progress.

This is the beginning of the cooperative mental model.

06 · TUTORIAL

We Already Use This Idea Across Running Applications

This should feel familiar because we have spent this entire article series building the same idea from the operating system upward.

Imagine an operating system without concurrency.

EXECUTION DIAGRAM

Photos

████████████████████████

FINISH EVERYTHING


Messages

                        ████████████████

FINISH EVERYTHING


Safari

                                        ███████████████

That would be an awful multitasking system.

We do not want Photos to finish everything it could possibly calculate before Messages receives an opportunity to execute.

Instead, processor resources are shared over time.

EXECUTION DIAGRAM

TIME ─────────────────────────────────────────────▶

Photos

████        ████                ████

Messages

    ████            ████

Safari

        ████                ████

Each application can make progress over the same period of time.

That is concurrency.

07 · TUTORIAL

Now Bring That Mental Model Inside Our App

Our application itself contains many pieces of work.

CODE EXAMPLE

Render UI

Load Launches

Decode JSON

Save Favourites

Load Images

Respond to User Input

Refresh Account

Update Animations

Why should one of those features assume that once it begins executing, it deserves to finish absolutely everything before other important work can progress?

This is where our mental model begins to change.

Instead of thinking:

CODE EXAMPLE

I have execution now.

Finish everything.

we begin thinking:

CODE EXAMPLE

I have work to perform.

How can that work make progress
while the rest of the application
also continues making progress?

That is a much more useful way to approach modern concurrent application design.

08 · TUTORIAL

Could We Chop Our Work Into Smaller Pieces?

Conceptually, yes.

If a large computation does not need to happen atomically as one uninterrupted operation, we can sometimes design it as smaller units of work and provide opportunities for other eligible work to execute between those units.

Instead of:

EXECUTION DIAGRAM

Task A

████████████████████████████████████████

we can aim for a system that behaves more like:

EXECUTION DIAGRAM

Task A

████        ████        ████

Task B

    ████            ████

Task C

            ████            ████

Now several pieces of logical application work are making progress over the same period.

This is the same powerful idea that made operating-system multitasking possible, applied at a different abstraction layer.

09 · TUTORIAL

But Swift Does Not Automatically Chop Up Your Code

This is where we must be precise.

Swift Concurrency does not take arbitrary synchronous code such as:

CODE EXAMPLE

for item in items {
    process(item)
}

and secretly transform it into:

CODE EXAMPLE

Process 100 items

Stop

Run another Task

Process another 100 items

Stop

Run another Task

Continue later

That is not what cooperative Swift concurrency means.

If our synchronous loop takes 200 milliseconds, it can continue executing synchronously for those 200 milliseconds.

Swift does not automatically decide that we have used enough execution time and preempt the Task at some convenient Swift source-code line.

💡 This Is Important

Swift Concurrency does not automatically interrupt long-running synchronous code and divide it into tiny concurrent chunks.

Synchronous work remains synchronous work. We have to architect our application so that long-running work does not unnecessarily monopolise execution resources needed by other important work.

10 · TUTORIAL

This Is Where the Word Cooperative Matters

The system is called cooperative for a reason.

Our code participates in the execution model.

Swift provides mechanisms through which asynchronous work can suspend and later continue. It provides Tasks, executors, structured concurrency and actor isolation.

But it does not magically turn every synchronous algorithm into perfectly interleaved concurrent work.

We still need to understand what our code is doing.

We still need to identify expensive synchronous work.

We still need to decide where that work belongs.

And when appropriate, we may need to structure large computations into meaningful units that allow other work to make progress.

11 · TUTORIAL

Now We Can Understand Suspension

With this mental model in place, suspension becomes much easier to understand.

Imagine our Task begins an asynchronous network operation:

CODE EXAMPLE

let launches = try await api.loadLaunches()

Our Task begins executing.

Eventually it reaches the asynchronous call.

The result may not yet be available.

Perhaps we have sent a network request and the response will arrive later.

At this point, keeping the current Task continuously executing cannot magically make the remote server respond sooner.

More importantly, we do not need to keep a system thread synchronously blocked simply to represent the fact that this Task has unfinished work.

The Task can suspend.

12 · TUTORIAL

A Suspended Task Is Still an Unfinished Task

Consider:

CODE EXAMPLE

func loadLaunch() async throws -> Launch {

    let data = try await api.loadLaunchData()

    let launch = try decoder.decode(
        Launch.self,
        from: data
    )

    return launch
}

If suspension occurs at the await, the function has not finished.

The Task has not disappeared.

The code after the asynchronous call still needs to execute.

EXECUTION DIAGRAM

Begin loadLaunch()

        │
        ▼

Start loadLaunchData()

        │
        ▼

      await

        │
        ▼

     SUSPEND

        ⋮

        ⋮

Data becomes available

        │
        ▼

Continuation becomes eligible

        │
        ▼

Decode Launch

        │
        ▼

Return Launch

The Task's lifetime continues across that period of suspension.

13 · TUTORIAL

Suspension Is Not the Same as Blocking

This is one of the most useful distinctions in Swift Concurrency.

Imagine synchronous waiting.

EXECUTION DIAGRAM

THREAD

Begin work
    │
    ▼

WAIT WAIT WAIT WAIT WAIT WAIT WAIT

██████████████████████████████████

    │
    ▼

Continue

The thread is tied up in that synchronous wait.

Now compare that with Task suspension:

EXECUTION DIAGRAM

TASK A

████████
       │
       ▼
    SUSPEND

       ⋮

       ⋮

       │
       ▼
    CONTINUE

The Task is unfinished, but the system thread that happened to be executing its current work does not need to remain permanently attached to it while it is suspended.

That execution resource can potentially be used for other eligible work.

14 · TUTORIAL

The Task and the Thread Are Different Things

This is why the distinction between Tasks and threads is so important.

A Task represents logical asynchronous work.

A thread is an operating-system execution context through which instructions can ultimately reach a processor core.

The relationship is not:

CODE EXAMPLE

Task A = Thread 1

Task B = Thread 2

Task C = Thread 3

A better mental model is:

EXECUTION DIAGRAM

Task A ──┐
Task B ──┤
Task C ──┤
Task D ──┤
Task E ──┘
         │
         ▼
   Eligible Jobs
         │
         ▼
     Executors
         │
         ▼
 System Threads
         │
         ▼
   OS Scheduler
         │
         ▼
     CPU Cores

A Task can therefore have a much longer lifetime than any one uninterrupted period of thread execution.

15 · TUTORIAL

await Marks a Potential Suspension Point

Consider:

CODE EXAMPLE

let launches = try await api.loadLaunches()

The await is important because it tells us that execution may suspend while performing this asynchronous call.

But we need to be careful with the language we use.

await does not mean:

CODE EXAMPLE

Create another thread.

It does not mean:

CODE EXAMPLE

Move this code to the background.

And it does not mean:

CODE EXAMPLE

The Task definitely suspends here.

It marks a point at which suspension is possible.

💡 await

await identifies a potential suspension point in an asynchronous function.

It should make us aware that when execution continues afterward, time may have passed and other permitted concurrent work may have progressed.

16 · TUTORIAL

Think of Suspension as a Save Point

A useful mental model is a save point in a game.

Imagine our Rocket Launch Task has progressed through several stages:

CODE EXAMPLE

ROCKET LAUNCH TASK

✓ Build Request
✓ Start Download

       🚀

  [ SAVE POINT ]

○ Receive Data
○ Decode Launches
○ Update Model
○ Finish

We do not need to keep the character actively moving simply because the game has not finished.

We can preserve enough information to continue later.

A suspended Swift Task follows a loosely similar conceptual idea.

Its asynchronous lifetime continues, but it does not need to continuously occupy a thread while waiting to become eligible to continue.

17 · TUTORIAL

The Thread Can Now Perform Other Work

Suppose Task A was executing using a system thread.

EXECUTION DIAGRAM

Task A
   │
   ▼
Thread
   │
   ▼
CPU Core

Task A reaches a suspension point and actually suspends.

We no longer need:

EXECUTION DIAGRAM

Task A
   │
   ▼
Thread
   │
   ▼
WAITING
   │
   ▼
WAITING
   │
   ▼
WAITING

Instead, Task A can remain suspended while the execution resource becomes available for other eligible work.

EXECUTION DIAGRAM

Task A

SUSPENDED


Task B
   │
   ▼
Thread
   │
   ▼
CPU Core

This is how large numbers of asynchronous operations can exist without requiring one permanently dedicated thread for every unfinished operation.

18 · TUTORIAL

Now Return to Our Frames

This brings us back to the application experience.

Imagine several pieces of asynchronous work:

EXECUTION DIAGRAM

TIME ─────────────────────────────────────────────▶

Task A

████        SUSPENDED            ████


Task B

    ████              ████


Task C

            ████              ████


UI work

  ██      ██      ██      ██      ██

Again, this is a conceptual diagram rather than an exact representation of Swift's scheduler.

But it shows the design goal beautifully.

Our application contains many things that need to make progress.

Some are executing.

Some are waiting.

Some are suspended.

Some become eligible again.

The goal is not to let one piece of work unnecessarily dominate the entire application.

19 · TUTORIAL

What About CPU-Heavy Work?

This is where our original 1,000-item loop becomes important again.

CODE EXAMPLE

for item in items {
    process(item)
}

This loop does not suspend simply because it takes a long time.

It may continue executing synchronously until all 1,000 items have been processed.

If that work belongs somewhere that blocks important progress elsewhere in the application, we have an architectural problem to solve.

Depending on the work, one possible design is to divide a large computation into meaningful batches and deliberately provide opportunities for other eligible work to run.

Conceptually:

EXECUTION DIAGRAM

Batch 1

████

Allow other eligible work to progress

    ██

Batch 2

      ████

Allow other eligible work to progress

          ██

Batch 3

            ████

Swift provides mechanisms such as Task.yield() that can be useful in particular cooperative workloads.

But this should not be turned into another rule to memorise such as “put Task.yield() inside every loop.”

The important lesson is architectural.

Break long-running work into meaningful units when the entire operation does not need to execute atomically, and provide opportunities for other eligible work to make progress when appropriate.

20 · TUTORIAL

Async Does Not Automatically Make CPU Work Cooperative

Consider:

CODE EXAMPLE

@MainActor
func calculateEverything() async {

    for item in items {
        performExpensiveCalculation(item)
    }
}

There is an async keyword in the function declaration.

That does not mean Swift automatically pauses the loop every few milliseconds.

It does not mean the computation automatically moves away from the MainActor.

It does not mean the UI is guaranteed to remain responsive.

If that synchronous work occupies the MainActor's execution context for too long, our interface can still suffer.

Async gives a function the ability to participate in asynchronous execution and suspension. It does not transform every synchronous instruction inside that function into automatically interleaved work.

21 · TUTORIAL

This Is Why Async Does Not Mean Background

We can now understand another phrase that developers frequently memorise:

CODE EXAMPLE

async does not mean background

Consider:

CODE EXAMPLE

@MainActor
final class LaunchModel {

    var launches: [Launch] = []

    func refresh() async throws {

        launches = try await api.loadLaunches()
    }
}

The method is MainActor isolated.

It is also asynchronous.

Those facts are not contradictory.

The function can begin executing in the MainActor's isolation domain, reach the asynchronous operation and potentially suspend.

While suspended, it does not need to synchronously block the main thread merely because its Task has not finished.

Later, its continuation can become eligible to execute again while respecting the MainActor's isolation requirements.

22 · TUTORIAL

This Is Exactly What We Want for UI Code

Think again about our animation frames.

We do not want:

EXECUTION DIAGRAM

MAIN EXECUTION

Start refresh()

████████████████████████████████████████

Network response finally arrives

Update UI

We want the asynchronous lifetime of the operation to exist without requiring the main execution context to remain blocked throughout the wait.

EXECUTION DIAGRAM

Start refresh()

████
   │
   ▼
 await
   │
   ▼
SUSPEND


UI continues

    ██    ██    ██    ██    ██


Network result becomes available

                              │
                              ▼

                         Continue Task

                              ███

                         Update model

This is why suspension is so valuable for interactive software.

23 · TUTORIAL

Suspension Also Changes Our Assumptions About State

There is another consequence that becomes very important when we begin studying actors.

Consider:

CODE EXAMPLE

actor LaunchStore {

    var launches: [Launch] = []

    func refresh() async throws {

        let newLaunches = try await api.loadLaunches()

        launches = newLaunches
    }
}

The actor method can reach the await and suspend.

While that operation is suspended, other eligible work for the actor may execute.

That means an actor method should not always assume that every piece of actor state remains exactly as it was before an await.

EXECUTION DIAGRAM

Actor Method

Read State
    │
    ▼
  await
    │
    ▼
 SUSPEND

    │
    │
    │ Other eligible actor work
    │ may execute
    │
    ▼

 RESUME

    │
    ▼
Continue

This is known as actor reentrancy.

We will study it properly later, but suspension is the concept that makes it possible to understand.

24 · TUTORIAL

Swift Concurrency Adds Another Layer of Concurrency

We can now connect everything we have learned.

The operating system already provides concurrency between threads.

EXECUTION DIAGRAM

Thread A

████        ████

Thread B

    ████        ████

Swift Concurrency introduces a higher-level model for coordinating logical application work.

EXECUTION DIAGRAM

Task A

████    SUSPEND             ████

Task B

    ████        ████

Task C

            ████        ████

                │
                ▼

            EXECUTORS

                │
                ▼

          SYSTEM THREADS

                │
                ▼

           OS SCHEDULER

                │
                ▼

             CPU CORES

The lower-level machinery still exists.

Processes still exist.

Threads still exist.

The OS scheduler still exists.

CPU cores still execute machine instructions.

Swift Concurrency gives us another layer through which the logical work inside our application can be structured and coordinated.

25 · TUTORIAL

The Big Change Is How We Think About Progress

Perhaps the most useful thing to take away from suspension is not the implementation detail.

It is the change in how we think about our application.

Our old instinct might be:

CODE EXAMPLE

I started this work.

I should finish all of it
as quickly as possible.

Our concurrent instinct should increasingly become:

CODE EXAMPLE

This work needs to make progress.

What other work also needs
to make progress?

How can the system remain responsive
while all of it progresses?

That is a much more useful question for an iOS developer.

26 · TUTORIAL

What to Remember

💡 What to Remember

Concurrency is about allowing multiple pieces of work to make progress over the same period of time.

Finishing one operation in the shortest possible time is not always the most important measure of application performance.

A 200 millisecond synchronous computation may sound fast, but if it monopolises the main execution context it can span roughly twelve 60 Hz frame intervals and produce a visibly unresponsive interface.

Large computations do not always need to execute as one uninterrupted block of work. When appropriate, work can be architected into meaningful units that allow other eligible work to progress.

Swift Concurrency does not automatically chop arbitrary synchronous code into tiny pieces.

A long synchronous loop remains synchronous unless the architecture introduces appropriate concurrency or suspension opportunities.

await marks a potential suspension point. It does not mean “create a thread,” “move to the background,” or “definitely suspend.”

When a Swift Task actually suspends, the Task remains unfinished but temporarily stops executing.

The Task does not need to keep the system thread that was executing it permanently blocked while it waits.

When the awaited operation allows progress to continue, the Task's continuation can later become eligible for execution again through the appropriate executor.

A Task does not permanently own one particular system thread.

Async code is not automatically background code, and CPU-heavy synchronous work does not become cooperative merely because it exists inside an async function.

The modern mental model is:

EXECUTION DIAGRAM

Many pieces of application work
             ↓
      Make progress together
             ↓
     Suspend when appropriate
             ↓
 Allow other eligible work to run
             ↓
      Continue when ready
             ↓
 Keep the application responsive

27 · TUTORIAL

Your Next Move

We now have a much stronger picture of what Swift means by cooperative concurrency.

Our goal is not simply to create more threads.

Our goal is not to move everything away from the main thread.

And our goal is not to execute as much code as physically possible every time one feature gets an opportunity to run.

Our application contains many pieces of work that all need opportunities to make progress.

Swift Tasks give us a way to describe that asynchronous work.

Suspension allows a Task to remain unfinished without requiring continuous thread occupation.

And when a suspended Task becomes ready to continue, something has to decide how that eligible Swift work should execute.

That brings us to one of the most important pieces of the Swift Concurrency Runtime:

the executor.

What exactly is an executor?

Is it a thread?

Is it a queue?

Does every Task have one?

Does every actor have one?

And how does an executor eventually connect our Swift code to the system threads that are scheduled onto the processor?

That is the next article:

What Is an Executor in Swift Concurrency?

bottom of page