redis/redis-py · error · RuntimeError

Scheduler is stopped

Error message

Scheduler is stopped

What it means

Raised as RuntimeError in BackgroundScheduler.run_coro_sync when self._stopped is True. The scheduler has been shut down (stop() was called, or __del__ ran), so it can no longer submit coroutines to its event loop. This protects against scheduling work on a dead scheduler.

Solutions

  1. Ensure health checks are not scheduled after calling stop() on the scheduler/client (fix shutdown ordering).
  2. Guard callers with a check of scheduler._stopped (or a public is_running flag) before submitting work.
  3. Cancel pending health-check timers before stopping the scheduler.

Example fix

# before
await client.close()  # stops scheduler
await run_health_check()  # run_coro_sync -> RuntimeError
# after
await client.close()  # stops scheduler
# do not schedule further work after close
Defensive patterns

Strategy: validation

Validate before calling

if scheduler._stopped:
    logger.debug("scheduler stopped; skipping health check")
else:
    scheduler.run_coro_sync(hc, client)

Type guard

def scheduler_alive(scheduler) -> bool:
    return not getattr(scheduler, "_stopped", True)

Try / catch

try:
    scheduler.run_coro_sync(hc, client)
except RuntimeError as e:
    if "Scheduler is stopped" in str(e):
        logger.debug("scheduler stopped; ignoring health check")
    else:
        raise

Prevention

When it happens

Trigger: Calling run_coro_sync(...) on a BackgroundScheduler after stop() has been invoked. This happens in multidb health-check/circuit-breaker paths when a health check is attempted after the client or scheduler was shut down, or during interpreter teardown when __del__ already stopped it.

Common situations: Application shutdown ordering: the client/scheduler was stopped before a final health check fired. A background thread attempting a health check racing with explicit stop(). Interpreter exit triggering __del__ while a thread is still using the scheduler.

Related errors


AI-assisted analysis of redis/redis-py@6a6b581b48 (2026-08-10). Data as JSON: /api/errors/483a4eccce96a941. Report an issue: GitHub.

Appendix: 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 6a6b581b48)