Graceful Shutdown Executor Context Manager in Python

A context manager that starts a background thread and ensures it stops gracefully on exit, handling timeouts and exceptions.

Medium Python 3.9+ Aug 9, 2026 Concurrency & performance 15 views 0 copies

Python code

42 lines
Python 3.9+
import signal
import threading
import time
from contextlib import contextmanager


@contextmanager
def graceful_shutdown_executor(timeout=5.0):
    """Context manager that runs a task and gracefully stops it on timeout or exception."""
    stop_event = threading.Event()

    def task():
        print("Task started")
        while not stop_event.is_set():
            time.sleep(0.1)
        print("Task stopped gracefully")

    thread = threading.Thread(target=task, daemon=True)
    thread.start()

    try:
        yield stop_event
    except BaseException:
        print("Exception caught, initiating shutdown...")
        stop_event.set()
        thread.join(timeout=timeout)
        if thread.is_alive():
            print("Task still running after timeout")
        raise
    else:
        print("Context exited normally, shutting down...")
        stop_event.set()
        thread.join(timeout=timeout)
        if thread.is_alive():
            print("Task still running after timeout")


if __name__ == "__main__":
    with graceful_shutdown_executor(timeout=2.0) as stop:
        print("Inside context, doing work...")
        time.sleep(1.0)
        stop.set()  # Early stop example

Output

stdout
Task started
Inside context, doing work...
Context exited normally, shutting down...
Task stopped gracefully

How it works

This context manager uses threading.Event to coordinate shutdown. The yield exposes the event to the caller, allowing manual early termination. On normal exit or exception, stop_event.set() signals the thread to break its loop, and join(timeout) blocks until the thread finishes or the timeout expires. The finally-like structure ensures cleanup even if the context body raises.

Common mistakes

  • Forgetting to set the stop event before joining, causing deadlock
  • Not handling `thread.join(timeout)` return value to detect timeout
  • Making the thread non-daemon, which can block program exit if not joined

Variations

  1. Use `concurrent.futures.ThreadPoolExecutor` with `shutdown(wait=True)` for simpler pooling
  2. Use `asyncio` with `asyncio.Event` and async context manager for non-blocking tasks

Real-world use cases

  • Shutting down a background polling loop when a service receives a termination signal.
  • Cleaning up worker threads in a batch job after processing is complete or on error.
  • Managing a long-running background task in a CLI tool that must exit gracefully on Ctrl+C.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Concurrency & performance

Related tutorials and quizzes for this topic.