skip to content

What is the difference between subscribeOn and publishOn, and how does each affect which thread the operators run on?

level: middleimportance: must knowfreq 85%

answer

  1. publishOn = downstream, position matters
  2. subscribeOn = source/subscription, position-independent
  3. multiple subscribeOn → closest to source wins
  4. publishOn cumulative; each switches again
  5. no operator → subscribing (event-loop) thread

basics

~10 s

publishOn switches the thread for operators placed after it (downstream). subscribeOn sets the thread for the source and the whole subscription, no matter where you put it in the chain.

solid answer

~50 s

`publishOn(scheduler)` changes the thread for **everything downstream of it** — operators after the call run on the given Scheduler, until the next `publishOn`. Its **position matters**. `subscribeOn(scheduler)` affects the **subscription and emission at the source** — it decides which thread the data source runs on, and therefore the upstream part of the chain — regardless of where you place it, because the subscription signal travels upward to the source. Roughly: `subscribeOn` = where the whole thing *starts*; `publishOn` = where things *continue* from that point downstream. If you have multiple `subscribeOn`, the one **closest to the source** wins. `publishOn` is applied cumulatively — each one hands off to a new thread for the operators below it. In practice you use `subscribeOn` to move a blocking source onto boundedElastic and `publishOn` to move post-processing onto another pool.

code

java · 16 lines
java
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;

Flux.range(1, 3)
    // runs on boundedElastic because subscribeOn moves the SOURCE,
    // regardless of its position in the chain:
    .map(i -> { log("map A", i); return i; })       // boundedElastic-N
    .subscribeOn(Schedulers.boundedElastic())
    .publishOn(Schedulers.parallel())
    .map(i -> { log("map B", i); return i; })       // parallel-N (downstream of publishOn)
    .subscribe(i -> log("subscribe", i));            // parallel-N

// Output threads:
//   map A      -> boundedElastic-1
//   map B      -> parallel-1
//   subscribe  -> parallel-1

go deeper

for a junior

State the one-liner: publishOn = downstream, subscribeOn = source/whole subscription.

for a middle

Predict thread names in a mixed chain and explain closest-to-source subscribeOn winning.

for a senior

Explain the subscription-upward vs data-downward directions and publishOn's async-boundary queue.

for a principal

Discuss ordering/backpressure implications of publishOn boundaries and when subscribeOn is a no-op on already-async sources.

## The mental model A reactive chain has two signal directions: **subscription** flows *upward* (from your `subscribe()` call up to the source), and **data/onNext** flows *downward* (source → operators → subscriber). `subscribeOn` and `publishOn` hook into these two directions differently. ### `publishOn(Scheduler)` — downstream thread switch When a signal (onNext/onComplete/onError) passes through `publishOn`, Reactor **re-dispatches** everything **below** that operator onto the given Scheduler. It's a boundary: operators *above* run on whatever thread was in effect before; operators *below* run on the new Scheduler's thread — until the next `publishOn` moves them again. **Position is significant**: moving a `publishOn` up or down the chain changes which operators are affected. You can chain several `publishOn`s to route different stages to different pools. ### `subscribeOn(Scheduler)` — where the source runs `subscribeOn` affects the **place the chain gets subscribed**, i.e., where the *source* emits. When you subscribe, the subscription travels up the chain; when it hits `subscribeOn`, the actual subscribe-to-source (and thus the source's emissions) is dispatched onto that Scheduler. Consequently the source and every operator up to the first `publishOn` run on that Scheduler — **regardless of where in the chain you wrote `subscribeOn`**. Position doesn't matter for *which* thread the source uses. **Multiple `subscribeOn`:** only one takes effect for the source — the one **closest to the source** (earliest upstream) wins; the others are effectively no-ops for source placement. ### How they combine A common pattern: `source.subscribeOn(boundedElastic).map(...).publishOn(parallel).map(...)`. The source + first `map` run on boundedElastic; after `publishOn(parallel)`, the second `map` runs on a parallel thread. If there is **no** `subscribeOn` and **no** `publishOn`, everything runs on the subscribing thread (in WebFlux, the Netty event loop). ## Edge cases & gotchas - **`subscribeOn` does NOT retroactively move operators that are downstream of a later `publishOn`.** Once a `publishOn` switches threads, downstream stays on the publishOn Scheduler. - **Synchronous sources vs. async sources:** `subscribeOn` is what you need to offload a *blocking synchronous* source (`Mono.fromCallable`). For an already-async source that emits on its own thread (e.g., a non-blocking driver), `subscribeOn` may have little effect on emission threads. - **Only the closest-to-source `subscribeOn` matters** — a frequent trick interview question. - **`publishOn` respects backpressure** and introduces an async boundary (an internal queue), which can matter for ordering/latency. - Operators like `flatMap`/`concatMap` can also change threads because inner publishers may run on their own Schedulers. ## When to use which - Blocking source (JDBC, blocking HTTP) → `subscribeOn(Schedulers.boundedElastic())`. - Offload only the *downstream* transformation/CPU work → `publishOn(Schedulers.parallel())`. - Both, at different stages → combine them.

  • If you put subscribeOn at the very end of the chain, does it still affect the source's thread?
    Yes. subscribeOn is position-independent for the source: the subscription propagates up to the source and dispatches it onto the given Scheduler no matter where the operator sits.
  • You have two publishOn calls in a chain — what runs where?
    Operators between the first publishOn and the second run on the first Scheduler; operators after the second run on the second Scheduler. Each publishOn hands downstream work to a new thread.
  • Which wins if you write subscribeOn twice?
    The one closest to the source (earliest upstream). The other has no effect on where the source is subscribed.

saying these in an interview costs you the question

  • 'subscribeOn only affects operators after it' — that's publishOn
  • 'publishOn changes the source thread' — no, only downstream
  • Thinking the last subscribeOn wins (it's the first/closest to source)
  • Believing subscribeOn position changes which operators it affects

context