FUTO Notes server errors, logs, and health check
The FUTO Notes server answers errors with an HTTP status code and a small JSON body, writes its logs to standard error, and reports its state at GET /health. In a Docker install, docker compose logs is where to look first.
Error responses
API errors are JSON with one error field:
{"error": "version conflict"}Expired or invalid sessions also carry a machine-readable code:
{"code": "invalid_session", "error": "session expired or invalid"}A 500 never says what went wrong. The details go to the server log instead, so nothing about your server’s internals leaks to a client.
HTTP status codes
| Status | Error text | What it means |
|---|---|---|
400 | invalid json, password is required, empty body, … | The request was malformed. Usually a client bug. |
401 | unauthorized | No session cookie or bearer token was sent. |
401 | invalid password | Sign-in with the wrong sync password. |
401 | session expired or invalid (code: invalid_session) | The session ended. Sessions last 7 days from sign-in, and activity does not extend them. The response also carries WWW-Authenticate: Bearer realm="futo-notes", error="invalid_token". |
404 | not found | The item does not exist, or it belongs to another user. The server never answers 403, so it doesn’t reveal what exists. |
405 | Method Not Allowed (plain text) | Wrong HTTP method for the path. |
409 | version conflict, blob is not staged, blob is in use, mutation still in progress, key conflict | Two writes collided, or a retry arrived while the first attempt was still running. These are part of normal sync, and the app resolves them. |
413 | blob too large, request too large | A blob over 100 MiB, or a batch over 32 MiB. If the body is an HTML page, the limit came from your reverse proxy instead; see reverse proxy setup. |
429 | too many requests | More than 10 sign-in attempts in 60 seconds from one address. Retry-After gives the seconds to wait. Only sign-in is rate limited. |
500 | internal server error | Something failed on the server. The log has the cause. |
503 | none (health check only) | /health could not reach the database. |
A 502, 504, or any HTML error page does not come from the FUTO Notes server, which only returns JSON or short plain-text errors. It comes from a proxy in front of it, usually because the server is down or restarting.
The /health endpoint
GET /health needs no sign-in. It pings the database with a 2-second timeout:
HTTP/1.1 200 OKContent-Type: application/json
{"status":"ok","db":"connected"}HTTP/1.1 503 Service UnavailableContent-Type: application/json
{"status":"degraded","db":"unreachable"}It checks the database only, not the blob directory or free disk space.
The Docker image runs this check every 30 seconds (5-second timeout, 3 retries), so docker compose ps shows the container as healthy or unhealthy. Docker does not restart an unhealthy container on its own; the restart policy acts only when the process exits.
GET / also needs no sign-in, and it returns the server’s name, version and auth mode, which is the quickest way to see what version you are running:
curl http://localhost:3005/There is no /metrics endpoint and no Prometheus exporter. For uptime monitoring, poll /health and alert on anything other than 200.
Where the logs are
The server logs to standard error only. It writes no log file, and the level and format are fixed (LOG_LEVEL is ignored).
With Docker Compose, from the install directory:
docker compose logs --tail 100docker compose logs -fdocker compose logs --since 1hWith the systemd unit from the README:
journalctl -u futo-notes-server -fHow to read the logs
Each line is key=value pairs with a timestamp, a level, and a message:
time=2026-09-23T17:08:11.798-07:00 level=INFO msg="applied database migrations" count=1 migrations=001_baselinetime=2026-09-23T17:08:11.798-07:00 level=INFO msg=listening addr=:3000time=2026-09-23T17:09:11.801-07:00 level=INFO msg=sessions summary="reaped 0"time=2026-09-23T17:09:11.803-07:00 level=INFO msg="blob GC" summary="purged 0 rows, removed 0 files"What gets logged:
- At startup: a
WARNfor each ignored legacy variable, any database migrations applied, andlistening. - Every
500: anERRORline with what the server was doing, the method, the path, and the underlying error. - Cleanup jobs: one line per job per run, hourly for sessions and every 6 hours for storage. See server storage.
Ordinary requests are not logged, and neither are 4xx errors. A wrong password or a 429 will not appear in the server log. Use your reverse proxy’s access log for that.
Startup errors
When the configuration is wrong, the server logs one ERROR and exits. Under Docker the container then restarts over and over, and docker compose ps shows it as Restarting.
| Log message | Fix |
|---|---|
FUTO_NOTES_PASSWORD or FUTO_NOTES_PASSWORD_HASH is required when AUTH_MODE=password | Set one of them. In Docker, check .env. |
set only one of FUTO_NOTES_PASSWORD or FUTO_NOTES_PASSWORD_HASH when AUTH_MODE=password | Remove one. |
AUTH_MODE must be dev or password, got "..." | Use password. |
PORT: expected an integer, got "..." | The same form of message appears for DB_POOL_MAX and DB_POOL_IDLE_TIMEOUT_MS. |
unsupported DATABASE_URL scheme (want postgres://, postgresql://, or sqlite:) | Fix DATABASE_URL. |
refusing to create fresh SQLite database ... contains blob files | The server lost track of its database. See the fresh-database safety guard. |
A malformed FUTO_NOTES_PASSWORD_HASH does not stop startup. Instead every sign-in fails with 500, and the log shows password login with an error such as unrecognized password hash format. See the hash format.
Panic recovery
If a bug makes a request handler crash (a Go panic), the server catches it, logs an ERROR line with msg=panic and the full stack trace, and answers that one request with a JSON 500 and Connection: close. The server keeps running and other requests are unaffected.
A crash in the middle of a streamed response, such as the live event stream or a batch download, cannot turn into a 500 because the response has already started. The client sees the connection drop.
A panic is always a server bug. Please report it, with the stack trace from the log, at the server repository.
Next steps
- Server configuration: fix a variable a startup error names.
- Reverse proxy setup: when the error comes from the proxy, not the server.
- Sync troubleshooting: what to check on the device side.