Skip to content

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

StatusError textWhat it means
400invalid json, password is required, empty body, …The request was malformed. Usually a client bug.
401unauthorizedNo session cookie or bearer token was sent.
401invalid passwordSign-in with the wrong sync password.
401session 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".
404not foundThe item does not exist, or it belongs to another user. The server never answers 403, so it doesn’t reveal what exists.
405Method Not Allowed (plain text)Wrong HTTP method for the path.
409version conflict, blob is not staged, blob is in use, mutation still in progress, key conflictTwo writes collided, or a retry arrived while the first attempt was still running. These are part of normal sync, and the app resolves them.
413blob too large, request too largeA 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.
429too many requestsMore than 10 sign-in attempts in 60 seconds from one address. Retry-After gives the seconds to wait. Only sign-in is rate limited.
500internal server errorSomething failed on the server. The log has the cause.
503none (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 OK
Content-Type: application/json
{"status":"ok","db":"connected"}
HTTP/1.1 503 Service Unavailable
Content-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 100
docker compose logs -f
docker compose logs --since 1h

With the systemd unit from the README:

journalctl -u futo-notes-server -f

How 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_baseline
time=2026-09-23T17:08:11.798-07:00 level=INFO msg=listening addr=:3000
time=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 WARN for each ignored legacy variable, any database migrations applied, and listening.
  • Every 500: an ERROR line 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 messageFix
FUTO_NOTES_PASSWORD or FUTO_NOTES_PASSWORD_HASH is required when AUTH_MODE=passwordSet one of them. In Docker, check .env.
set only one of FUTO_NOTES_PASSWORD or FUTO_NOTES_PASSWORD_HASH when AUTH_MODE=passwordRemove 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 filesThe 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