DEV Community

ArshTechPro
ArshTechPro

Posted on

Hot and Cold Publishers in Swift Combine

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<Int, Never> { 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"
Enter fullscreen mode Exit fullscreen mode

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 subscribers receive events from the point they subscribe 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"
Enter fullscreen mode Exit fullscreen mode

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)") }
Enter fullscreen mode Exit fullscreen mode

This makes two separate network requests. You probably 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) }
Enter fullscreen mode Exit fullscreen mode

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)") }
Enter fullscreen mode Exit fullscreen mode

Each subscriber gets a different timestamp because Just creates new values 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))
        .decode(type: User.self, decoder: JSONDecoder())
        .eraseToAnyPublisher()
}

// Each call should make its own request
fetchUserData(id: "123").sink { ... }
fetchUserData(id: "456").sink { ... }
Enter fullscreen mode Exit fullscreen mode

You're creating reusable publisher factories

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

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) }
    }
}
Enter fullscreen mode Exit fullscreen mode

You're managing shared state

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

You need to share expensive operations

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

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

Converting Between Hot and Cold

Cold to 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)") }
Enter fullscreen mode Exit fullscreen mode

Hot to Cold: Use Deferred

let hotPublisher = PassthroughSubject<Int, Never>()

let coldPublisher = Deferred { hotPublisher }

// Each subscription creates a new relationship to the subject
Enter fullscreen mode Exit fullscreen mode

Though 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) }
Enter fullscreen mode Exit fullscreen mode

Pitfall 2: 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
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

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 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. The 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.

Top comments (1)

Collapse
 
arshtechpro profile image
ArshTechPro

Common cold publishers:

Just
Fail
Deferred
URLSession.dataTaskPublisher
Enter fullscreen mode Exit fullscreen mode