productivity

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

SymptomLikely CauseResolution
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

ComponentFunctionFailure 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

DateIncident TypeDurationImpact
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.

Set up Actual Budget monitoring free at ezmon.com →

actual-budgetpersonal-financebudgetingself-hostedzero-based-budgetingstatus-checker