developer-tools

Is qBittorrent Down? Real-Time Status & Outage Checker

Is qBittorrent Down? Real-Time Status & Outage Checker

qBittorrent is an open-source BitTorrent client with 28,000+ GitHub stars and the most popular self-hosted download client for home media servers. It offers a full Web API (qBittorrent-nox for headless operation), RSS automation, sequential downloading, IP binding, and search plugins. It integrates with Sonarr, Radarr, Lidarr, and other *arr apps via its Web API, making it the standard BitTorrent backend for automated media pipelines. Available on Linux, macOS, Windows, and as a Docker container. When qBittorrent goes down or loses authentication, every *arr app loses its download client, queued grabs stall, and your media pipeline stops completely.

qBittorrent exposes its Web API on port 8080 by default. Monitoring authentication, transfer state, and active torrent health is the fastest way to catch failures before they cascade into hours of missed downloads.

Quick Status Check

#!/bin/bash
# qBittorrent health check
# Usage: QB_USERNAME=admin QB_PASSWORD=adminadmin ./qbittorrent-check.sh

QB_HOST="${QB_HOST:-http://localhost:8080}"
QB_USERNAME="${QB_USERNAME:-admin}"
QB_PASSWORD="${QB_PASSWORD:-adminadmin}"
PASS=0
FAIL=0

echo "=== qBittorrent Health Check ==="
echo "Host: $QB_HOST"
echo ""

# 1. Check process is running
if pgrep -x "qbittorrent-nox" > /dev/null 2>&1 || pgrep -x "qbittorrent" > /dev/null 2>&1; then
  echo "[OK]   qBittorrent process is running"
  PASS=$((PASS+1))
else
  echo "[FAIL] qBittorrent process not found"
  FAIL=$((FAIL+1))
fi

# 2. Check port 8080 is open
if nc -z localhost 8080 2>/dev/null; then
  echo "[OK]   Port 8080 is open"
  PASS=$((PASS+1))
else
  echo "[FAIL] Port 8080 is not reachable"
  FAIL=$((FAIL+1))
fi

# 3. Check app version (unauthenticated)
VERSION=$(curl -sf --max-time 10 "$QB_HOST/api/v2/app/version")
if [ $? -eq 0 ]; then
  echo "[OK]   API responding — qBittorrent v$VERSION"
  PASS=$((PASS+1))
else
  echo "[FAIL] /api/v2/app/version did not respond"
  FAIL=$((FAIL+1))
fi

# 4. Authenticate and get SID cookie
SID=$(curl -sf --max-time 10 -c - \
  --data "username=$QB_USERNAME&password=$QB_PASSWORD" \
  "$QB_HOST/api/v2/auth/login" | grep SID | awk '{print $NF}')
if [ -n "$SID" ]; then
  echo "[OK]   Authentication succeeded — SID obtained"
  PASS=$((PASS+1))
else
  echo "[FAIL] Authentication failed — check username and password"
  FAIL=$((FAIL+1))
fi

# 5. Check transfer info (requires auth)
if [ -n "$SID" ]; then
  TRANSFER=$(curl -sf --max-time 10 \
    -b "SID=$SID" \
    "$QB_HOST/api/v2/transfer/info")
  if [ $? -eq 0 ]; then
    DL_SPEED=$(echo "$TRANSFER" | grep -o '"dl_info_speed":[0-9]*' | cut -d: -f2)
    echo "[OK]   Transfer info accessible — DL speed: ${DL_SPEED:-0} B/s"
    PASS=$((PASS+1))
  else
    echo "[FAIL] /api/v2/transfer/info did not respond"
    FAIL=$((FAIL+1))
  fi
fi

echo ""
echo "=== Result: $PASS passed, $FAIL failed ==="
[ "$FAIL" -eq 0 ] && exit 0 || exit 1

Python Health Check

#!/usr/bin/env python3
"""
qBittorrent health check
Authenticates via Web API, checks version, transfer info, DHT status, torrent states,
and global stats.
"""

import os
import sys
import json
import urllib.request
import urllib.error
import urllib.parse
import http.cookiejar

QB_HOST = os.environ.get("QB_HOST", "http://localhost:8080")
QB_USERNAME = os.environ.get("QB_USERNAME", "admin")
QB_PASSWORD = os.environ.get("QB_PASSWORD", "adminadmin")

results = []
failures = 0
cookie_jar = http.cookiejar.CookieJar()
cookie_handler = urllib.request.HTTPCookieProcessor(cookie_jar)
opener = urllib.request.build_opener(cookie_handler)


def check(label, ok, detail=""):
    global failures
    status = "OK  " if ok else "FAIL"
    if not ok:
        failures += 1
    msg = f"[{status}] {label}"
    if detail:
        msg += f" — {detail}"
    results.append(msg)
    print(msg)


def api_get(path, timeout=10):
    url = f"{QB_HOST}{path}"
    req = urllib.request.Request(url)
    try:
        with opener.open(req, timeout=timeout) as resp:
            return resp.read().decode()
    except urllib.error.HTTPError as e:
        raise RuntimeError(f"HTTP {e.code} from {path}")
    except Exception as e:
        raise RuntimeError(f"Request failed: {e}")


def api_get_json(path, timeout=10):
    return json.loads(api_get(path, timeout))


def api_post(path, data, timeout=10):
    url = f"{QB_HOST}{path}"
    body = urllib.parse.urlencode(data).encode()
    req = urllib.request.Request(url, data=body,
                                  headers={"Content-Type": "application/x-www-form-urlencoded"})
    try:
        with opener.open(req, timeout=timeout) as resp:
            return resp.read().decode()
    except urllib.error.HTTPError as e:
        raise RuntimeError(f"HTTP {e.code} from {path}")
    except Exception as e:
        raise RuntimeError(f"Request failed: {e}")


print("=== qBittorrent Python Health Check ===")
print(f"Host: {QB_HOST}\n")

# 1. App version — no auth required
try:
    version = api_get("/api/v2/app/version")
    check("App version API", True, f"qBittorrent v{version.strip()}")
except RuntimeError as e:
    check("App version API", False, str(e))

# 2. Authenticate — POST username/password, get SID cookie
authenticated = False
try:
    result = api_post("/api/v2/auth/login", {
        "username": QB_USERNAME,
        "password": QB_PASSWORD,
    })
    if result.strip() == "Ok.":
        authenticated = True
        check("Authentication", True, f"logged in as '{QB_USERNAME}'")
    else:
        check("Authentication", False, f"unexpected response: {result.strip()}")
except RuntimeError as e:
    check("Authentication", False, str(e))

# 3. Transfer info — download/upload speed and DHT status
if authenticated:
    try:
        transfer = api_get_json("/api/v2/transfer/info")
        dl_speed = transfer.get("dl_info_speed", 0)
        up_speed = transfer.get("up_info_speed", 0)
        dht_nodes = transfer.get("dht_nodes", 0)
        dht_ok = dht_nodes > 0
        check(
            "Transfer info",
            True,
            f"DL {dl_speed // 1024} KB/s, UP {up_speed // 1024} KB/s",
        )
        check(
            "DHT nodes",
            dht_ok,
            f"{dht_nodes} nodes connected"
            + ("" if dht_ok else " — DHT disabled or no peers; public torrents may be affected"),
        )
    except RuntimeError as e:
        check("Transfer info", False, str(e))
else:
    check("Transfer info", False, "skipped — not authenticated")

# 4. Torrent list — breakdown by state
if authenticated:
    try:
        torrents = api_get_json("/api/v2/torrents/info")
        states = {}
        for t in torrents:
            state = t.get("state", "unknown")
            states[state] = states.get(state, 0) + 1
        error_count = states.get("error", 0) + states.get("missingFiles", 0)
        total = len(torrents)
        summary = ", ".join(f"{v} {k}" for k, v in sorted(states.items()))
        ok = error_count == 0
        check(
            "Torrent states",
            ok,
            f"{total} total: {summary}"
            + ("" if ok else f" — {error_count} in error/missingFiles state"),
        )
    except RuntimeError as e:
        check("Torrent states", False, str(e))
else:
    check("Torrent states", False, "skipped — not authenticated")

# 5. Global sync stats — cumulative download/upload
if authenticated:
    try:
        maindata = api_get_json("/api/v2/sync/maindata")
        server_state = maindata.get("server_state", {})
        dl_total = server_state.get("dl_info_data", 0) // (1024 ** 3)
        up_total = server_state.get("up_info_data", 0) // (1024 ** 3)
        free_space = server_state.get("free_space_on_disk", 0) // (1024 ** 3)
        ok = free_space > 5
        check(
            "Disk free space",
            ok,
            f"{free_space} GB free on download disk"
            + ("" if ok else " — critically low, downloads will stall"),
        )
        check("Session totals", True, f"downloaded {dl_total} GB, uploaded {up_total} GB")
    except RuntimeError as e:
        check("Global sync stats", False, str(e))
else:
    check("Global sync stats", False, "skipped — not authenticated")

# Summary
print(f"\n=== Result: {len(results) - failures} passed, {failures} failed ===")
sys.exit(0 if failures == 0 else 1)

Common qBittorrent Outage Causes

SymptomLikely CauseResolution
*arr apps report download client connection failed Web UI authentication failure — password changed or session expired Update credentials in Sonarr/Radarr/Lidarr Settings → Download Clients; verify password in qBittorrent Web UI settings
All torrents stall at a fixed percentage Download directory disk full — writes blocked Run df -h; free space on the download drive; resume paused torrents after cleanup
Torrent shows 0% with no peers connecting Tracker unreachable — domain blocked, DNS failure, or tracker offline Check tracker URL in torrent details; test tracker reachability; try an alternative tracker via magnet link
Public torrents find no peers DHT disabled — required for trackerless peer discovery Tools → Options → BitTorrent → enable DHT, PeX, and Local Peer Discovery
Traffic routing through wrong network interface Bind IP misconfigured — all traffic goes through unintended interface Tools → Options → Advanced → Network Interface; set correct interface or IP binding for VPN/LAN
*arr apps queue unlimited downloads until disk is full Free disk space check not configured in qBittorrent or *arr apps Set a disk space reserve in qBittorrent (Options → Downloads); configure max queue size in each *arr app

Architecture Overview

ComponentFunctionFailure Impact
qBittorrent-nox (C++) Main headless process — torrent engine, Web API, RSS Total failure; all downloads stop and Web API becomes unreachable
Web UI / Web API HTTP API on port 8080 — used by *arr apps and browser *arr apps lose download client; no new torrents can be queued
BitTorrent Engine (libtorrent) Peer discovery, piece management, tracker communication Downloads stall; existing connections drop; tracker announces fail
DHT / PeX Decentralized peer discovery for public torrents Public torrents find no peers if disabled; private trackers unaffected
Download Storage Target disk for downloaded files before import by *arr apps All active downloads stall when disk reaches capacity
RSS Downloader Polls RSS feeds to auto-add torrents matching rules RSS-triggered downloads stop; manually added torrents unaffected

Uptime History

DateIncident TypeDurationImpact
2026-01 Web UI password changed — *arr apps lost connection ~10 hrs undetected Sonarr and Radarr queued grabs internally but no torrents were sent to qBittorrent
2025-11 Download disk full — all active transfers stalled ~3 hrs All in-progress downloads froze at current progress; no new torrents completing
2025-08 Bind IP set to VPN interface — VPN disconnected ~6 hrs All torrent traffic dropped; qBittorrent appeared healthy but no data transferred
2025-07 DHT disabled after settings reset ~12 hrs undetected Public torrents found zero peers; private tracker torrents unaffected

Monitor qBittorrent Automatically

qBittorrent failures are invisible to *arr apps until they try to queue a new download — by then your media pipeline may have been broken for hours. ezmon.com monitors your qBittorrent endpoints from multiple external probes and alerts your team via Slack, PagerDuty, or SMS the moment the Web API stops responding or authentication starts failing.

Set up qBittorrent monitoring free at ezmon.com →

qbittorrentbittorrentdownload-clientself-hostedhome-serverstatus-checker