# How the Speech-to-Speech Pipeline Handles Graceful Shutdown with Signal Handlers

> Learn how the Hugging Face speech-to-speech pipeline achieves graceful shutdown using SIGINT and SIGTERM signal handlers. Ensure clean thread exits with a 5-second timeout.

- Repository: [Hugging Face/speech-to-speech](https://github.com/huggingface/speech-to-speech)
- Tags: internals
- Published: 2026-07-11

---

**The Hugging Face speech-to-speech pipeline registers custom handlers for `SIGINT` and `SIGTERM` in [`s2s_pipeline.py`](https://github.com/huggingface/speech-to-speech/blob/main/s2s_pipeline.py) that trigger a coordinated shutdown via `ThreadManager.stop()`, ensuring all handler threads exit cleanly within a 5-second timeout.**

The `huggingface/speech-to-speech` repository implements a real-time audio processing system built around concurrent handler threads managed by a central `ThreadManager`. To prevent data loss and resource leaks when the process receives termination signals, the pipeline implements a robust **graceful shutdown mechanism** using Python's `signal` module.

## Signal Handler Registration in the Entry Point

The pipeline installs signal handlers during initialization in the main entry point. In [`src/speech_to_speech/s2s_pipeline.py`](https://github.com/huggingface/speech-to-speech/blob/main/src/speech_to_speech/s2s_pipeline.py), the code registers a handler for both `SIGINT` (Ctrl-C) and `SIGTERM` (system kill) before starting the thread manager.

```python
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)

```

This registration occurs around lines 1056–1064 in [`s2s_pipeline.py`](https://github.com/huggingface/speech-to-speech/blob/main/s2s_pipeline.py), ensuring that any external termination request gets intercepted before the process can terminate abruptly.

## The Graceful Shutdown Sequence

When a termination signal arrives, the pipeline executes a controlled shutdown sequence that prevents orphaned threads and ensures audio resources are released properly.

### Signal Handler Logic

The `signal_handler` function defined in [`s2s_pipeline.py`](https://github.com/huggingface/speech-to-speech/blob/main/s2s_pipeline.py) uses a mutable flag `shutdown_requested` to prevent duplicate shutdown attempts. Its implementation follows this pattern:

```python
def signal_handler(_sig: int, _frame: Optional[FrameType]) -> None:
    if not shutdown_requested[0]:
        shutdown_requested[0] = True
        console.print("\n[yellow]Shutting down gracefully...[/yellow]")
        pipeline_manager.stop()
        console.print("[green]✓ Pipeline stopped successfully[/green]")

```

The handler performs three critical actions: it flags that shutdown is in progress, prints status messages to the console, and invokes `pipeline_manager.stop()`. The same cleanup logic also executes in the `except KeyboardInterrupt` block as a fallback for interactive sessions.

### ThreadManager.stop() Implementation

The `pipeline_manager` is an instance of `ThreadManager` defined in [`src/speech_to_speech/utils/thread_manager.py`](https://github.com/huggingface/speech-to-speech/blob/main/src/speech_to_speech/utils/thread_manager.py). Its `stop()` method orchestrates the actual thread termination:

```python
def stop(self) -> None:
    # Signal all handlers to stop

    for handler in self.handlers:
        handler.stop_event.set()

    # Wait for all threads to finish with timeout

    for i, thread in enumerate(self.threads):
        if thread.is_alive():
            thread.join(timeout=5.0)
            if thread.is_alive():
                logger.warning(
                    f"Thread {i} ({thread.name}) did not terminate within timeout"
                )

```

By setting each handler's `stop_event`, the manager requests cooperative cancellation. The `join(timeout=5.0)` ensures the shutdown cannot hang indefinitely, logging warnings for any threads that fail to terminate within the 5-second window.

## How Handler Threads Respond to Stop Events

Each processing component inherits from the base handler class in [`src/speech_to_speech/baseHandler.py`](https://github.com/huggingface/speech-to-speech/blob/main/src/speech_to_speech/baseHandler.py), which provides the `stop_event` attribute. Handlers check this event in their processing loops to break out of blocking operations and release resources like audio streams and network sockets.

When `ThreadManager.stop()` sets the event, handlers can:
- Exit processing loops cleanly
- Flush remaining audio buffers
- Close open file descriptors
- Release GPU memory if applicable

## Complete Shutdown Workflow

The full lifecycle of the speech-to-speech pipeline follows this sequence:

1. **Initialization**: `main()` parses arguments, builds the pipeline, and creates a `ThreadManager` instance containing all handler threads
2. **Registration**: Signal handlers for `SIGINT` and `SIGTERM` are registered via `signal.signal()`
3. **Execution**: `pipeline_manager.start()` launches all threads, followed by `pipeline_manager.wait()` to block the main thread
4. **Termination**: Upon receiving a signal, the custom handler triggers `pipeline_manager.stop()`
5. **Cleanup**: All threads receive the stop event, join with timeout, and the process exits cleanly

## Code Examples

### Running the Pipeline with Default Signal Handling

Start the pipeline from the command line. Press Ctrl-C or send `SIGTERM` to trigger the graceful shutdown:

```bash
python -m speech_to_speech.s2s_pipeline \
    --mode realtime \
    --tts melo \
    --stt whisper

# Press Ctrl-C to stop:

# → "Shutting down gracefully..." appears

# → All threads stop and join before exit

```

### Embedding in Another Python Application

When integrating the speech-to-speech pipeline into larger applications, register custom handlers that call the manager's stop method:

```python
from speech_to_speech.s2s_pipeline import parse_arguments, build_pipeline
from speech_to_speech.utils.thread_manager import ThreadManager
import signal

# Build pipeline components

args = parse_arguments()
manager: ThreadManager = build_pipeline(args)

def custom_handler(sig, frame):
    print(f"Received signal {sig}, initiating cleanup...")
    manager.stop()  # Graceful shutdown of S2S pipeline

    # Additional application cleanup here

    exit(0)

signal.signal(signal.SIGINT, custom_handler)
signal.signal(signal.SIGTERM, custom_handler)

manager.start()
manager.wait()

```

## Summary

- **Signal Registration**: The pipeline registers handlers for `SIGINT` and `SIGTERM` in [`src/speech_to_speech/s2s_pipeline.py`](https://github.com/huggingface/speech-to-speech/blob/main/src/speech_to_speech/s2s_pipeline.py) (lines 1056–1064) before starting the thread manager.
- **ThreadManager.stop()**: Sets `stop_event` on all handlers and joins threads with a 5.0-second timeout to prevent hanging.
- **Cooperative Cancellation**: Handlers in [`src/speech_to_speech/baseHandler.py`](https://github.com/huggingface/speech-to-speech/blob/main/src/speech_to_speech/baseHandler.py) check the `stop_event` to exit loops and release resources cleanly.
- **Duplicate Prevention**: The `shutdown_requested` flag ensures the shutdown sequence runs only once even if multiple signals arrive.
- **Fallback Handling**: `KeyboardInterrupt` exceptions trigger the same cleanup logic for interactive terminal sessions.

## Frequently Asked Questions

### What signals trigger the graceful shutdown in the speech-to-speech pipeline?

The pipeline explicitly handles `SIGINT` (typically sent via Ctrl-C) and `SIGTERM` (sent by process managers like systemd or Docker). Both signals trigger the same `signal_handler` function that initiates the graceful shutdown sequence through `ThreadManager.stop()`.

### How does ThreadManager ensure threads don't hang during shutdown?

The `stop()` method in [`src/speech_to_speech/utils/thread_manager.py`](https://github.com/huggingface/speech-to-speech/blob/main/src/speech_to_speech/utils/thread_manager.py) uses `thread.join(timeout=5.0)` for every handler thread. If a thread fails to terminate within 5 seconds, the manager logs a warning and continues shutdown, preventing the process from hanging indefinitely on unresponsive components.

### Can I customize the shutdown behavior when embedding the pipeline?

Yes. When using the pipeline as a library, build the `ThreadManager` instance manually and register your own signal handlers that call `manager.stop()`. You can add custom cleanup logic before or after the stop call, such as saving state or closing database connections, before exiting the process.

### What happens if a thread doesn't terminate within the 5-second timeout?

If a thread remains alive after the 5-second `join()` timeout expires, `ThreadManager.stop()` logs a warning message identifying the specific thread by index and name. The process continues shutting down, potentially leaving that thread as a zombie, but preventing the entire application from hanging indefinitely.