skip to content

Why does retry require the upstream Flow to be cold/restartable, and what happens if you retry a hot or non-idempotent source?

level: middleimportance: should knowfreq 28%

answer

  1. Retry re-subscribes => needs a restartable (cold) source
  2. Cold (flow{}, asFlow) re-runs producer per collection
  3. Hot (SharedFlow/StateFlow) doesn't restart on re-subscribe
  4. Side effects repeat each retry => idempotency matters
  5. Retry smallest cold idempotent segment; use idempotency keys

basics

~20 s

Retry works by starting the flow over from the beginning. That only makes sense for a cold flow that re-runs cleanly. With a hot or one-shot source, restarting may do nothing useful or repeat real side effects.

solid answer

~40 s

retry/retryWhen re-subscribe to the upstream, re-executing the producer. Cold flows (built with flow { }, asFlow(), etc.) re-run their block on every collection, so re-subscribing genuinely re-does the work — perfect for retry. Hot flows (SharedFlow/StateFlow, or a flow bridging a live event source) don't restart on re-subscribe; collecting again just attaches a new subscriber to the same stream, so retry may not reproduce the failed work and can behave unexpectedly. Even with a cold flow, if the producer has non-idempotent side effects (charge a card, send an email, increment a DB counter), each retry repeats them. Best practice: retry the smallest cold, idempotent upstream possible, and use idempotency keys for write operations.

go deeper

for a junior

Knows retry starts the flow over and that cold flows re-run.

for a middle

Distinguishes cold vs hot and explains why hot sources don't restart on re-subscribe.

for a senior

Places retry around the smallest cold idempotent segment and reasons about side-effect repetition.

for a principal

Designs end-to-end idempotency (keys, dedup) and decides which layer owns retry vs higher-level resilience.

## Cold vs hot flows - A **cold flow** starts producing only when collected and **re-runs its producer block for each collector**. Builders: `flow { }`, `flowOf(...)`, `asFlow()`, `channelFlow { }`. Re-collecting = re-doing the work. - A **hot flow** emits independently of collectors and is **shared**: `MutableSharedFlow`/`SharedFlow`, `MutableStateFlow`/`StateFlow`. Subscribing doesn't start fresh work; it taps an ongoing stream. ## Why retry assumes restartability `retry` recovers by **re-subscribing to the upstream** — it cancels the failed collection and collects again. The mental model is "run the whole upstream again." That model is only meaningful when re-collection actually re-attempts the failed operation, which is exactly what a **cold** flow does. ```kotlin // Cold: each retry re-executes load() flow { emit(load()) }.retry(3) ``` ## What goes wrong with hot sources If the upstream is hot (say a `StateFlow` reflecting a connection, or a `shareIn`/`stateIn` result), re-collecting after a failure does **not** restart the underlying producer. You get a new subscription to the same shared state, so: - The original failed operation isn't re-attempted. - Whether the exception even reaches `retry` depends on how the hot flow surfaces errors (often it terminates the shared collector). For hot sources, retry should be applied **inside/around the cold upstream that feeds the hot flow** (e.g. retry the network call before `shareIn`), not after sharing. ## Idempotency, even when cold A cold flow re-runs cleanly, but "cleanly" still means **the side effects repeat**: - Read-only fetches: safe to repeat. - Writes (POST, payment, email): repeating may double-act. Use an **idempotency key** so the server dedupes, or move retry to a layer where repetition is safe. ```kotlin flow { emit(api.charge(orderId, idempotencyKey = key)) } .retry(2) // server dedupes by key, so a retried charge is safe ``` ## Rule of thumb Retry the **smallest cold, idempotent** segment of the pipeline. Keep stateful/hot and non-idempotent work out of the retried region.

  • Where should you place retry relative to shareIn/stateIn?
    Before sharing — retry the cold upstream that does the work, then shareIn/stateIn the result, so retries actually re-run the operation.
  • How do you make a retried write operation safe?
    Use an idempotency key so the server deduplicates repeated requests, or perform the retry where repeating has no harmful effect.

Retrying a cold flow is rewinding a recording and replaying it; retrying a hot flow is tuning back into a live broadcast — you can't replay what already aired.

saying these in an interview costs you the question

  • Assuming retry restarts a SharedFlow/StateFlow producer
  • Ignoring that side effects repeat per retry
  • Retrying non-idempotent writes with no dedup
  • Placing retry after shareIn expecting the work to re-run
  • Conflating 'cold' with 'safe to retry' regardless of side effects

context