DEV Community

matteo adorni
matteo adorni

Posted on

Keep Freqtrade running 24/7: systemd vs a zero-dependency watchdog

You built the strategy. You backtested it. You started it with nohup. Three days later you check the P&L and… the process is gone. It has been gone since roughly two hours after you closed the terminal.

"Keep Freqtrade running" is the number one operational question in every Freqtrade forum, and the answers split into two camps: systemd and a cron watchdog. I have run both in production-style setups. Here is the honest comparison, and the pattern I actually use.

Option 1: systemd (the "proper" way)

# /etc/systemd/system/freqtrade.service
[Unit]
Description=Freqtrade
After=network.target

[Service]
User=freqtrade
WorkingDirectory=/home/freqtrade
ExecStart=/home/freqtrade/.venv/bin/freqtrade trade --config user_data/config.json
Restart=always
RestartSec=30

[Install]
WantedBy=multi-user.target
Enter fullscreen mode Exit fullscreen mode
sudo systemctl enable --now freqtrade
Enter fullscreen mode Exit fullscreen mode

What you get: automatic restart on crash, restart on reboot, journalctl logs, clean systemctl stop.

The catches, in practice:

  • It only restarts the process. It does not check that the thing is healthy. A Freqtrade process can be alive and stuck: exchange connection dropped, a hung API, a wedged websocket. Restart=always sees a running PID and does nothing.
  • No P&L record. You find out about a silent stall by noticing the trade count never moved.
  • No risk rule. If your bot is bleeding, systemd faithfully keeps it bleeding.
  • It needs root to install, and sudo on a cheap VPS is a thing you have to think about.

systemd is excellent at "make sure a binary is running". It is not an observability or risk layer, and it never claimed to be.

Option 2: cron + watchdog (the pattern I use)

A tiny script, run every 15 minutes, that does three jobs: restart if dead, record health, and enforce a kill switch.

*/15 * * * * /home/user/freqtrade/scripts/crypto_watchdog.sh
Enter fullscreen mode Exit fullscreen mode

The restart detection is the part that matters, and the naive version is a trap:

# DO NOT DO THIS
if ! pgrep -f freqtrade > /dev/null; then restart; fi
Enter fullscreen mode Exit fullscreen mode

pgrep -f matches the full command line, and your watchdog's own invocation may contain the pattern — so the script can match itself, conclude "all good", or pkill its own shell. I hit exactly this and wasted an hour on a watchdog that was killing itself.

Do it properly by scanning /proc and requiring two independent facts:

VENV_PY=$(readlink -f "$BASE/.venv/bin/python")

find_bot() {
  for p in /proc/[0-9]*; do
    pid=${p#/proc/}
    [ -r "$p/cmdline" ] || continue
    cmd=$(tr '\0' ' ' < "$p/cmdline" 2>/dev/null) || continue
    case "$cmd" in
      *"freqtrade trade --config"*)
        exe=$(readlink -f "$p/exe" 2>/dev/null)
        [ "$exe" = "$VENV_PY" ] && echo "$pid"
        ;;
    esac
  done
}
Enter fullscreen mode Exit fullscreen mode
  • /proc/<pid>/cmdline is the real argv (NUL-separated), not a shell's mangled string.
  • /proc/<pid>/exe must resolve to your venv interpreter — so you can never match a stray system Python or the watchdog itself.

Then the loop adds the two things systemd does not:

if [ -z "$(find_bot)" ]; then
  nohup "$BASE/.venv/bin/freqtrade" trade --config "$BASE/config.json" \
      --strategy SwingHighToSky --dry-run >> "$BASE/logs/trade_stdout.log" 2>&1 &
  sleep 30
  [ -n "$(find_bot)" ] || { echo "$(ts) restart FAILED"; exit 1; }
  echo "crypto-bot: restarted at $(ts)"   # speaks only when it acts
fi

OUT=$("$PY" "$BASE/monitor_pnl.py" 2>&1); RC=$?
if [ $RC -eq 2 ]; then                       # health script says: drawdown >= 10%
  for pid in $(find_bot); do kill "$pid"; done
  echo "$(ts) DRAWDOWN LIMIT HIT -> bot stopped"
  exit 2
fi
echo "$OUT" >> "$LOG"                        # one P&L line every 15 minutes
Enter fullscreen mode Exit fullscreen mode

monitor_pnl.py polls the local Freqtrade REST API (/api/v1/profit, /api/v1/status, /api/v1/balance) and appends a single line:

2026-09-25T15:02:58Z wallet=500.13 closed_pnl=+0.000USDT (+0.00%) all_pnl=-0.000USDT trades=0W/0L wr=0.0% open=1
Enter fullscreen mode Exit fullscreen mode

Now you have a time series, not a vibe. You can see the day the behaviour changed.

The comparison, honestly

systemd cron + watchdog
restart on crash ✅ ✅
restart on reboot ✅ (enable) ✅ (cron is persistent)
needs root ✅ ❌
detects "alive but stuck" ❌ ✅ (health script)
P&L time series ❌ ✅
enforced risk rule ❌ ✅
moves to any machine in 2 files ❌ ✅

Neither is "better". systemd is the right tool if all you need is "keep the binary up" and you have root. The watchdog wins when you want evidence and a kill switch — which, for anything handling money, is the actual requirement.

You can also run both: systemd to survive reboots, the watchdog cron for health and risk. They do not conflict.

What actually kills unattended bots

Ranked by how often I have seen them:

  1. The process dies and nobody notices — the watchdog fixes this in the first five minutes.
  2. It runs for weeks and you never look at the data — the P&L log fixes this.
  3. It draws down and you are asleep — the kill switch fixes this.
  4. Machine reboots and nothing restarts it — cron/enable fixes this.

Notice that three of the four are observability and risk, not supervision.


I packaged this exact layer — the /proc-based watchdog, the P&L logger and the drawdown kill switch, with a dry-run config and setup guide — as the Freqtrade Self-Healing Starter Kit ($9). It is the code from this post, ready to drop in, so you skip the hour I lost on the self-killing watchdog.

Related: Keep any long-running Python process alive on Linux for $0 (the /proc watchdog pattern) — the same pattern, generalised beyond Freqtrade.

Related in this series:

The watchdog from this post is open source: **https://github.com/moon-hacks/proc-watchdog* (MIT).*

Prefer someone to just do it? I also offer a **done-for-you setup* (https://tntofficial.gumroad.com/l/vweza) — Freqtrade installed, backtested and left running in dry-run with the watchdog active.*

Top comments (0)