Logo for tanaschita.com

How to avoid retain cycles when working with tasks in Swift

Understanding memory management in Swift concurrency.

08 Dec 2025 · 4 min read

Swift's concurrency system gives us a clean, readable way to express asynchronous work using async/await and tasks. Since we are capturing values when working with tasks, it's important to understand how memory management works here to avoid potencial retain cycles.

In this article, we look at how tasks hold on to references and where retain cycles might appear.

Sponsorship logo
Architecture & Design Patterns for iOS
This book is a practical guide to essential architectural principles and design patterns for iOS development. It covers strategies for building maintainable apps with Swift and SwiftUI, including dependency injection, navigation, common patterns, and modularization.
LEARN MORE

Whenever we create a task, for example through Task {}, Swift captures any values we reference inside the task body. These captures are strong by default.

This means:

  • A task keeps its captured values alive until the task completes.
  • If we capture self strongly, and self also holds a reference to the task, we create a retain cycle.
  • But importantly, this cycle lasts only for the lifetime of the task.

Let's look at a concrete example:

class ImageLoader {
private var task: Task<Void, Never>?
func load() {
task = Task {
await loadImage()
}
}
func loadImage() async {
// ...
}
}

What happens here is:

  • the ImageLoader instance strongly retains the task (because task is a stored property)
  • the task strongly retains its closure,
  • the closure strongly retains self (because we call downloadImages())

So, while the task is running, we do have a retain cycle:

ImageLoaderTaskImageLoader

However, tasks have an important difference from classic stored completion handlers: a task releases its closure once the task finishes.

This means:

  • In this example, the cycle lasts only for as long as the image is loading.
  • When the task completes, it drops its closure and therefore releases self.

If we prefer to avoid even this temporary cycle, we can capture self weakly with [weak self] but for short-lived tasks it is usually optional.

Where retain cycles become memory leaks

Temporary retain cycles are rarely an issue, but with permanent ones we may run into memory leaks. This happens when all three are true:

  1. We store the task (as a property).
  2. The task's closure captures self strongly.
  3. The task never finishes or finishes much later than we expect.

This can appear in long-running loops or stream processing:

class MessageListener {
private var task: Task<Void, Never>?
func start() {
task = Task { [weak self] in
for await message in await messages.stream() {
guard let self else { return }
self.handle(message)
}
}
}
func handle(_ message: Message) { }
}

In this example, the loop may run indefinitely. Without [weak self], the task keeps self alive, and self keeps the task alive, forming a classic retain cycle.

Using a weak capture breaks this cycle and ensures that the task naturally terminates once the listener goes out of scope.

Sponsorship logo
Preparing for a technical iOS job interview
Preparing for a technical iOS Job Interview with over 300 questions & answers. Covering Swift & Objective-C, SwiftUI & UIKit, Combine, HTTP Networking, iOS File System, Core Data, Concurrency with async/await, Security, Automated Testing, Dependency Management, AI & Machine Learning and more.
LEARN MORE
Sponsorship logo
Architecture & Design Patterns for iOS
This book is a practical guide to essential architectural principles and design patterns for iOS development. It covers strategies for building maintainable apps with Swift and SwiftUI, including dependency injection, navigation, common patterns, and modularization.
LEARN MORE
Sponsorship logo
Become a sponsor of tanaschita.com
By publishing an article on different iOS topics every week, tanaschita.com is constantly growing in the developer community and may provide a great audience for you as a sponsor.
CLICK TO LEARN MORE

Newsletter

Image of a reading marmot
Subscribe

Like to support my work?

Say hi

Related tags

Articles with related topics

async/await

concurrency

swift

Understanding Sendable in Swift

Learn what Sendable means in Swift concurrency and how it helps prevent data races.

06 Jul 2026 · 8 min read

Latest articles and tips

© 2026 tanaschita.com

Privacy policy

Impressum