Memory management in Combine
Understand how retain cycles appear and when they turn into memory leaks in Combine.
12 Jan 2026 · 5 min read
Combine allows us to model asynchronous workflows declaratively. Publishers emit values, subscribers react to them, and operators connect everything into a clear pipeline.
What's easy to overlook is that Combine also introduces a specific ownership model. If we don't understand who holds references to whom, objects may live longer than expected, or never be released at all.
Let's look at how memory management works in Combine.

The ownership model in Combine
When we subscribe to a publisher using operators like sink or assign, Combine creates:
- a subscriber
- an AnyCancellable, which represents the subscription's lifetime
A simple rule of thumb is:
- Whoever owns the AnyCancellable owns the subscription.
As long as the cancellable exists, the subscription remains active.
How retain cycles appear in Combine
Let's look at the following example:
var cancellable: AnyCancellable?func startTimer() {cancellable = Timer.publish(every: 1, on: .main, in: .default).autoconnect().sink { _ inself.tick()}}
This creates a retain cycle:
self → cancellable → subscriber → closure → self
An important thing to understand is that a retain cycle not always leads to a memory leak.
Why a retain cycle does not always become a memory leak
A retain cycle is a structural issue: two or more objects hold strong references to each other, forming a cycle. A memory leak is a behavioral problem: an object remains in memory longer than intended and can never be released.
These two concepts are related, but they are not the same. A retain cycle can cause a memory leak, but only if nothing ever breaks the cycle.
For example, if the publisher completes, Combine tears down the subscription, so the closure gets released. This is why retain cycles caused by short-lived subscriptions do not turn into memory leaks.
Examples of short-lived publishers are network requests, Just, Future, finite sequences. In these cases, the retain cycle exists only temporarily and resolves itself automatically.
Long-lived subscriptions are different
Some publishers never complete on their own:
- timers
- NotificationCenter publishers
- subjects
- continuous streams
With these publishers, the retain cycle remains in place until we explicitly break it.
If we never cancel the subscription and capture self strongly, the cycle becomes a real memory leak.
Cancellation breaks the cycle
Explicit cancellation has the same effect as completion.
When we cancel a subscription or release the AnyCancellable, Combine tears down the subscription and releases the subscriber and closure.
func stopTimer() {cancellable = nil}
With this in place the retain cycle still exists while the timer is running. Once cancelled, the cycle is resolved. This is an important distinction: the code does create a retain cycle, but it does not necessarily leak. Of course, we risk a memory leak, for example when forgetting to call stopTimer.
Using weak self
To avoid retain cycles entirely, we can capture self weakly:
.sink { [weak self] _ inself?.tick()}
This breaks the cycle proactively. When self is deallocated, the subscription is cancelled automatically. This approach is especially important for long-lived subscriptions where cancellation is not guaranteed.
In practice:
- If we capture strongly, we must guarantee cancellation
- If cancellation is uncertain, weak captures are safer
In practice, using [weak self] is often the safest default, especially when the lifetime of a subscription is unclear or may change over time.



Newsletter
Like to support my work?
Say hi
Related tags
Articles with related topics
Latest articles and tips



