Overview
The subscribeOn operator controls what scheduler is used when the subscription to the source Observable happens. This affects when and how the subscription side-effects execute, not when values are delivered (use observeOn for that).
subscribeOn affects when subscription happens , while observeOn affects when notifications are delivered . These are complementary operators.
Signature
function subscribeOn < T >(
scheduler : SchedulerLike ,
delay : number = 0
) : MonoTypeOperatorFunction < T >
Parameters
The scheduler to use for subscription actions. Determines when the source Observable is subscribed to.
A delay (in milliseconds) to pass to the scheduler before subscribing to the source.
Returns
return
MonoTypeOperatorFunction<T>
A function that returns an Observable modified so that its subscriptions happen on the specified scheduler.
Usage Examples
Change Subscription Order
Control the order of Observable emissions with schedulers:
Without subscribeOn
With subscribeOn
import { of , merge } from 'rxjs' ;
const a = of ( 1 , 2 , 3 );
const b = of ( 4 , 5 , 6 );
merge ( a , b ). subscribe ( console . log );
// Output (synchronous):
// 1
// 2
// 3
// 4
// 5
// 6
Delayed Subscription
Delay when the source is subscribed to:
import { of , subscribeOn , asyncScheduler } from 'rxjs' ;
import { tap } from 'rxjs/operators' ;
console . log ( 'Before subscribe' );
of ( 1 , 2 , 3 ). pipe (
tap (() => console . log ( 'Source executing' )),
subscribeOn ( asyncScheduler , 2000 )
). subscribe ({
next: console . log ,
complete : () => console . log ( 'Complete' )
});
console . log ( 'After subscribe' );
// Output:
// Before subscribe
// After subscribe
// (2 second delay)
// Source executing
// 1
// Source executing
// 2
// Source executing
// 3
// Complete
Non-Blocking Subscription
Prevent blocking the current execution context:
import { range , subscribeOn , asyncScheduler } from 'rxjs' ;
console . log ( 'Start' );
range ( 1 , 1000000 ). pipe (
subscribeOn ( asyncScheduler )
). subscribe ({
next : value => {
// Heavy computation
performExpensiveOperation ( value );
},
complete : () => console . log ( 'Processing complete' )
});
console . log ( 'Subscription initiated' );
// UI remains responsive because subscription is async
// Output:
// Start
// Subscription initiated
// (then processing happens asynchronously)
// Processing complete
Testing with Scheduler
Control timing in unit tests:
import { of , subscribeOn } from 'rxjs' ;
import { TestScheduler } from 'rxjs/testing' ;
const testScheduler = new TestScheduler (( actual , expected ) => {
expect ( actual ). toEqual ( expected );
});
testScheduler . run (({ expectObservable , cold }) => {
const source$ = cold ( 'a-b-c|' ). pipe (
subscribeOn ( testScheduler )
);
expectObservable ( source$ ). toBe ( 'a-b-c|' );
});
How It Works
The operator schedules the subscription action on the specified scheduler:
export function subscribeOn < T >( scheduler : SchedulerLike , delay : number = 0 ) : MonoTypeOperatorFunction < T > {
return ( source ) =>
new Observable (( subscriber ) => {
subscriber . add ( scheduler . schedule (() => source . subscribe ( subscriber ), delay ));
});
}
Instead of immediately subscribing to the source, it schedules that subscription for later execution.
Key Differences: subscribeOn vs observeOn
subscribeOn : Controls WHEN subscription happens (affects subscription side-effects)observeOn : Controls WHEN notifications are delivered (affects next/error/complete timing)
Feature subscribeOn observeOn Affects Subscription timing Notification timing Position matters No (can be anywhere) Yes (affects downstream) Delays Subscription Each notification Use case Control subscription context Control delivery context Multiple uses Last one wins Each affects downstream
import { of , subscribeOn , observeOn , asyncScheduler } from 'rxjs' ;
// subscribeOn - position doesn't matter
of ( 1 , 2 , 3 )
. pipe (
map ( x => x * 2 ),
subscribeOn ( asyncScheduler ) // Could be anywhere in pipe
)
. subscribe ( console . log );
// observeOn - affects everything downstream
of ( 1 , 2 , 3 )
. pipe (
map ( x => x * 2 ), // Runs synchronously
observeOn ( asyncScheduler ), // Everything after runs async
map ( x => x + 1 ) // Runs asynchronously
)
. subscribe ( console . log );
Position Independence
Unlike observeOn, the position of subscribeOn in the pipe doesn’t matter - it always affects the initial subscription to the source.
import { of , subscribeOn , asyncScheduler , tap } from 'rxjs' ;
// These are equivalent:
// subscribeOn at the start
of ( 1 , 2 , 3 ). pipe (
subscribeOn ( asyncScheduler ),
tap ( console . log )
). subscribe ();
// subscribeOn at the end
of ( 1 , 2 , 3 ). pipe (
tap ( console . log ),
subscribeOn ( asyncScheduler )
). subscribe ();
// Both delay the subscription equally
Common Use Cases
Async Initialization : Defer expensive setup operations
Controlling Execution Order : Manage which Observable subscribes first in merges
Non-Blocking Operations : Keep UI responsive during subscriptions
Testing : Control subscription timing with TestScheduler
Background Processing : Move subscription work off main thread (in Worker environments)
Multiple subscribeOn Operators
When multiple subscribeOn operators are used, the last one closest to the source wins.
import { of , subscribeOn , asyncScheduler , queueScheduler } from 'rxjs' ;
of ( 1 , 2 , 3 ). pipe (
subscribeOn ( asyncScheduler ),
subscribeOn ( queueScheduler ) // This one is used
). subscribe ( console . log );
// queueScheduler is used because it's last
Practical Example: API Client
Defer expensive API initialization:
import { Observable , subscribeOn , asyncScheduler } from 'rxjs' ;
class ApiClient {
private initializeConnection () : void {
// Expensive: establish connection, authenticate, etc.
console . log ( 'Initializing API connection...' );
}
getUsers () : Observable < User []> {
return new Observable ( subscriber => {
this . initializeConnection ();
fetch ( '/api/users' )
. then ( response => response . json ())
. then ( users => {
subscriber . next ( users );
subscriber . complete ();
})
. catch ( error => subscriber . error ( error ));
}). pipe (
subscribeOn ( asyncScheduler ) // Don't block on initialization
);
}
}
const client = new ApiClient ();
console . log ( 'Creating subscription...' );
client . getUsers (). subscribe ( users => {
console . log ( 'Users:' , users );
});
console . log ( 'Subscription created' );
// Output:
// Creating subscription...
// Subscription created
// Initializing API connection...
// Users: [...]
Schedulers Overview
Choose the scheduler based on when you want subscription to occur:
asyncScheduler : Next macrotask (setTimeout)
asapScheduler : Next microtask (Promise)
queueScheduler : Synchronous but queued
animationFrameScheduler : Before next browser repaint
subscribeOn adds minimal overhead - just schedules one task
Use when you need to control subscription timing, not for every Observable
For high-frequency resubscriptions, consider the scheduler overhead
In most cases, synchronous subscription is fine
Best Practices
Use sparingly : Only when you need to control subscription timing
Position anywhere : Take advantage of position independence for readability
Choose right scheduler : Match scheduler to use case
Document why : Make it clear why async subscription is needed
Testing : Leverage with TestScheduler for deterministic tests
See Also