redis/redis-py · error · RuntimeError

Scheduler is stopped

Error message

Scheduler is stopped

What it means

Raised as RuntimeError by BackgroundScheduler.run_coro_sync (redis/background.py:143) when it is called after stop() has set _stopped. The scheduler is single-use: once stopped it will no longer execute coroutines synchronously. This guards against submitting work to a torn-down scheduler.

Source

Thrown at redis/background.py:143

        Args:
            coro: Coroutine function to execute
            *args: Arguments to pass to the coroutine
            timeout: Maximum seconds to wait for the result. None means wait
                forever. Default is 10 seconds to avoid blocking indefinitely
                if the event loop is busy with long-running health checks.

        Returns:
            The result of the coroutine

        Raises:
            TimeoutError: If the coroutine doesn't complete within timeout
            Any exception raised by the coroutine
        """

        with self._lock:
            if self._stopped:
                raise RuntimeError("Scheduler is stopped")

        # Ensure the shared loop exists
        self._ensure_health_check_loop()

        with self._lock:
            loop = self._health_check_loop

        # Submit the coroutine to the shared loop and wait for result
        future = asyncio.run_coroutine_threadsafe(coro(*args), loop)
        try:
            return future.result(timeout=timeout)
        except TimeoutError:
            # Cancel the future to avoid leaving orphaned tasks
            future.cancel()
            raise

    def run_coro_fire_and_forget(
        self, coro: Callable[..., Coroutine[Any, Any, Any]], *args

View on GitHub (pinned to da03cdc7e8)

Solutions

  1. Do not call run_coro_sync after stop(); order shutdown so no checks are scheduled post-stop.
  2. Create a fresh BackgroundScheduler if you need scheduling again after a stop.
  3. Guard call sites with a check of the scheduler lifecycle (or catch RuntimeError and skip).

Example fix

# before
scheduler.stop()
scheduler.run_coro_sync(initial_check)  # RuntimeError
# after
# ensure all checks are done before stop, or construct a new scheduler
new_scheduler = BackgroundScheduler()
new_scheduler.run_coro_sync(initial_check)
Defensive patterns

Strategy: validation

Validate before calling

if scheduler._stopped:
    scheduler = BackgroundScheduler()  # fresh instance
scheduler.run_coro_sync(check)

Type guard

def scheduler_alive(scheduler) -> bool:
    return not scheduler._stopped

Try / catch

try:
    scheduler.run_coro_sync(check)
except RuntimeError as e:
    if 'stopped' in str(e):
        logger.debug('scheduler stopped; skipping check')
    else:
        raise

Prevention

When it happens

Trigger: Calling run_coro_sync (e.g. an initial health check) on a BackgroundScheduler whose stop() was already invoked; reuse of a scheduler object after shutdown; ordering bug where stop() runs before the first check.

Common situations: Shutdown sequence races with a pending health-check trigger; atexit/finally calling stop() then later code attempting a check; scheduler stored on an object that gets closed and reused.

Related errors


AI-assisted analysis of redis/redis-py@da03cdc7e8 (2026-08-04). Data as JSON: /data/errors/483a4eccce96a941.json. Report an issue: GitHub.