Is Linkding Down? Real-Time Status & Outage Checker
Is Linkding Down? Real-Time Status & Outage Checker
Linkding is an open-source self-hosted bookmark manager with 7,000+ GitHub stars, created by Alexander Feddersen. It is designed to be minimal, fast, and keyboard-driven, making it a favorite among power users who prefer a no-frills interface over visual bookmark boards. Linkding supports tagging, full-text search across saved pages, browser extensions for Chrome and Firefox, bulk import and export in Netscape HTML and Pinboard JSON formats, and a REST API for automation. Built on Django with SQLite as the default database, it can be deployed as a Docker container in under five minutes. It is a compelling self-hosted alternative to Pinboard, Delicious, and Raindrop.io.
A Linkding outage means your browser extension silently stops saving bookmarks, your API automations fail, and your entire curated link collection becomes inaccessible. Because the stack is deliberately simple — a single Django process and a SQLite file — failures tend to be binary: either the process is running and healthy, or everything is down. That simplicity also means a corrupted database or an OOM-killed process will take down the entire service with no graceful degradation.
Quick Status Check
#!/bin/bash
# Linkding health check
set -euo pipefail
LD_HOST="${LD_HOST:-localhost}"
LD_PORT="${LD_PORT:-9090}"
LD_TOKEN="${LD_TOKEN:-}"
LD_DATA_DIR="${LD_DATA_DIR:-/etc/linkding/data}"
echo "=== Linkding Status Check ==="
# 1. Check web port
echo -n "Linkding port $LD_PORT: "
if nc -z -w3 "$LD_HOST" "$LD_PORT" 2>/dev/null; then
echo "OPEN"
else
echo "CLOSED — Django process may be down"
fi
# 2. Check root URL returns 200
echo -n "Web UI (root URL): "
HTTP=$(curl -s -o /dev/null -w "%{http_code}" \
--connect-timeout 5 \
"http://${LD_HOST}:${LD_PORT}/" 2>/dev/null || echo "000")
echo "HTTP $HTTP"
# 3. Check API with auth token if provided
if [ -n "$LD_TOKEN" ]; then
echo -n "API /api/bookmarks/ (authenticated): "
API_HTTP=$(curl -s -o /tmp/ld_api.json -w "%{http_code}" \
--connect-timeout 5 \
-H "Authorization: Token $LD_TOKEN" \
"http://${LD_HOST}:${LD_PORT}/api/bookmarks/?limit=1" 2>/dev/null || echo "000")
echo -n "HTTP $API_HTTP "
[ "$API_HTTP" = "200" ] && python3 -c "
import json, sys
d=json.load(open('/tmp/ld_api.json'))
print(f'({d.get(\"count\",\"?\")} bookmarks)')" 2>/dev/null || echo ""
fi
# 4. Check SQLite database file
echo -n "SQLite database file: "
DB_PATH="${LD_DATA_DIR}/db.sqlite3"
if [ -f "$DB_PATH" ]; then
SIZE=$(du -sh "$DB_PATH" 2>/dev/null | cut -f1)
echo "EXISTS ($SIZE)"
else
echo "NOT FOUND at $DB_PATH — check LD_DATA_DIR"
fi
# 5. Check Django/gunicorn process
echo -n "Django/gunicorn process: "
pgrep -f "gunicorn\|django\|linkding" &>/dev/null \
&& echo "RUNNING" || echo "NOT FOUND — check Docker container or service"
echo "=== Check complete ==="
Python Health Check
#!/usr/bin/env python3
"""
Linkding health check
Checks web UI availability, REST API bookmark and tag counts,
JSON response structure, and SQLite database file integrity.
"""
import json
import os
import sqlite3
import sys
import time
import urllib.request
import urllib.error
HOST = os.environ.get("LD_HOST", "localhost")
PORT = int(os.environ.get("LD_PORT", "9090"))
TOKEN = os.environ.get("LD_TOKEN", "")
DATA_DIR = os.environ.get("LD_DATA_DIR", "/etc/linkding/data")
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 = "", expect_json: bool = True):
url = BASE_URL + path
headers = {"Accept": "application/json" if expect_json else "text/html"}
if token:
headers["Authorization"] = f"Token {token}"
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
body = resp.read().decode()
if expect_json:
return json.loads(body)
return body, resp.status
def check_web_ui() -> None:
try:
_, status = http_get("/", expect_json=False)
log("Web UI", "ok", f"HTTP {status}")
except urllib.error.HTTPError as e:
# Linkding redirects unauthenticated requests to login — 302 is fine
ok = e.code in (200, 301, 302)
log("Web UI", "ok" if ok else "fail", f"HTTP {e.code}")
except Exception as e:
log("Web UI", "fail", str(e))
def check_bookmarks() -> None:
if not TOKEN:
log("Bookmark API", "fail",
"LD_TOKEN not set — set your Linkding API token in environment")
return
try:
data = http_get("/api/bookmarks/?limit=1", token=TOKEN)
count = data.get("count", -1)
results_list = data.get("results", [])
valid_structure = isinstance(results_list, list) and "count" in data
log("Bookmark count", "ok" if count >= 0 else "fail", f"{count} bookmarks")
log("API response structure", "ok" if valid_structure else "fail",
"count + results keys present" if valid_structure else "unexpected JSON shape")
except urllib.error.HTTPError as e:
log("Bookmark API", "fail", f"HTTP {e.code} — check API token")
except Exception as e:
log("Bookmark API", "fail", str(e))
def check_tags() -> None:
if not TOKEN:
return
try:
data = http_get("/api/tags/?limit=1", token=TOKEN)
count = data.get("count", -1)
log("Tag count", "ok" if count >= 0 else "fail", f"{count} tags")
except Exception as e:
log("Tag API", "fail", str(e))
def check_sqlite() -> None:
db_path = os.path.join(DATA_DIR, "db.sqlite3")
if not os.path.exists(db_path):
log("SQLite database file", "fail", f"not found at {db_path}")
return
size = os.path.getsize(db_path)
if size == 0:
log("SQLite database file", "fail", f"file is empty at {db_path}")
return
log("SQLite database file", "ok", f"exists, {size // 1024} KB")
try:
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
conn.execute("PRAGMA integrity_check").fetchone()
row = conn.execute(
"SELECT COUNT(*) FROM bookmarks_bookmark"
).fetchone()
conn.close()
log("SQLite integrity check", "ok", f"{row[0]} rows in bookmarks table")
except sqlite3.OperationalError as e:
log("SQLite integrity check", "fail", str(e))
except Exception as e:
log("SQLite integrity check", "fail", str(e))
def main() -> None:
print(f"=== Linkding Health Check — {BASE_URL} ===\n")
t0 = time.time()
check_web_ui()
check_bookmarks()
check_tags()
check_sqlite()
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 Linkding Outage Causes
| Symptom | Likely Cause | Resolution |
|---|---|---|
| All access stops; port 9090 not responding | Django/gunicorn process OOM-killed by kernel; Docker container exited | Restart container; set memory limits appropriately; check docker logs linkding for OOM message |
| SQLite database error on startup or queries return garbled data | Database file corrupted after unclean shutdown (power loss, force-kill) | Run sqlite3 db.sqlite3 "PRAGMA integrity_check"; restore from backup if corruption confirmed |
| Browser extension icon shows error; bookmarks not saving | API token missing or revoked in Linkding settings | Generate a new API token in Linkding Settings → API and update extension options |
| Import of large Netscape HTML file times out at 30 seconds | Default gunicorn worker timeout exceeded processing thousands of bookmarks | Split import file into smaller chunks; increase gunicorn --timeout via LD_REQUEST_TIMEOUT env var |
| Full-text search returns no results after version upgrade | Search index not rebuilt; SQLite FTS5 virtual table needs regeneration | Run python manage.py rebuildindex inside container; or use the admin UI rebuild option |
| Port 9090 conflicts with another service after host reboot | Another application (Cockpit, custom service) now occupying port 9090 | Change Linkding port via -p 9091:9090 in Docker run command; update browser extension URL |
Architecture Overview
| Component | Function | Failure Impact |
|---|---|---|
| Gunicorn / Django | WSGI application server handling all HTTP requests and business logic | Complete service unavailability; port stops accepting connections |
| SQLite Database | Single-file storage for all bookmarks, tags, users, and API tokens | Any corruption or lock contention makes all data inaccessible |
REST API (/api/) | Programmatic access for browser extensions, bulk operations, and integrations | Browser extension saves fail silently; automation scripts break |
| Browser Extensions (Chrome/Firefox) | One-click bookmark saving with automatic tag suggestions | Extension becomes non-functional if API token is invalid or server is unreachable |
| FTS5 Search Index | SQLite full-text search virtual table for fast bookmark search | Search returns no results or stale results after import or upgrade |
| Static File Server | Serves CSS, JS, and UI assets (handled by gunicorn or separate nginx) | UI renders without styles if static files are missing; functional but visually broken |
Uptime History
| Date | Incident Type | Duration | Impact |
|---|---|---|---|
| 2025-07-09 | Docker container OOM-killed during large bookmark import (15,000 entries); no restart policy set | 4 hours | Service completely down until manually restarted; browser extension silently failing |
| 2025-09-27 | SQLite database locked after unclean host shutdown during write operation | 1.5 hours | All reads and writes failed with "database is locked" until WAL cleanup ran |
| 2025-12-14 | Version upgrade to 0.9.x did not migrate FTS5 index; full-text search returned zero results | 2 days | Search appeared broken; bookmarks accessible by tag/URL but not content search |
| 2026-02-03 | Port 9090 conflict with newly installed Cockpit web console after server OS upgrade | 3 hours | Port not reachable; browser extension unable to connect; resolved by remapping port |
Monitor Linkding Automatically
Linkding's simplicity is a double-edged sword: when the single process goes down, there is no fallback, and your browser extension will silently fail to save every bookmark you click until you notice the problem manually. External monitoring closes that gap. ezmon.com monitors your Linkding endpoints from multiple external probes and alerts your team via Slack, PagerDuty, or SMS the moment port 9090 stops accepting connections or the API returns an unexpected status code.