Python systemd Integration — Core Concepts

What systemd provides

systemd is the init system and service manager on most modern Linux distributions. For Python developers, it offers several capabilities that are painful to build from scratch:

  • Process supervision: Automatically restart your app if it crashes
  • Startup ordering: Ensure the database is running before your app starts
  • Logging: Capture stdout/stderr into the systemd journal with metadata
  • Resource limits: Cap CPU, memory, and file descriptors per service
  • Readiness protocol: Know when your app is actually ready to serve traffic, not just started

Creating a systemd service for Python

A basic unit file at /etc/systemd/system/myapp.service:

[Unit]
Description=My Python Application
After=network.target postgresql.service
Wants=postgresql.service

[Service]
Type=notify
User=appuser
Group=appuser
WorkingDirectory=/opt/myapp
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/myapp/venv/bin/python -m myapp.server
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
WatchdogSec=30

[Install]
WantedBy=multi-user.target

Key settings explained:

  • Type=notify: systemd waits for the app to send a “ready” signal before considering it started
  • After/Wants: Start after PostgreSQL is up
  • Restart=on-failure: Restart only on non-zero exit codes (not on clean shutdown)
  • WatchdogSec=30: Expect a heartbeat every 30 seconds; restart if silent

The notification protocol

With Type=notify, your Python app needs to tell systemd when it is ready:

import sdnotify

notifier = sdnotify.SystemdNotifier()

# During startup — after loading config, connecting to DB, etc.
notifier.notify("READY=1")

# Periodic status updates (visible via systemctl status)
notifier.notify("STATUS=Serving 142 active connections")

# Watchdog heartbeat (call regularly if WatchdogSec is set)
notifier.notify("WATCHDOG=1")

# Before shutdown
notifier.notify("STOPPING=1")

The sdnotify library is a lightweight Python wrapper that sends messages through a Unix socket that systemd sets up automatically.

How the watchdog works

When WatchdogSec=30 is set, systemd expects your application to call WATCHDOG=1 at least every 30 seconds. If it does not, systemd assumes the process is stuck (deadlocked, hung, or livelocked) and restarts it.

This catches failures that a simple “is the process alive?” check misses. A process can be running but completely unresponsive — consuming CPU in an infinite loop or stuck waiting on a lock.

import threading
import sdnotify
import os

def watchdog_loop(notifier, interval):
    """Run in a background thread to send periodic heartbeats."""
    while True:
        notifier.notify("WATCHDOG=1")
        threading.Event().wait(interval)

# Calculate interval from environment
watchdog_usec = int(os.environ.get('WATCHDOG_USEC', 0))
if watchdog_usec > 0:
    # Notify at half the watchdog interval for safety margin
    interval = (watchdog_usec / 1_000_000) / 2
    notifier = sdnotify.SystemdNotifier()
    t = threading.Thread(target=watchdog_loop, args=(notifier, interval), daemon=True)
    t.start()

Journal logging

Instead of managing log files yourself, write to the systemd journal:

from systemd import journal

journal.send("Application started", PRIORITY=journal.LOG_INFO)
journal.send("Connection failed", PRIORITY=journal.LOG_ERR, DB_HOST="db.example.com")

Or use Python’s logging module with a journal handler:

import logging
from systemd.journal import JournalHandler

logger = logging.getLogger('myapp')
logger.addHandler(JournalHandler())
logger.setLevel(logging.INFO)

logger.info("Server ready on port 8080")

The journal offers structured logging, automatic timestamp tracking, and query tools (journalctl -u myapp.service --since "1 hour ago").

Common misconception

Many developers think Type=simple is fine for everything. With Type=simple, systemd considers the service “started” the instant the process is spawned — before your app finishes loading config, connecting to the database, or binding to a port. Dependent services may start too early. Type=notify solves this by waiting for your explicit ready signal.

Practical use cases

  • Web applications that notify readiness only after binding to a port and warming caches
  • Workers that signal status with queue depth and processing rate
  • Watchdog-enabled services that auto-restart on deadlocks, not just crashes
  • Microservices with proper startup ordering through dependency declarations
  • Zero-downtime deploys using ExecReload to trigger graceful config reload

One thing to remember: systemd integration turns your Python script into a first-class Linux service — with supervised restarts, ordered startup, structured logging, and health monitoring — all through a few lines of notification code and a unit file.

pythonlinuxsystem-administrationservices

See Also

  • Python Crontab Management How Python can set up automatic timers on your computer — like programming an alarm clock that runs tasks instead of waking you up.
  • Python Disk Usage Monitoring How Python helps you keep an eye on your computer's storage — like a fuel gauge that warns you before you run out of space.
  • Python Log Rotation Management Why your program's diary needs page limits — and how Python keeps log files from eating all your disk space.
  • Python Network Interface Monitoring How Python watches your computer's network connections — like having a traffic counter on every road leading to your house.
  • Python Process Management How Python lets you see and control all the programs running on your computer — like being the manager of a busy office.