Common issues
First-run and daemon startup issues: port 9420 conflicts, systemd-user linger and XDG_RUNTIME_DIR on Linux, and Windows service gotchas.
This page covers first-run and daemon startup failures that are not hardware, audio, or network related. They share a common character: the daemon either silently refuses to start, starts with reduced capability, or appears healthy while one subsystem is broken.
Run the built-in self-check first; it catches most of these automatically:
hypercolor diagnoseFor devices not showing up at all, see Devices not found. For audio-reactive effects staying static, see Audio not reacting.
#Port 9420 already in use
Symptom: The daemon exits immediately with an error containing failed to bind API server to 127.0.0.1:9420 or Os error 98 (address already in use).
Why it happens: Only one process can bind a given address and port. The daemon has a single-instance guard that prints hypercolor-daemon is already running; exiting when it fires correctly, but if the previous process died uncleanly (or something else claimed port 9420), the TCP bind fails before the guard is relevant.
Find what is on the port:
ss -tlnp | grep 9420Three outcomes:
Another
hypercolor-daemonprocess. A previous instance did not exit cleanly. Stop it:hypercolor service stop # or directly: pkill -x hypercolor-daemonThen start fresh:
hypercolor service start.A different process. Something else owns port 9420. Change Hypercolor’s port in
~/.config/hypercolor/hypercolor.toml:[daemon] port = 9421Tell the CLI where to reach the daemon, either per-command or exported for the session:
export HYPERCOLOR_PORT=9421Nothing is there. The port is free but the error appeared anyway. This is rare and usually means a stale Unix socket or a loopback-interface permission issue. Check
hypercolor service statusand review the log:hypercolor service logs --lines 50
Verify the daemon is accepting connections:
curl -i http://localhost:9420/healthA 200 OK with a JSON body (status, version, uptime_seconds, checks) means the daemon is up and healthy. A 503 means it is running but a subsystem is degraded. Connection refused means it is not running.
#Systemd user service fails on first login (Linux)
Symptom: hypercolor service status shows the service as failed or not started after hypercolor service enable. Running hypercolor service start manually works, but it does not start automatically at login.
Why it happens: Hypercolor installs a systemd user unit at ~/.config/systemd/user/hypercolor.service, managed with systemctl --user. The unit is ordered after graphical-session.target:
After=graphical-session.target dbus.socket
Wants=graphical-session.targetOn headless or SSH-only machines graphical-session.target is never reached, so the service never starts automatically. A second issue on headless systems: user systemd sessions are created at login and destroyed at logout, so a daemon started via SSH dies when you disconnect.
Fix for desktop systems (graphical session present):
The service should start on login. If it does not, confirm it is enabled:
systemctl --user is-enabled hypercolorIf the output is disabled:
hypercolor service enable
# equivalent to:
systemctl --user enable hypercolorFix for headless and server systems:
Enable systemd user linger so the user session persists when no one is logged in:
loginctl enable-linger "$USER"With linger active the user’s systemd instance starts at boot and keeps running indefinitely. The hypercolor service can then activate without a graphical session.
Verify linger took effect:
loginctl show-user "$USER" | grep Linger
# Linger=yesReload and restart:
systemctl --user daemon-reload
systemctl --user restart hypercolor
systemctl --user status hypercolorLinger requires systemd 230 or later, which is standard on any distribution shipping systemd since 2016. If loginctl enable-linger returns an error, confirm that systemd-logind is running: systemctl status systemd-logind.
#XDG_RUNTIME_DIR not set (Linux, silent failure)
Symptom: The daemon starts and LEDs light up, but audio-reactive effects stay static, D-Bus integration is absent, or PipeWire screen capture fails. No error is logged. This appears most often on headless or SSH-only machines.
Why it happens: XDG_RUNTIME_DIR points to a per-user runtime directory (/run/user/$UID on most systems) that holds sockets for PipeWire, PulseAudio, D-Bus, and XDG portals. systemd-logind creates it when a user session starts and removes it when the last session ends.
When the daemon runs outside an active user session (started via cron, or via linger but before a first interactive login), XDG_RUNTIME_DIR may not exist. The daemon treats the variable as optional and starts successfully, but any subsystem that depends on the runtime directory to locate its socket degrades silently.
Specifically affected:
- Audio. PipeWire locates its socket at
$XDG_RUNTIME_DIR/pipewire-0and PulseAudio at$XDG_RUNTIME_DIR/pulse/native. Without the directory, audio capture is unavailable and audio-reactive effects are static. - D-Bus session bus. The session bus socket lives at
$XDG_RUNTIME_DIR/bus. Desktop notifications and MPRIS media-player control will not connect. - Screen capture.
xdg-desktop-portal(which manages PipeWire screen sharing) depends on the runtime directory.
The daemon does not log a warning when XDG_RUNTIME_DIR is missing. Silent static effects on a server install with no errors in the log is the tell. Check the runtime directory first.
Diagnose:
# Check whether the directory exists
ls /run/user/$(id -u)/
# Check for audio sockets
ls /run/user/$(id -u)/pipewire-0 2>/dev/null || echo "PipeWire socket missing"
ls /run/user/$(id -u)/pulse/native 2>/dev/null || echo "PulseAudio socket missing"
# Check what the daemon sees
hypercolor audio devicesFix on desktop systems:
Log out and log back in. systemd-logind recreates the runtime directory and restarts the user services that populate it. If the problem persists after a fresh login, check that systemd-logind is running:
systemctl status systemd-logindFix on headless systems:
Enable linger first (see Systemd user service fails on first login above). With linger active, systemd-logind maintains XDG_RUNTIME_DIR even without an active session.
If audio or D-Bus integration is required on a headless machine, enable socket activation for those services explicitly:
systemctl --user enable --now pipewire.socket pipewire-pulse.socketFor audio-reactive effects on a headless server you may also need a virtual sink or a hardware audio device accessible to the daemon’s user. See your distribution’s documentation for headless PipeWire or PulseAudio setup.
#Windows: service mode, hardware support, and firewall
Three Windows-specific situations look like general breakage but have narrow causes.
A daemon installed as a Windows service sees no keyboard or mouse input. Raw Input cannot cross the session boundary: a service runs on its own window station and never receives your desktop’s input, and no error is produced anywhere. Input-reactive effects silently do nothing. Run the daemon in your own session (the desktop app does this for you) rather than as a service. Full detail in Input capture.
PawnIO hardware setup failed during install. The installer’s hardware-access step (PawnIO SMBus modules plus the HypercolorSmBus broker service) can fail without failing the install. USB and network lighting still work, but motherboard and DRAM SMBus lighting stays dark. Re-run the setup from Settings → Device Discovery → Hardware Support in the app, or run install-windows-hardware-support.ps1 (shipped in the tools folder of the install directory, and as scripts/install-windows-hardware-support.ps1 in the repo) from an elevated PowerShell.
Windows Firewall prompts when the daemon binds beyond loopback. The installer pre-grants inbound firewall rules for the installed daemon and app, so a default install never prompts. A daemon run from a different path (a source build, a portable copy) or reconfigured to bind non-loopback addresses triggers the standard Windows Firewall prompt; allow it, or add an equivalent inbound rule, otherwise mDNS discovery responses are dropped.
#WebSocket preview not connecting
Symptom: The daemon is running and devices are lit up, but the web UI canvas preview shows a black rectangle or the TUI spectrum display is empty. The effect is applying to LEDs correctly.
Why it happens: The canvas preview and spectrum data travel over a WebSocket connection to /api/v1/ws on port 9420. The preview channel is separate from the LED output pipeline, so devices can light up even when the WebSocket handshake fails.
Common causes:
Browser extension or proxy blocking the upgrade. Ad blockers and privacy extensions sometimes intercept WebSocket connections. Try disabling extensions or opening the UI in a private window.
Reverse proxy not forwarding WebSocket. If Hypercolor is behind nginx or Caddy for remote access, the proxy must forward the
Upgradeheader. A misconfigured proxy returns400 Bad Requeston the handshake, leaving the preview dark while the REST API works.Browser origin refused. The WebSocket upgrade always admits loopback origins (
localhost,127.0.0.1, and[::1], on any port), the desktop app, the daemon’s own origin, and any upgrade that carries an API key, so the bundled UI opened atlocalhostor at the daemon’s own address never hits this, and neither does a remote UI you connected with the key. It bites a page served from some other origin, such as a UI dev server on another host, when the daemon has no API key: a keyless daemon refuses every other browser origin, andcors_originsdoes not apply. SetHYPERCOLOR_API_KEY, chooseaccess_mode = "lan_protected", and enter the key when the UI asks for its Network API Key. On a keyed daemon, listing an origin incors_originsin~/.config/hypercolor/hypercolor.tomllets a page from that origin connect without the key, but only from a browser on the daemon’s own machine; a remote browser still needs the key:[web] cors_origins = ["https://lights.example.com"]
Verify the WebSocket endpoint directly:
# requires: cargo install websocat
websocat ws://localhost:9420/api/v1/wsYou should see a stream of JSON events. If the connection is refused or immediately closed, check the daemon log:
hypercolor service logs --lines 50If events arrive in websocat but the browser preview is still dark, the problem is on the browser side. Open the developer console and look for WebSocket errors in the Network tab.
#403: “not a name this daemon answers to”
Symptom: the web UI loads from a name like http://hypercolor.home:9420, but every API call fails with 403 forbidden and a message saying the host is not a name the daemon answers to without an API key.
Cause: a request without an API key has to call the daemon by an IP address, localhost, one of the machine’s own host names, or the daemon’s mDNS name. That rule keeps a malicious web page from reaching your daemon through DNS rebinding. A name from your router’s DNS is none of those.
Fix: add the name to network.allowed_hosts and restart the daemon:
[network]
allowed_hosts = ["hypercolor.home"]Or browse to the daemon’s IP address instead, or use an API key. See Host names.
If the name belongs to a reverse proxy in front of the daemon, give the daemon an API key instead: a keyless daemon refuses relayed requests unless you have opened keyless remote access on purpose (see Behind a reverse proxy).
#Undoing an unwanted change
Symptom: Your lights changed, or a scene, layout, or device setting is not what you left it, and you did not change it.
Find out what changed it. Every request that changes state (a REST POST, PUT, PATCH, or DELETE, a WebSocket command with one of those methods, or an MCP tool call that is not read-only) is recorded with its time, method, path, status, the client’s socket address, any address it claimed through X-Forwarded-For or X-Real-IP (kept separately, since any local process can send those headers), its user agent, and the state files it changed. A request whose client disconnects before the response is still recorded, with status 499, once the work it started has finished. Request bodies, query strings, credentials, and tool arguments are never recorded. Ask the daemon for the newest entries:
curl -s 'http://127.0.0.1:9420/api/v1/system/audit?limit=20'The same entries are kept on disk as JSON Lines, one object per line, newest at the end of the file, so they survive restarts. The file is logs/api-audit.jsonl in the daemon’s state directory:
${XDG_STATE_HOME:-~/.local/state}/hypercolor/logs/api-audit.jsonl # Linux
~/Library/Application Support/hypercolor/logs/api-audit.jsonl # macOS
%LOCALAPPDATA%\hypercolor\logs\api-audit.jsonl # WindowsThe file rotates at 4 MiB into api-audit.1.jsonl through api-audit.4.jsonl, about 70,000 entries in all. Each entry also appears in the daemon log at info level.
Put the previous state back. The daemon keeps the last ten versions of each state file under history/ in the same state directory (see state_history_generations in Configuration). List them:
hypercolor-daemon history list # every store
hypercolor-daemon history list runtime-state # just the live session
hypercolor-daemon history list scenes --jsonEach version shows when it was replaced and when it was originally saved. Stop the daemon, restore the version you want, and start it again:
hypercolor service stop
hypercolor-daemon history restore runtime-state 12
hypercolor service startrestore refuses to run while the daemon is up, because the daemon would write its in-memory state over the restored file. The restore saves the content it replaced as a new version and prints its number, so restoring that number undoes it. Run the command as the same user, with the same XDG_* environment, as the daemon, and pass --config <path> if the daemon uses a non-default config file.
The stores with history are config, layouts, layout-auto-exclusions, scenes, logical-devices, attachment-profiles, display-preferences, device-settings, simulated-displays, runtime-state, device-aliases, and library. Credentials, discovery caches, and migration journals keep no history.