Hot and Cold Publishers in Swift Combine

Published: (December 23, 2025 at 11:57 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

What Are Hot and Cold Publishers?

Cold Publishers

A cold publisher creates a new execution for each subscriber. The work starts fresh when you subscribe.

let coldPublisher = Deferred {
    Future { promise in
        print("Making API call")
        // Simulate API call
        promise(.success(Int.random(in: 1...100)))
    }
}

coldPublisher.sink { print("Subscriber 1: \($0)") }
// Output: "Making API call", "Subscriber 1: 42"

coldPublisher.sink { print("Subscriber 2: \($0)") }
// Output: "Making API call", "Subscriber 2: 87"

Each subscriber triggers a separate API call.

Common cold publishers

  • Just
  • Fail
  • Deferred
  • URLSession.dataTaskPublisher
  • Future (somewhat cold‑like)

Hot Publishers

A hot publisher shares a single execution among all subscribers. The work happens independently of subscribers, and each subscriber receives events from the point it subscribes onward.

let hotPublisher = PassthroughSubject<Int, Never>()

hotPublisher.sink { print("Subscriber 1: \($0)") }

hotPublisher.send(1)
// Output: "Subscriber 1: 1"

hotPublisher.sink { print("Subscriber 2: \($0)") }

hotPublisher.send(2)
// Output: "Subscriber 1: 2"
// Output: "Subscriber 2: 2"

Both subscribers receive the same value from a single emission.

Common hot publishers

  • PassthroughSubject
  • CurrentValueSubject
  • Timer.publish
  • NotificationCenter.publisher

Why This Matters

1. Resource Management

Cold publishers can waste resources:

let apiCall = URLSession.shared.dataTaskPublisher(for: url)

apiCall.sink { print("UI: \($0)") }
apiCall.sink { print("Cache: \($0)") }

The code above makes two separate network requests. In many cases you want one request shared between subscribers.

2. State Consistency

Hot publishers ensure all subscribers see the same data:

let userState = CurrentValueSubject<User?, Never>(nil)

// All parts of your app observe the same user state
userState.sink { user in updateUI(user) }
userState.sink { user in syncToDatabase(user) }

3. Timing Issues

Cold publishers can cause race conditions if you expect shared execution:

let timestamp = Just(Date())

timestamp.sink { print("Time 1: \($0)") }
Thread.sleep(forTimeInterval: 1)
timestamp.sink { print("Time 2: \($0)") }

Each subscriber gets a different timestamp because Just creates a new value per subscription.

When to Use Which

Use Cold Publishers When

You want independent executions per subscriber

func fetchUserData(id: String) -> AnyPublisher<User, Error> {
    URLSession.shared.dataTaskPublisher(for: makeURL(id))
        .map(\.data)
        .decode(type: User.self, decoder: JSONDecoder())
        .eraseToAnyPublisher()
}

// Each call makes its own request
fetchUserData(id: "123").sink {  }
fetchUserData(id: "456").sink {  }

You’re creating reusable publisher factories

func createTimer(interval: TimeInterval) -> AnyPublisher<Date, Never> {
    Deferred {
        Timer.publish(every: interval, on: .main, in: .common)
            .autoconnect()
    }
    .eraseToAnyPublisher()
}

You want lazy evaluation – work only happens when someone actually subscribes.

Use Hot Publishers When

You need to broadcast events to multiple observers

class LocationManager {
    let locationPublisher = PassthroughSubject<CLLocation, Never>()

    func locationManager(_ manager: CLLocationManager,
                         didUpdateLocations locations: [CLLocation]) {
        locations.forEach { locationPublisher.send($0) }
    }
}

You’re managing shared state

class AppState {
    let isLoggedIn = CurrentValueSubject<Bool, Never>(false)
    let currentUser = CurrentValueSubject<User?, Never>(nil)
}

You need to share expensive operations

let sharedAPICall = apiPublisher
    .share()
    .eraseToAnyPublisher()

// Multiple subscribers, single network request
sharedAPICall.sink { updateUI($0) }
sharedAPICall.sink { cacheData($0) }

Converting Between Hot and Cold

Cold → Hot: Use .share()

let coldPublisher = URLSession.shared.dataTaskPublisher(for: url)
let hotPublisher = coldPublisher.share()

// Now multiple subscribers share one request
hotPublisher.sink { print("Subscriber 1: \($0)") }
hotPublisher.sink { print("Subscriber 2: \($0)") }

Hot → Cold: Use Deferred

let hotPublisher = PassthroughSubject<Int, Never>()

let coldPublisher = Deferred { hotPublisher }

// Each subscription creates a new relationship to the subject

Converting hot to cold is rarely needed in practice.

Common Pitfalls

Pitfall 1: Unintentional Multiple Executions

// Bad: Makes 3 separate API calls
let apiCall = URLSession.shared.dataTaskPublisher(for: url)
apiCall.sink { handleResponse($0) }
apiCall.sink { cacheResponse($0) }
apiCall.sink { logResponse($0) }

// Good: Makes 1 API call, shared among subscribers
let sharedCall = apiCall.share()
sharedCall.sink { handleResponse($0) }
sharedCall.sink { cacheResponse($0) }
sharedCall.sink { logResponse($0) }

Pitfall 2: Missing Subscription Side‑Effects

If you create a hot publisher but never subscribe, the work never starts:

let timer = Timer.publish(every: 1.0, on: .main, in: .common)
// No `.autoconnect()` or `sink` → timer never fires

Always ensure a hot publisher has at least one active subscriber (or call .autoconnect() for Timer.publish).

Pitfall 3: Memory Leaks with .share()

let shared = expensivePublisher.share()

// If no subscribers exist, .share() can keep resources alive
// Consider using .share(replay: 0) or managing lifecycle explicitly

Missing Events with Hot Publishers

let subject = PassthroughSubject<String, Never>()

subject.send("Event 1") // Lost, no subscribers yet

subject.sink { print($0) } // Subscribes now

subject.send("Event 2") // Received

If you need late subscribers to receive values, use CurrentValueSubject or replay operators.

Quick Reference

Cold Publisher Characteristics

  • New execution per subscriber
  • Work starts on subscription
  • Each subscriber gets independent results
  • Good for operations that should repeat

Hot Publisher Characteristics

  • Shared execution across subscribers
  • Work happens independently of subscriptions
  • All subscribers receive the same events
  • Good for broadcasting and shared state

Conclusion

Knowing when you’re working with hot vs. cold publishers helps you avoid bugs, optimize performance, and write more predictable reactive code.

Rule of thumb:

  • If multiple subscribers should share the same work or state, use hot publishers.
  • If each subscriber should trigger independent work, use cold publishers.
Back to Blog

Related posts

Read more »

Combine #2: Emisores & Suscriptores

Publisher Un emisor Publisher puede transmitir cero o más valores a uno o más suscriptores, y un solo evento de fin que puede ser éxito o error. Una vez emitid...

SwiftUI Gesture System Internals

markdown !Sebastien Latohttps://media2.dev.to/dynamic/image/width=50,height=50,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%...

SwiftUI View Diffing & Reconciliation

SwiftUI doesn’t “redraw the screen”. It diffs view trees. If you don’t understand how SwiftUI decides what changed vs what stayed the same, you’ll see unnecessa...