Is Wallabag Down? Real-Time Status & Outage Checker
Is Wallabag Down? Real-Time Status & Outage Checker
Wallabag is an open-source self-hosted read-later application with 10,000+ GitHub stars, created by Nicolas Lœuillet and maintained by an active community. It lets you save web articles from any browser, annotate them, tag them, and read them offline across iOS and Android mobile apps. Beyond article saving, it features browser extensions for Chrome and Firefox, Kindle export via email, RSS feed generation per tag or all articles, and a full REST API. Built on the Symfony PHP framework with support for SQLite, MySQL, and PostgreSQL backends, it is the privacy-first self-hosted alternative to Pocket, Instapaper, and Readwise Reader.
A Wallabag outage means articles saved from your browser silently fail to sync, your mobile reading queue stops updating, and any RSS consumers stop receiving new entries. Because the stack involves a PHP-FPM process, a web server, a database, and optionally Redis for sessions, diagnosing which layer failed requires checking each component in sequence rather than just the front page.
Quick Status Check
#!/bin/bash
# Wallabag health check
set -euo pipefail
WB_HOST="${WB_HOST:-localhost}"
WB_PORT="${WB_PORT:-80}"
echo "=== Wallabag Status Check ==="
# 1. Check web port
echo -n "Wallabag web port $WB_PORT: "
if nc -z -w3 "$WB_HOST" "$WB_PORT" 2>/dev/null; then
echo "OPEN"
else
echo "CLOSED — web server may be down"
fi
# 2. Check API version endpoint (unauthenticated)
echo -n "API version endpoint: "
RESP=$(curl -s -o /tmp/wb_version.json -w "%{http_code}" \
--connect-timeout 5 \
"http://${WB_HOST}:${WB_PORT}/api/version" 2>/dev/null || echo "000")
echo -n "HTTP $RESP "
[ "$RESP" = "200" ] && cat /tmp/wb_version.json && echo "" \
|| echo "(expected 200)"
# 3. Check PHP-FPM process
echo -n "PHP-FPM process: "
pgrep -f "php-fpm\|php8\|php7" &>/dev/null \
&& echo "RUNNING" || echo "NOT RUNNING — check php-fpm service"
# 4. Check database (attempt socket or port)
echo -n "MySQL/MariaDB port 3306: "
nc -z -w2 "$WB_HOST" 3306 2>/dev/null \
&& echo "OPEN" || echo "CLOSED (may use PostgreSQL or SQLite)"
echo -n "PostgreSQL port 5432: "
nc -z -w2 "$WB_HOST" 5432 2>/dev/null \
&& echo "OPEN" || echo "CLOSED (may use MySQL or SQLite)"
# 5. Check Redis if configured
echo -n "Redis port 6379: "
nc -z -w2 "$WB_HOST" 6379 2>/dev/null \
&& echo "OPEN" || echo "CLOSED (Redis may not be configured)"
echo "=== Check complete ==="
Python Health Check
#!/usr/bin/env python3
"""
Wallabag health check
Checks API version endpoint, authenticates via OAuth2, queries entry count,
tag count, and current user info.
"""
import json
import os
import sys
import time
import urllib.request
import urllib.error
import urllib.parse
HOST = os.environ.get("WB_HOST", "localhost")
PORT = int(os.environ.get("WB_PORT", "80"))
CLIENT_ID = os.environ.get("WB_CLIENT_ID", "")
CLIENT_SECRET = os.environ.get("WB_CLIENT_SECRET", "")
WB_USER = os.environ.get("WB_USER", "wallabag")
WB_PASS = os.environ.get("WB_PASS", "wallabag")
TIMEOUT = 10
BASE_URL = f"http://{HOST}:{PORT}"
results = []
def log(label: str, status: str, detail: str = "") -> None:
symbol = "OK" if status == "ok" else "FAIL"
line = f"[{symbol}] {label}"
if detail:
line += f": {detail}"
print(line)
results.append({"label": label, "status": status, "detail": detail})
def http_get(path: str, token: str = "") -> dict:
url = BASE_URL + path
headers = {"Accept": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
return json.loads(resp.read().decode())
def check_api_version() -> None:
try:
data = http_get("/api/version")
version = data if isinstance(data, str) else data.get("version", str(data))
log("API version endpoint", "ok", f"version={version}")
except urllib.error.HTTPError as e:
log("API version endpoint", "fail", f"HTTP {e.code}")
except Exception as e:
log("API version endpoint", "fail", str(e))
def authenticate() -> str:
if not CLIENT_ID or not CLIENT_SECRET:
log("OAuth2 authentication", "fail",
"WB_CLIENT_ID and WB_CLIENT_SECRET not set — "
"find values in Wallabag API clients management page")
return ""
payload = urllib.parse.urlencode({
"grant_type": "password",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"username": WB_USER,
"password": WB_PASS,
}).encode()
req = urllib.request.Request(
BASE_URL + "/oauth/v2/token",
data=payload,
headers={"Content-Type": "application/x-www-form-urlencoded"},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
data = json.loads(resp.read().decode())
token = data.get("access_token", "")
log("OAuth2 authentication", "ok" if token else "fail",
"token obtained" if token else "no token in response")
return token
except urllib.error.HTTPError as e:
body = e.read().decode()
log("OAuth2 authentication", "fail", f"HTTP {e.code}: {body[:120]}")
return ""
except Exception as e:
log("OAuth2 authentication", "fail", str(e))
return ""
def check_entries(token: str) -> None:
try:
data = http_get("/api/entries?perPage=1&page=1", token)
total = data.get("total", data.get("_embedded", {}).get("items", []))
if isinstance(total, int):
log("Saved entries", "ok" if total >= 0 else "fail", f"{total} articles")
else:
log("Saved entries", "ok", "entries endpoint responded")
except Exception as e:
log("Saved entries", "fail", str(e))
def check_tags(token: str) -> None:
try:
data = http_get("/api/tags", token)
count = len(data) if isinstance(data, list) else 0
log("Tag count", "ok", f"{count} tags")
except Exception as e:
log("Tag count", "fail", str(e))
def check_user(token: str) -> None:
try:
data = http_get("/api/user", token)
username = data.get("username", "unknown")
email = data.get("email", "")
log("Current user", "ok", f"username={username} email={email}")
except Exception as e:
log("Current user", "fail", str(e))
def main() -> None:
print(f"=== Wallabag Health Check — {BASE_URL} ===\n")
t0 = time.time()
check_api_version()
token = authenticate()
if token:
check_entries(token)
check_tags(token)
check_user(token)
elapsed = time.time() - t0
failures = [r for r in results if r["status"] != "ok"]
print(f"\n--- Summary ({elapsed:.1f}s) ---")
print(f"Checks passed: {len(results) - len(failures)}/{len(results)}")
if failures:
print("Failures:")
for f in failures:
print(f" - {f['label']}: {f['detail']}")
sys.exit(1)
else:
print("All checks passed.")
if __name__ == "__main__":
main()
Common Wallabag Outage Causes
| Symptom | Likely Cause | Resolution |
|---|---|---|
| 502 Bad Gateway from nginx or Apache | PHP-FPM process crashed or not running | Restart php-fpm service; check /var/log/php-fpm/error.log for crash reason |
| All articles inaccessible; API returns 500 | Database connection lost; MySQL/PostgreSQL server down or credentials changed | Verify database server is running; test connection with mysql/psql client; check parameters.yml credentials |
| Browser extension saves articles silently with no error | OAuth2 API token expired; extension using stale token | Regenerate API token in Wallabag settings; update token in browser extension options |
| Mobile app shows "Synchronization failed" | App credentials changed or server certificate expired; sync endpoint returning error | Re-enter server URL and credentials in mobile app; renew TLS certificate if HTTPS |
| Kindle export emails not arriving | SMTP relay misconfigured or credentials expired after password rotation | Update SMTP credentials in Wallabag settings; verify with a test email from app/console swiftmailer:email:send |
| All users suddenly logged out | Redis session store flushed or Redis restarted unexpectedly | Check Redis service; if sessions are critical, configure persistent Redis or use database session handler |
Architecture Overview
| Component | Function | Failure Impact |
|---|---|---|
| Nginx / Apache | Web server; forwards PHP requests to FPM, serves static assets | 502/504 errors on all requests; complete service unavailability |
| PHP-FPM | PHP process manager; executes Symfony application code | All dynamic pages and API endpoints fail; static assets still served by web server |
| Symfony Application | Core application logic: article saving, annotation, tagging, REST API | Errors in application code produce 500 responses; check Symfony logs in var/logs/ |
| Database (SQLite / MySQL / PostgreSQL) | Stores all articles, annotations, tags, users, and OAuth tokens | Any database failure makes all content inaccessible; API returns 500 errors |
| Redis (optional) | Session store and cache backend | Session loss logs out all users; cache miss causes slower page loads |
REST API (/api/) | Programmatic access for browser extensions, mobile apps, and third-party integrations | Browser extension saves fail; mobile app sync stops; RSS generation may break |
Uptime History
| Date | Incident Type | Duration | Impact |
|---|---|---|---|
| 2025-07-22 | PHP-FPM OOM-killed after memory_limit set too low for large article import | 90 minutes | All API and web requests returned 502; browser extension saves silently failed |
| 2025-10-11 | MySQL server stopped after disk partition hosting data directory filled to 100% | 3 hours | All articles inaccessible; database writes had been failing for hours before full outage |
| 2025-12-05 | Redis flushed during maintenance window; all active sessions invalidated | 20 minutes | All users logged out simultaneously; re-login required; no data loss |
| 2026-02-19 | TLS certificate expired on self-hosted instance; mobile apps refused HTTPS connection | 6 hours | Mobile app sync completely broken; browser extension on HTTP subdomain unaffected |
Monitor Wallabag Automatically
Wallabag outages are particularly insidious because browser extensions show no error — articles simply disappear into the void while you assume they were saved. Without external monitoring, you may only discover an outage days later when you try to read a saved article on your phone. ezmon.com monitors your Wallabag endpoints from multiple external probes and alerts your team via Slack, PagerDuty, or SMS the moment the API version endpoint stops returning a 200 response or PHP-FPM stops accepting connections.