Is Actual Budget Down? Real-Time Status & Outage Checker
Is Actual Budget Down? Real-Time Status & Outage Checker
Actual Budget is an open-source local-first personal finance application with over 16,000 GitHub stars. Originally a paid app, it was open-sourced in 2022 and has grown into one of the most popular self-hosted YNAB alternatives. Actual Budget features envelope (zero-based) budgeting, transaction reconciliation, detailed spending reports, and bank sync via SimpleFIN and GoCardless integrations. Its sync server enables end-to-end encrypted synchronization across multiple devices — phones, tablets, and desktops — while keeping all data local-first, meaning the app continues working offline. Self-hosted via Node.js or Docker, the Actual Budget server stores encrypted budget files that clients download and decrypt locally. The combination of local-first architecture, end-to-end encryption, and zero vendor lock-in has made Actual Budget the go-to choice for privacy-conscious budgeting enthusiasts.
Because Actual Budget uses a local-first architecture, a server outage does not immediately prevent budgeting — clients continue working with their local data. However, sync stops, changes made on one device don't reach others, and automated bank transaction imports cease. A server crash after a reinstall, encryption key mismatch, or reverse proxy misconfiguration can silently break sync for days before a user notices that their phone and desktop have diverged, with potentially conflicting budget changes that are difficult to reconcile.
Quick Status Check
#!/bin/bash
# Actual Budget health check
# Checks web server, Node.js process, data directory, and port
ACTUAL_HOST="${ACTUAL_HOST:-localhost}"
ACTUAL_PORT="${ACTUAL_PORT:-5006}"
ACTUAL_DATA="${ACTUAL_DATA:-/data}"
FAIL=0
echo "=== Actual Budget Status Check ==="
echo "Host: ${ACTUAL_HOST}:${ACTUAL_PORT}"
echo ""
# Check root URL (web app should return 200)
HTTP_CODE=$(curl -so /dev/null -w "%{http_code}" --max-time 5 \
"http://${ACTUAL_HOST}:${ACTUAL_PORT}/" 2>/dev/null)
if [ "$HTTP_CODE" = "200" ]; then
echo "[OK] Web app root returned HTTP 200"
else
echo "[FAIL] Web app root returned HTTP ${HTTP_CODE}"
FAIL=1
fi
# Check API login endpoint exists (POST — 400/401 means API is live)
LOGIN_CODE=$(curl -so /dev/null -w "%{http_code}" --max-time 5 \
-X POST "http://${ACTUAL_HOST}:${ACTUAL_PORT}/api/login" \
-H "Content-Type: application/json" \
-d '{}' 2>/dev/null)
if echo "${LOGIN_CODE}" | grep -qE "^(400|401|422)$"; then
echo "[OK] API login endpoint alive (HTTP ${LOGIN_CODE} = API responding)"
elif [ "$LOGIN_CODE" = "200" ]; then
echo "[OK] API login endpoint returned HTTP 200"
else
echo "[FAIL] API login endpoint returned HTTP ${LOGIN_CODE}"
FAIL=1
fi
# Check Node.js process
if pgrep -f "actual\|actual-server\|@actual-app" > /dev/null 2>&1; then
NODE_PID=$(pgrep -f "actual\|actual-server" | head -1)
echo "[OK] Actual Budget Node.js process running (PID ${NODE_PID})"
elif docker inspect actual_budget 2>/dev/null | grep -q '"Running": true'; then
echo "[OK] Actual Budget Docker container is running"
else
echo "[WARN] Actual Budget process/container not detected"
fi
# Check port is listening
if ss -tlnp 2>/dev/null | grep -q ":${ACTUAL_PORT}" || \
netstat -tlnp 2>/dev/null | grep -q ":${ACTUAL_PORT}"; then
echo "[OK] Port ${ACTUAL_PORT} is listening"
else
echo "[FAIL] Port ${ACTUAL_PORT} not listening"
FAIL=1
fi
# Check data directory for SQLite budget files
if [ -d "${ACTUAL_DATA}" ]; then
DB_COUNT=$(find "${ACTUAL_DATA}" -name "*.db" -o -name "*.sqlite" 2>/dev/null | wc -l)
if [ "$DB_COUNT" -gt 0 ]; then
echo "[OK] Data directory has ${DB_COUNT} SQLite file(s)"
else
echo "[WARN] Data directory exists but no .db files found — budget may not be initialized"
fi
# Check disk usage
DISK_PCT=$(df "${ACTUAL_DATA}" 2>/dev/null | tail -1 | awk '{print $5}' | tr -d '%')
if [ -n "${DISK_PCT}" ] && [ "${DISK_PCT}" -gt 90 ]; then
echo "[WARN] Disk at ${DISK_PCT}% capacity — sync may fail when full"
fi
else
echo "[WARN] Data directory not found at ${ACTUAL_DATA}"
fi
echo ""
if [ "$FAIL" -eq 0 ]; then
echo "Result: Actual Budget appears healthy"
else
echo "Result: Actual Budget has failures — review output above"
exit 1
fi
Python Health Check
#!/usr/bin/env python3
"""
Actual Budget health check
Verifies web server, API liveness, Node.js process, budget files, and disk usage
"""
import json
import os
import shutil
import subprocess
import sys
import time
from pathlib import Path
try:
import urllib.request as urlreq
import urllib.error as urlerr
except ImportError:
print("ERROR: urllib not available")
sys.exit(1)
HOST = os.environ.get("ACTUAL_HOST", "localhost")
PORT = int(os.environ.get("ACTUAL_PORT", "5006"))
DATA_DIR = Path(os.environ.get("ACTUAL_DATA", "/data"))
BASE_URL = f"http://{HOST}:{PORT}"
DISK_WARN_PCT = 90
TIMEOUT = 8
results = []
def check(label, ok, detail=""):
status = "OK" if ok else ("WARN" if ok is None else "FAIL")
msg = f"[{status}] {label}"
if detail:
msg += f" — {detail}"
print(msg)
if ok is not None:
results.append(ok)
return ok
def fetch(path, method="GET", data=None, timeout=TIMEOUT):
try:
url = f"{BASE_URL}{path}"
headers = {"Content-Type": "application/json", "Accept": "application/json,text/html"}
body = json.dumps(data).encode() if data else None
req = urlreq.Request(url, data=body, headers=headers, method=method)
with urlreq.urlopen(req, timeout=timeout) as resp:
return resp.status, resp.read().decode("utf-8", errors="replace")
except urlerr.HTTPError as e:
return e.code, e.read().decode("utf-8", errors="replace")
except Exception as e:
return 0, str(e)
print(f"=== Actual Budget Python Health Check ===")
print(f"Target: {BASE_URL}")
print()
# 1. Root URL — web app should return 200 with HTML
t0 = time.time()
status, body = fetch("/")
latency_ms = (time.time() - t0) * 1000
if status == 200:
# Look for Actual Budget app markers in the HTML
has_markers = any(marker in body for marker in [
"Actual", "actual", "budget", "Budget", "
Common Actual Budget Outage Causes
| Symptom | Likely Cause | Resolution |
|---|---|---|
| Devices can't sync — changes stay local only | Node.js sync server crashed — clients use local data but cannot push or pull changes | Restart the Actual Budget server container or process; check logs for uncaught exceptions or port conflicts; verify Node.js version compatibility |
| Budget files missing or unreadable after reinstall | Data directory permissions wrong — container running as different UID after re-deploy | Run chown -R node:node /data or match the container UID; verify Docker volume mount path matches ACTUAL_DATA_DIR environment variable |
| Bank transactions stop importing automatically | SimpleFIN or GoCardless bank sync credentials expired or revoked | Re-authorize in Actual Budget settings under Bank Sync; check SimpleFIN token expiry at simplefin.org; rotate GoCardless API credentials in the developer portal |
| Cannot open budget after server reinstall — decryption error | End-to-end encryption key mismatch — server reinstalled without preserving the encryption secret | Restore from a local client export (File → Export) made before the server loss; the local client retains the decrypted budget and can re-upload to a fresh server |
| Real-time sync fails, changes require manual refresh | Reverse proxy not forwarding WebSocket connections — missing Upgrade header passthrough |
Add WebSocket proxy headers to nginx/Caddy/Traefik config: proxy_http_version 1.1, Upgrade $http_upgrade, Connection "upgrade" |
| Server starts but port 5006 is not accessible | Port conflict — another process bound to 5006 before Actual Budget started | Run lsof -i :5006 or ss -tlnp | grep 5006 to find the conflicting process; change Actual Budget port via ACTUAL_PORT environment variable |
Architecture Overview
| Component | Function | Failure Impact |
|---|---|---|
| Node.js Sync Server | Hosts encrypted budget files and synchronizes changes across devices via WebSocket | Multi-device sync stops; clients continue with local data but diverge over time |
| Web App (React) | Browser-based budget interface served by the Node.js server | Browser users cannot access budgets; mobile/desktop apps use cached local data |
| SQLite Budget Database | Encrypted local-first database storing all transactions, budgets, and accounts | Budget data inaccessible if server-side file lost without local client backup |
| End-to-End Encryption | Client-side encryption using a user-controlled key before data is sent to the server | Server compromise exposes only ciphertext; key loss makes server data permanently unrecoverable |
| Bank Sync (SimpleFIN/GoCardless) | Fetches bank transactions automatically and imports them into Actual Budget | Transactions stop auto-importing; manual CSV import still works as fallback |
| Reverse Proxy (nginx/Caddy) | Terminates TLS and proxies HTTP and WebSocket connections to the Node.js server | HTTPS access fails or WebSocket sync breaks if proxy is misconfigured |
Uptime History
| Date | Incident Type | Duration | Impact |
|---|---|---|---|
| 2026-02 | Node.js 22 compatibility issue in Actual Budget server release | ~2 days until patch | Server crashed on startup for users on Node.js 22; Node.js 20 LTS users unaffected |
| 2025-10 | SimpleFIN API breaking change affecting bank sync token format | ~1 week until patch | Automatic bank transaction import stopped for all SimpleFIN users; manual import worked |
| 2025-08 | Database migration regression in upgrade path from v24.x to v25.0 | Variable (user-managed) | Server failed to start after upgrade; budget files required manual migration script |
| 2025-07 | Docker volume mount breaking change — default data path moved | Until manually fixed | Existing budget files not found after container update; appeared as fresh install |
Monitor Actual Budget Automatically
Actual Budget's local-first design means a server outage is deceptively quiet — you keep budgeting on your device, unaware that sync has been broken for days and your family members' budget changes are diverging on their devices. ezmon.com monitors your Actual Budget endpoints from multiple external probes and alerts your team via Slack, PagerDuty, or SMS the moment the sync server stops responding or the web app becomes unreachable.