The 3 AM Wake-Up Call
At 3:47 AM on a Tuesday, your phone buzzes. A margin call. You fumble for your laptop, SSH into your server, and find the Python process running your pairs trading strategy dead — killed silently by an out-of-memory error triggered by a data spike that your exception handler never caught. By the time you reconnect and restart the strategy, the arbitrage window has closed. You lost $12,000 in six minutes of downtime.
This is not a hypothetical. It is the most common post-mortem in systematic trading: a process failure that goes unnoticed until the damage is already done. The irony is that preventing this class of failure costs less than one bad trade. This article covers two battle-tested tools for process supervision — supervisor and systemd — and shows you exactly how to configure both for a live quantitative trading system.
Why Quant Processes Are Fragile by Design
A production quant system sits at the intersection of unreliable inputs: network connections to market data feeds, exchange APIs with rate limits, broker connections with intermittent failures, and downstream databases that can lag or corrupt data. Each of these failure points can trigger an exception that your strategy does not handle gracefully.
Consider the typical failure taxonomy for a live quant system:
| Failure mode | Frequency | Mean time to detection (typical) | Impact |
|---|---|---|---|
| Out-of-memory kill | Daily to weekly | Minutes to hours (if at all) | Strategy halts, positions unmanaged |
| Data feed disconnection | Hourly to daily | Seconds to minutes | Stale prices, wrong signals |
| Unhandled exception in strategy loop | Daily | Minutes to hours | Strategy stops or enters invalid state |
| Broker API timeout | Weekly | Minutes | Orders stuck, fills missed |
| Server reboot (intentional or crash) | Monthly | Minutes to hours | Complete outage until human intervenes |
None of these failures require sophisticated tooling to prevent. They require process supervision: a watchdog that detects when a process dies, restarts it, and logs what happened. Both supervisor and systemd serve this purpose. They are not mutually exclusive — many production setups run supervisor inside a systemd unit — but they address different layers of the infrastructure.
Core Concepts: What Process Supervision Does
Before diving into configuration, establish a clear mental model. Process supervision performs four functions:
- Process lifecycle management — Start, stop, and restart your strategy process.
- Failure detection — Detect abnormal process termination (crash, OOM kill, signal termination).
- Automatic restart — Pull the process back up on failure, optionally with a backoff delay.
- Logging and stdout capture — Redirect process output to files so you can inspect what happened.
Both supervisor and systemd provide all four. The difference lies in the operational model: supervisor runs in user space as a regular daemon, while systemd is the init system — it starts at boot and manages processes with root-level privileges. This distinction drives the configuration trade-offs.
Supervisor: User-Space Process Supervision
When Supervisor Makes Sense
Supervisor is the better choice when you are running as a non-root user, want a simple configuration syntax, or need to manage multiple processes with a single tool. It is also the default choice when your server runs inside a container or VM where you do not have access to systemd.
Installation
# Debian / Ubuntu
sudo apt-get update
sudo apt-get install -y supervisor
# Verify installation
supervisord --version
Configuration
Supervisor's configuration lives in /etc/supervisor/conf.d/. Each process gets its own configuration file. Here is a production-grade configuration for a pairs trading strategy:
; /etc/supervisor/conf.d/pairs-trading-strategy.conf
[program:pairs-trading-strategy]
; Command to execute. Using absolute paths is critical.
command=/home/quant/venv/bin/python /home/quant/strategies/pairs_trading.py
; Working directory — log files are written here by default
directory=/home/quant/strategies
; Run as a non-root user for security
user=quant
; Auto-restart policy
autorestart=true
; Treat unexpected exit codes as errors (exit code 0 is normal termination)
exitcodes=0
; Restart delay: start with 10s, increase up to 60s on repeated failures
startsecs=10
startretries=3
; Number of seconds between successive restarts before supervisor gives up
; 0 means "always restart"
; Setting a limit prevents infinite crash loops that could hammer the exchange
stopwaitsecs=60
; Environment variables — API keys should NEVER be in the command line
environment=TICKDB_API_KEY="%(ENV_TICKDB_API_KEY)s",LOG_LEVEL="INFO"
; Redirect stdout and stderr to log files
stdout_logfile=/var/log/supervisor/pairs-trading-stdout.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10
stderr_logfile=/var/log/supervisor/pairs-trading-stderr.log
stderr_logfile_maxbytes=50MB
stderr_logfile_backups=10
; Send SIGTERM (not SIGKILL) on stop — allows graceful shutdown
stopasgroup=true
killasgroup=true
; Priority in startup order (lower number = starts first)
priority=999
The Critical Configuration Details
Several options in this file are frequently misconfigured in quant setups:
command: Always use an absolute path to the virtual environment's Python binary. Usingpythonwithout a path resolves to whateverpythonis in$PATH, which may not be the version you tested against.environment: Storing API keys in environment variables rather than the command line prevents keys from appearing in process listings (ps aux) and log files.stopasgroup/killasgroup: Ensures that when supervisor stops the process, it terminates the entire process group. Without this, child processes spawned by your strategy (data fetchers, logging threads) can become orphaned zombies.stopwaitsecs: The default value of 10 seconds is too short for strategies that maintain open orders. Give the strategy time to cancel resting orders before it is killed.
Managing Supervisor
# Reload configuration after editing
sudo supervisorctl reread
sudo supervisorctl update
# Common operations
sudo supervisorctl status pairs-trading-strategy
sudo supervisorctl stop pairs-trading-strategy
sudo supervisorctl start pairs-trading-strategy
sudo supervisorctl restart pairs-trading-strategy
# View recent logs
sudo supervisorctl tail pairs-trading-strategy stderr
Health Check Script for Supervisor
Supervisor can use an external health check to determine if a process is healthy beyond just "is it running." This is essential for quant strategies, where a process may be alive but stuck in an infinite loop or producing garbage signals.
#!/bin/bash
# /home/quant/health_check.sh
# Health check for pairs trading strategy
# Check 1: Is the process running?
if ! supervisorctl status pairs-trading-strategy | grep -q "RUNNING"; then
echo "Process not running"
exit 1
fi
# Check 2: Is the strategy producing log output?
# If the last log entry is older than 5 minutes, the loop may be stuck
LAST_LOG=$(stat -c %Y /var/log/supervisor/pairs-trading-stdout.log 2>/dev/null)
NOW=$(date +%s)
LOG_AGE=$((NOW - LAST_LOG))
if [ "$LOG_AGE" -gt 300 ]; then
echo "No log output in $LOG_AGE seconds — possible loop deadlock"
exit 1
fi
# Check 3: Is the strategy connected to the data feed?
# Ping the internal health endpoint (assumes your strategy exposes one)
HEALTH_RESPONSE=$(curl -s --max-time 5 http://localhost:8080/health 2>/dev/null)
if [ "$HEALTH_RESPONSE" != "OK" ]; then
echo "Health endpoint returned: $HEALTH_RESPONSE"
exit 1
fi
echo "All checks passed"
exit 0
Integrate this with supervisor using a separate monitor process or with systemd as a timer.
systemd: Boot-Level Process Supervision
When systemd Makes Sense
systemd is the right tool when you need your strategy to start at boot without human intervention, when you want system-level resource limits (CPU, memory, file descriptors), or when you need to coordinate startup order between your strategy and its dependencies (Redis, a database, a reverse proxy).
systemd is also the only choice if your server runs on a major cloud provider's managed instances — AWS EC2, GCP Compute Engine, and Azure VMs all use systemd as the init system.
The systemd Unit File
; /etc/systemd/system/quant-pairs-trading.service
[Unit]
Description=Pairs Trading Strategy
After=network-online.target
Wants=network-online.target
; Start after dependencies — e.g., Redis for order state
After=redis.service
Wants=redis.service
[Service]
Type=simple
; Run as non-root user
User=quant
Group=quant
; Working directory
WorkingDirectory=/home/quant/strategies
; The actual command
ExecStart=/home/quant/venv/bin/python /home/quant/strategies/pairs_trading.py
; Environment
Environment="TICKDB_API_KEY=%(ENV_TICKDB_API_KEY)s"
Environment="LOG_LEVEL=INFO"
; Restart policy: always restart on non-zero exit, but cap restart frequency
Restart=on-failure
RestartSec=10
; Prevent restart storms: maximum 5 restarts in 60 seconds
StartLimitIntervalSec=60
StartLimitBurst=5
; Resource limits — critical for preventing OOM crashes
MemoryMax=2G
MemoryHigh=1.8G
LimitNOFILE=65536
; Logging: journald captures stdout/stderr automatically
StandardOutput=journal
StandardError=journal
SyslogIdentifier=pairs-trading
; Graceful shutdown: give strategy 30 seconds to cancel orders and flush state
TimeoutStopSec=30
; Restart on filesystem changes (useful during deployments)
; WatchPath=/home/quant/strategies/
; ExecReload=/bin/kill -HUP $MAINPID
[Install]
WantedBy=multi-user.target
The Critical systemd Options for Trading Systems
| Option | Purpose | Common mistake |
|---|---|---|
Restart=on-failure |
Restart on crash but not on clean exit | Using Restart=always — this causes a restart loop on strategies that intentionally exit |
RestartSec=10 |
Wait 10 seconds before restarting | Restarting too fast, flooding the exchange with reconnect attempts |
StartLimitBurst=5 |
After 5 restarts in 60s, stop trying | Not setting this — a crashing strategy can hammer an exchange's rate limits indefinitely |
TimeoutStopSec=30 |
Allow 30 seconds for graceful shutdown | Setting this too low — a strategy with open orders needs time to cancel them before being killed |
MemoryMax=2G |
Hard cap on memory usage | Not setting this — an OOM kill is silent; MemoryMax triggers a controlled restart with a journal log |
After=network-online.target |
Wait for the network to be fully up before starting | Starting before the network is ready, then failing every data connection |
Managing systemd
# Reload unit files after editing
sudo systemctl daemon-reload
# Enable to start at boot
sudo systemctl enable quant-pairs-trading.service
# Start / stop / restart
sudo systemctl start quant-pairs-trading.service
sudo systemctl stop quant-pairs-trading.service
sudo systemctl restart quant-pairs-trading.service
# Status with recent log entries
sudo systemctl status quant-pairs-trading.service
sudo journalctl -u quant-pairs-trading.service -n 50 --no-pager
# View restart history — tells you how many times the process has crashed
sudo systemctl show quant-pairs-trading.service -p NRestarts,LastExitStatus
Configuring a Watchdog Timer
systemd can be configured to watch for process stalls using its built-in watchdog mechanism. Your strategy must send a periodic "I am alive" signal to systemd. If the signal stops, systemd kills and restarts the process.
[Service]
# Send watchdog keep-alive ping every 30 seconds
WatchdogSec=30
NotifyAccess=main
In your strategy code, signal systemd:
import socket
import struct
import os
# Use sd_notify to tell systemd the process is alive
def watchdog_ping():
"""Send watchdog ping to systemd. Call this every 30 seconds."""
notify_socket = os.environ.get("NOTIFY_SOCKET")
if not notify_socket:
return # Not running under systemd
# Send WATCHDOG=1 to indicate healthy state
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
try:
sock.sendto(b"WATCHDOG=1\n", notify_socket)
finally:
sock.close()
# In your main loop:
# while True:
# run_strategy_iteration()
# watchdog_ping()
# time.sleep(1) # or your actual loop interval
Supervisor vs systemd: A Direct Comparison
| Dimension | Supervisor | systemd |
|---|---|---|
| Scope | User-space daemon | Init system (kernel-adjacent) |
| Boot start | Manual (rc.local or cron) |
Native, via enable |
| Configuration syntax | INI-style, human-readable | INI-style unit files |
| Resource limits | Via ulimit in wrapper script |
Native (MemoryMax, LimitNOFILE) |
| Watchdog | DIY via health check script | Built-in (WatchdogSec) |
| Process groups | Requires stopasgroup |
Automatic via cgroup management |
| Log rotation | Manual (logrotate) | Automatic via journald |
| Dependency management | Implicit ordering only | Full dependency graph with After=/Wants= |
| Cluster management | Not natively | No, but works with systemd-machined |
| Best for | Shared hosting, containers, non-root users | Dedicated servers, boot recovery, cloud VMs |
For most quant trading deployments on cloud VMs, systemd is the primary choice and supervisor runs inside it as a supplementary layer for managing multi-process strategies (e.g., a data fetcher, strategy engine, and order router as separate supervisor-managed programs within a single systemd unit).
Deployment Guide by Scenario
Single Server, Non-Root User
Use supervisor as the sole process supervisor. Configure it to start on boot via cron:
# Add to crontab: @reboot /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
crontab -e
Dedicated Cloud VM (AWS EC2, GCP, Azure)
Use systemd as the primary supervisor. This ensures your strategy starts immediately after the operating system boots — even if you connect via SSM, serial console, or recovery mode.
# Deploy the unit file
sudo cp quant-pairs-trading.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable quant-pairs-trading.service
sudo systemctl start quant-pairs-trading.service
Docker Container Running a Strategy
Use supervisor inside the container (not systemd — containers typically run a single init process). The Dockerfile installs supervisor, and a supervisord.conf manages multiple processes:
# /etc/supervisor/conf.d/supervisord.conf (inside container)
[supervisord]
nodaemon=true
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
loglevel=info
[program:data-fetcher]
command=/app/data_fetcher.py
autorestart=true
stdout_logfile=/var/log/supervisor/data-fetcher.log
[program:strategy]
command=/app/strategy.py
autorestart=true
stdout_logfile=/var/log/supervisor/strategy.log
; Start after data fetcher is running
priority=100
Build the container with:
FROM python:3.11-slim
RUN apt-get update && apt-get install -y supervisor
COPY --chown=quant:quant /app /app
USER quant
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
Monitoring the Watchdogs
Regardless of which tool you use, configure alerting on restart events. Supervisor logs restarts to /var/log/supervisor/supervisord.log; systemd logs them to journald. Set up a simple alert:
# Alert on restart — check for restart events in systemd journal
import subprocess
import requests
def check_restart_events(service_name="quant-pairs-trading"):
result = subprocess.run(
["journalctl", "-u", service_name, "-p", "err", "--since", "1 hour", "--no-pager"],
capture_output=True, text=True
)
if "StartLimitBurst" in result.stdout or "restart" in result.stdout.lower():
message = f"⚠️ {service_name} restarted in the last hour.\n\n{result.stdout[-500:]}"
# Send to Slack, PagerDuty, or email
requests.post(
"https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
json={"text": message},
timeout=5
)
The Orderly Shutdown Problem
Both supervisor and systemd send SIGTERM by default when stopping a process. For a quant strategy, SIGTERM creates a critical race condition: the strategy receives the termination signal, but it still has open orders in the market. If it exits before canceling those orders, you are left with unmanaged risk.
The correct pattern is a two-stage shutdown:
- Signal the strategy to stop accepting new orders and begin canceling resting orders.
- Wait for order state to reach zero (or a timeout), then exit.
- If the timeout expires, force-kill with
SIGKILLand alert.
import signal
import sys
import threading
cancel_in_progress = threading.Event()
orders_cleared = threading.Event()
def shutdown_handler(signum, frame):
"""Called on SIGTERM. Initiates graceful order cancellation."""
print(f"Received signal {signum} — initiating graceful shutdown")
cancel_in_progress.set()
# Wait up to 25 seconds for orders to clear (TimeoutStopSec=30 in systemd)
if not orders_cleared.wait(timeout=25):
print("WARNING: Orders still open after timeout — forcing exit")
sys.exit(1)
print("All orders cleared — shutting down")
sys.exit(0)
signal.signal(signal.SIGTERM, shutdown_handler)
# In your order management loop:
# def cancel_all_orders():
# cancel_in_progress.set()
# # ... cancel resting orders via broker API
# orders_cleared.set()
This pattern is what TimeoutStopSec=30 in the systemd unit file is designed to accommodate. Without it, systemd sends SIGKILL 90 seconds after SIGTERM, which is far too long to wait before your strategy can restart.
Closing
A process that dies without a watchdog is not a system — it is a script. The difference between a reliable quant deployment and an expensive outage is three things: a supervisor that restarts the process, a resource cap that prevents OOM cascades, and a shutdown handler that closes positions before it exits.
Both supervisor and systemd give you these guarantees. Supervisor is simpler and portable. systemd is more powerful and boot-aware. The best production setups use both — systemd as the init system that survives server reboots, supervisor managing the individual strategy components inside a container or user session.
The configuration examples in this article are production-ready. Copy them, adapt them to your strategy, set your TICKDB_API_KEY environment variable, and deploy. The 3 AM wake-up call becomes far less likely.
Next Steps
If you're building your infrastructure from scratch, start with systemd on your server and the supervisor configuration for your development environment. The two systems are complementary, not competing.
If you want a data feed that survives the same failure modes as your strategy, TickDB provides a WebSocket-based market data API with native reconnection handling on the client side — compatible with any supervisor or systemd setup. Sign up at tickdb.ai (free tier, no credit card required).
If you're running multiple strategies and need a control plane, explore TickDB's batch data retrieval endpoint for historical backtesting — your development environment should mirror your production environment as closely as possible.
This article does not constitute investment advice. Trading strategies involve risk; process supervision mitigates operational risk but does not eliminate market risk. Past performance does not guarantee future results.