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"
Each subscriber triggers a separate API call.
Common cold publishers:
JustFailDeferredURLSession.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"
Both subscribers receive the same value from a single emission.
Common hot publishers:
PassthroughSubjectCurrentValueSubjectTimer.publishNotificationCenter.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)") }
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) }
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 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 { ... }
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 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)") }
Hot to Cold: Use Deferred
let hotPublisher = PassthroughSubject<Int, Never>()
let coldPublisher = Deferred { hotPublisher }
// Each subscription creates a new relationship to the subject
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) }
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
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
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)
Common cold publishers: