productivity

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

SymptomLikely CauseResolution
All access stops; port 9090 not respondingDjango/gunicorn process OOM-killed by kernel; Docker container exitedRestart container; set memory limits appropriately; check docker logs linkding for OOM message
SQLite database error on startup or queries return garbled dataDatabase 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 savingAPI token missing or revoked in Linkding settingsGenerate a new API token in Linkding Settings → API and update extension options
Import of large Netscape HTML file times out at 30 secondsDefault gunicorn worker timeout exceeded processing thousands of bookmarksSplit import file into smaller chunks; increase gunicorn --timeout via LD_REQUEST_TIMEOUT env var
Full-text search returns no results after version upgradeSearch index not rebuilt; SQLite FTS5 virtual table needs regenerationRun python manage.py rebuildindex inside container; or use the admin UI rebuild option
Port 9090 conflicts with another service after host rebootAnother application (Cockpit, custom service) now occupying port 9090Change Linkding port via -p 9091:9090 in Docker run command; update browser extension URL

Architecture Overview

ComponentFunctionFailure Impact
Gunicorn / DjangoWSGI application server handling all HTTP requests and business logicComplete service unavailability; port stops accepting connections
SQLite DatabaseSingle-file storage for all bookmarks, tags, users, and API tokensAny corruption or lock contention makes all data inaccessible
REST API (/api/)Programmatic access for browser extensions, bulk operations, and integrationsBrowser extension saves fail silently; automation scripts break
Browser Extensions (Chrome/Firefox)One-click bookmark saving with automatic tag suggestionsExtension becomes non-functional if API token is invalid or server is unreachable
FTS5 Search IndexSQLite full-text search virtual table for fast bookmark searchSearch returns no results or stale results after import or upgrade
Static File ServerServes 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

DateIncident TypeDurationImpact
2025-07-09Docker container OOM-killed during large bookmark import (15,000 entries); no restart policy set4 hoursService completely down until manually restarted; browser extension silently failing
2025-09-27SQLite database locked after unclean host shutdown during write operation1.5 hoursAll reads and writes failed with "database is locked" until WAL cleanup ran
2025-12-14Version upgrade to 0.9.x did not migrate FTS5 index; full-text search returned zero results2 daysSearch appeared broken; bookmarks accessible by tag/URL but not content search
2026-02-03Port 9090 conflict with newly installed Cockpit web console after server OS upgrade3 hoursPort 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.

Set up Linkding monitoring free at ezmon.com →

linkdingbookmarksself-hostedbrowser-extensionlinksstatus-checker