Skip to main content

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

scheduler
SchedulerLike
required
The scheduler to use for subscription actions. Determines when the source Observable is subscribed to.
delay
number
default:"0"
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:
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)
FeaturesubscribeOnobserveOn
AffectsSubscription timingNotification timing
Position mattersNo (can be anywhere)Yes (affects downstream)
DelaysSubscriptionEach notification
Use caseControl subscription contextControl delivery context
Multiple usesLast one winsEach 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

  1. Async Initialization: Defer expensive setup operations
  2. Controlling Execution Order: Manage which Observable subscribes first in merges
  3. Non-Blocking Operations: Keep UI responsive during subscriptions
  4. Testing: Control subscription timing with TestScheduler
  5. 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

Performance Considerations

  • 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

  1. Use sparingly: Only when you need to control subscription timing
  2. Position anywhere: Take advantage of position independence for readability
  3. Choose right scheduler: Match scheduler to use case
  4. Document why: Make it clear why async subscription is needed
  5. Testing: Leverage with TestScheduler for deterministic tests

See Also