Troubleshooting¶
Known failure modes, each as cause and fix. They are grouped by when you hit them: standing the stack up, enrolling agents, and running in production.
Installation and startup¶
The stack will not come up: FATAL: sorry, too many clients already¶
Some services start; the rest fail to acquire database connections and exit. Nothing reaches a healthy state. Which services survive varies between restarts, because it is a race for a fixed pool of connections.
Cause. max_connections is below what the stack reserves. It reserves
103 — and PostgreSQL's default is 100, so an unmodified database is already
three short. HikariCP fills each pool to its maximum eagerly rather than on
demand, so the demand is immediate and permanent rather than gradual, and
probe-scheduler alone reserves 58 of the 103 (its pool is derived from its
50 dispatch workers). See
Scaling for the full table.
Fix. Start PostgreSQL with max_connections=160, as the bundled Compose
file does:
On a database you did not start from that Compose file, set it in
postgresql.conf and restart — max_connections is not reloadable, a SIGHUP
will not take. Confirm with SHOW max_connections; rather than assuming.
If you cannot raise it, lower the demand instead: reduce
SCHEDULER_DISPATCH_WORKERS on probe-scheduler (its pool follows) and
DB_POOL_SIZE on the services that honour it. Do not pin DB_POOL_SIZE on the
scheduler alone — a pool below the worker count turns the shortage into
30-second connection timeouts and lost probes instead of a clean startup
failure.
DB_POOL_SIZE does not apply everywhere
aggregate-worker, metrics-service and realtime-service pass
maximumPoolSize = 5 in code, which overrides the environment variable.
Their pools are 5 whatever you set. Count them as 5 each when budgeting.
See Scaling.
Postgres will not start: could not access file "timescaledb"¶
Only after upgrading from a release whose Compose file pulled
timescale/timescaledb:latest-pg16. Postgres exits immediately with
FATAL: could not access file "timescaledb": No such file or directory.
Cause. That image wrote shared_preload_libraries = 'timescaledb' into
postgresql.conf inside the data volume when the volume was first initialised.
The setting lives in the volume, not the image, so it survives the switch to a
plain postgres image — which does not ship the library.
Fix. That volume is also a PostgreSQL 16 cluster, and the current stack
runs PostgreSQL 18, which cannot open it — so the way through is the
dump-and-restore in Upgrading: dump
with the old image, restore into a fresh volume. The dump may carry a
CREATE EXTENSION timescaledb line, which is safe to delete. Nothing in the
schema used the extension, so nothing is lost.
If you are staying on a PostgreSQL 16 image for now, removing the setting from the existing volume is enough, and does not need the database to be running:
docker compose down
# The Compose project is named "tracedown", so the volume is prefixed.
# Confirm yours with: docker volume ls
docker run --rm -v tracedown_tracedown-pgdata:/pg alpine \
sed -i "s/^shared_preload_libraries = 'timescaledb'/#&/" /pg/postgresql.conf
Postgres will not start: there appears to be PostgreSQL data in¶
Only after upgrading to a release whose Compose file pulls postgres:18-alpine
while keeping a volume that an older image initialised. The container exits
with Error: in 18+, these Docker images are configured to store database data
in a format which is compatible with "pg_ctlcluster" and names the directory
it found.
Cause. The volume holds a PostgreSQL 16 cluster. The 18 image keeps its
cluster in a version-specific subdirectory (/var/lib/postgresql/18/docker)
and refuses to start next to data it did not create, rather than silently
initialising an empty database beside it.
Fix. Restore a dump into a fresh volume — the procedure is in Upgrading. The refusal is the safe outcome: the old data is untouched until you remove the volume yourself.
The Docker build fails at a COPY step¶
Cause. The backend's Docker build context is the parent of the
repository root, so the repository must sit at core/tracedown-core-backend
inside a containing directory. Cloned anywhere else, the build fails with a
"not found" error at the first COPY step.
Fix. Clone into the tree shown in Requirements.
realtime-service will not start¶
Cause. Its DATABASE_URL, DATABASE_USER, DATABASE_PASSWORD and
REDIS_A_URL are mandatory HOCON substitutions with no defaults. A missing
one fails config resolution at startup rather than degrading at runtime.
Fix. Set all four. See Configuration.
Agents and certificates¶
An agent registers but never receives work¶
The agent is up, enrolled, and idle.
Cause. The scheduler dials the agent — the agent does not poll for work. It must therefore be reachable inbound from the scheduler. An agent behind NAT with no inbound route registers successfully and then never hears from anyone, which is why this looks like a scheduling bug rather than a networking one.
Fix. Give the scheduler a route to the agent's URI. Also confirm the agent is active and passing health challenges — see Monitoring Tracedown and Probe Agents.
"CA root not initialized — run --agent-bootstrap first"¶
Cause. No active CA row exists in the database.
Fix. Run --agent-bootstrap, which creates it. This is exactly what the
compose tracedown-ca-init step does:
A bootstrap token is rejected¶
Cause. Tokens are single-use with a 1 hour TTL. A reused or stale token is refused.
Fix. Generate a fresh one with --agent-bootstrap.
An agent refuses to enrol: "could not authenticate the gateway"¶
Cause. The agent authenticates the gateway before the bootstrap token leaves the process, and it could not. Usually the gateway's certificate comes from a private CA the agent's system trust store does not carry, or is self-signed. Registration was abandoned before the token was sent, so nothing was leaked and the token is still unused — but it is still ticking towards its 1-hour expiry.
Fix. Set PROBE_AGENT_BOOTSTRAP_CA_BUNDLE to that CA's PEM bundle, or
PROBE_AGENT_BOOTSTRAP_PIN_SHA256 to the fingerprint of the certificate the
gateway presents. See Authenticating the gateway at
enrolment.
An agent refuses to enrol in production over http://¶
Cause. DEPLOYMENT_ENV=production and PROBE_AGENT_SCHEDULER_URL is not
https://, so the token would cross the wire in the clear. The agent raises
instead. PROBE_AGENT_INSECURE_SKIP_BOOTSTRAP_TLS_VERIFY is refused in
production too, so it is not a way round this.
Fix. Point the agent at an https:// URL that terminates TLS in front of
the gateway. The nginx.conf and apache.conf shipped in docker/deploy/
already proxy the enrolment paths; a vhost you wrote yourself may not, and a
request that misses them is answered with the dashboard's index.html rather
than by the gateway. See Reaching enrolment over
https. On a private network
where the agent and the gateway share the Docker network, the alternative is to
leave DEPLOYMENT_ENV unset on the agent container, which is what the local
stacks do.
Agent renewal skipped with an unknown-slug warning¶
Cause. The agent cannot determine its own slug. It is normally persisted at
/certs/agent-slug.txt; if that file is absent — typically a lost or recreated
cert volume — renewal has nothing to renew against and skips.
Fix. Set PROBE_AGENT_SLUG explicitly.
Variables decrypt as garbage, or agent mTLS fails after a config change¶
Cause. PLATFORM_AES_KEY differs between api-gateway, probe-scheduler and
notification-dispatcher. All three must share the exact same value — the
gateway encrypts, the other two decrypt. The dispatcher needs it for org
variables referenced from webhook URLs.
Fix. Make the value identical across all three. Note that data encrypted under a previous key is not recoverable by setting a new one — see Secrets & Encryption.
Running¶
Probes are skipped, and the banner says over capacity¶
Users see runs marked skipped and a dispatch_capacity banner. A skipped run
never reached an agent; three different faults produce one, and the banner says
which. The two entries after this one cover the others.
Cause. Either the dispatch queue overflowed (SCHEDULER_DISPATCH_QUEUE_SIZE,
default 100000 — it must exceed your largest per-tick fleet), or a service's
next tick came due while its previous tick was still waiting in the queue.
Both are capacity sheds; a run suppressed by the service's queue policy is
dropped silently and does not appear as skipped.
Fix. For overflow, raise the queue size — entries are tiny. For sustained
capacity pressure, add agents. Resist raising
SCHEDULER_DISPATCH_WORKERS: it is the global backpressure limit, and raising
it risks congestion collapse against slow targets. See Scaling.
Probes are skipped with no agent available¶
The alert is no_eligible_agent.
Cause. The tick found nothing to dispatch to. Either no agent is currently passing its health challenge, or the affected services are restricted to agents that are not — a service pinned to one agent has nowhere to go the moment that agent stops being eligible.
Fix. Check agent health first (Settings → Agents, or Monitoring Tracedown); a fleet that looks empty to the scheduler is often one failing challenge away from working. If the fleet is healthy, check the affected services' agent pickers — see Services.
Probes are skipped although the agents look healthy¶
The alert is agent_dispatch_failed.
Cause. Agents were eligible — they passed a challenge within the last minute — and every one of them was tried without a single one taking the run. The connection was refused, the handshake did not complete, or the job was turned away. This is what an agent lost between health rounds looks like: a container killed, a network path closed, or a certificate the scheduler no longer trusts.
Fix. Confirm the agent processes are running and reachable inbound from the scheduler on port 8443, and that their certificates are current. See Probe Agents and Certificate Authority.
Runs are recorded with status error¶
Cause. The run did not evaluate. Either the script or the executor failed, or the agent took the job and broke while running it — it answered HTTP 500, or with something that is not a probe result. Such a run is never re-dispatched: the agent may already have called the monitored target, and retrying would probe it twice for one tick.
Fix. The diagnostic is stored with the run, on its Raw result tab. If it points at the agent rather than the script, the agent's own logs have the rest. See Reading Results.
Agent statuses are frozen and health checks report as unavailable¶
The alert is health_token_unavailable, and it names an endpoint rather than an
agent.
Cause. A health challenge requires the agent to fetch a one-time token from
the gateway. The scheduler could not reach that endpoint itself, or could not
store the token in Redis A to begin with — so the round proves nothing about the
agent and is discarded as inconclusive rather than held against it. Usually the
gateway is down, Redis A is unreachable, or GATEWAY_URL on the scheduler points
somewhere the scheduler cannot actually reach.
Fix. Check the gateway and Redis A, then GATEWAY_URL — its default
http://localhost:8080 is not the gateway's own port and must be set
explicitly. See Configuration and
Monitoring Tracedown.
The mirror-image fault reads as agent_down
GATEWAY_URL has to be reachable by the agents too — the scheduler
hands each one GATEWAY_URL plus /internal/health/token/{challengeId} to
fetch. A value only the scheduler can resolve, such as the internal Docker
address against an agent on another host, produces the opposite symptom:
the scheduler reaches the endpoint, so the round is not excused, and the
agent is marked down instead. Agents off the Docker network need the public
https URL there, which the shipped vhosts proxy — see Reaching enrolment
over https.
Aggregation or retention work is duplicated¶
Cause. More than one aggregate-worker replica is running. It has no distributed lock — its jobs are plain coroutine loops that assume they are alone.
Fix. Run exactly one replica. See Retention & Aggregation.
Emails are not sent¶
Cause, the common one. EMAIL_PROVIDER defaults to console, which only
logs the message.
Cause, the subtle one. The variable is set on the wrong service.
email-service is the only process that talks to a mail provider — the gateway
and the notification-dispatcher publish envelopes onto the email_queue in
Redis A and never open an SMTP connection of their own. Provider settings
(EMAIL_PROVIDER, EMAIL_FROM_ADDRESS, EMAIL_SMTP_*, EMAIL_RESEND_API_KEY,
EMAIL_MAILGUN_*) therefore only do anything on email-service; set on any other
service they are ignored, and mail keeps going to a log.
Fix. Configure the provider on email-service and check its logs, not the sending service's. See Configuration.
The usage window is shorter than expected¶
Cause. RESULT_RETENTION_DAYS caps it — you cannot display history that has
been deleted. The gateway's value must match the aggregate-worker's, or the
dashboard advertises a window the worker has already pruned.
Fix. Set RESULT_RETENTION_DAYS identically on both. See
Retention & Aggregation.
Probes against a domain are limited to 3 calls, save no bodies, and run no more than every 5 minutes¶
Cause. TRUSTED_DOMAIN_MODE is false and the target domain is unverified.
The anti-abuse policy then caps the script at 3 calls, disables body saving, and
enforces a 5-minute minimum interval. Every tick the policy withholds is
recorded as a skipped result whose reason names the rule — unverified_throttle
(ran too soon after the previous run), unverified_max_calls (more than 3
calls) or unverified_includes (includes() against an unverified domain) —
so the gap is explicable from the history; no alert is raised for them.
Fix. Verify the domain (Settings → Domains), or set TRUSTED_DOMAIN_MODE=true
if every target is one you control — this turns the ownership checks off.
Verification is the default; it signals good-faith use of the platform. See the
User Manual and
Configuration.