Operations
How to tell whether EQProxy is healthy, what can be changed without a restart and what to look at when something fails. Everything on this page rests on counters, log lines and commands that the binary produces.
Health checks
A backend without [backend.*.health_check] considers all its servers operational. With the section, checks are active and periodic: type = "tcp" only connects; type = "http" makes a real exchange and evaluates expect_status and, if configured, expect_rstring.
A server falls after fall consecutive failed checks and comes back after rise successes. backup = true servers are checked also while unused, so that the first request after a primary fails does not wait for a probe. With passive = true, real traffic —5xx and connection failures— feeds the same state machine.
When no eligible server remains: 503 in HTTP mode, close in TCP mode, and the no_available_server counter rises in both cases. The drain and maintenance states are set through the API; a drain ends when the last in-flight exchange releases its guard, and that is logged only once.
Metrics
GET /metrics on the administration plane serves Prometheus text of bounded cardinality: 62 counters without labels, plus eqproxy_connections_active, eqproxy_config_reload_last_swap_seconds and eqproxy_build_info{version=…}.
| Counter | What it measures |
|---|---|
eqproxy_http_requests_proxied_total | HTTP exchanges served (h1, h2, h3, gRPC) |
eqproxy_connections_accepted_total, _rejected_total | Accepted and rejected by maxconn |
eqproxy_no_available_server_total | Requests with no eligible server |
eqproxy_connect_failures_total | Failures connecting to the backend |
eqproxy_header_timeouts_total | Header timeouts |
eqproxy_rate_limited_total, eqproxy_auth_denied_total | Rejections by rate limit and by authentication |
eqproxy_config_reloads_total, eqproxy_config_reload_failures_total | Reloads applied and rejected |
eqproxy_backend_connections_reused_total | Connections reused from the pool |
eqproxy_cache_hits_total, _misses_total, _revalidations_total | Cache performance |
eqproxy_map_reload_failures_total | Map updates rejected |
eqproxy_access_log_dropped_total | Access log lines lost to backpressure |
With [metrics] detailed = true, eqproxy_server_requests_total and eqproxy_server_errors_total are added (labels backend, server) and eqproxy_route_requests_total (listener, route). max_series counts leaves across both families: a series that does not fit is rejected and counted in eqproxy_metrics_detail_dropped_total, never admitted silently. Four series without labels —_enabled, _series, _budget and _dropped_total— make it possible to tell "no traffic", "detail off" and "the budget rejected it" apart.
Without an administration plane there is observability too: every --metrics-interval (30 s by default) the process writes METRICS with the counter snapshot to the error output, and a final METRICS … (final) line on shutdown.
Logs
There are two distinct streams.
- Process log: goes to the error output, with
INFO,ERROR,METRICSandAUDITlines. Example:INFO listener web listening on 0.0.0.0:8080. - Access log: opt-in with
[access_log]. One line per HTTP request reaching the engine —each HTTP/2 stream and each HTTP/3 request counts as one—, in JSON or in nginx'scombinedform.
{"time":"2026-10-25T03:31:17.518Z","listener":"web","client":"127.0.0.1:51632","protocol":"http/1.1","method":"GET","path":"/probe","host":"127.0.0.1:18181","status":200,"duration_ms":0,"backend":"api","server":"127.0.0.1:18180","user_agent":"curl/8.7.1","bytes":22}
The query string is never logged. A header that failed to parse is logged with - fields; an exchange that ended before the response carries status 0. The sink is a bounded channel of 4096 entries with its own task: if it fills up or fails, lines are lost and access_log_dropped rises, but a request is never delayed.
Hot reload
| What | How |
|---|---|
| Full configuration | SIGHUP to the process: re-reads the file, validates, resolves and switches generation atomically |
| TLS certificates | POST /api/v1/certificates/reload or SIGHUP; new handshakes use the new certificate without touching established connections |
| Data maps | On their own, every reload_interval, without reloading the configuration |
| Specific backends and listeners | PUT /api/v1/backends/{name} and PUT /api/v1/listeners/{name} |
docker kill --signal=HUP eqproxy # the container forwards the signal
A candidate that does not validate changes nothing: the reason is logged, config_reload_failures rises and the previous generation keeps serving. What is not reloadable: the administration plane's endpoint and tokens, the access log's destination and format, max_series and, on a live QUIC listener, ALPN, client_ca and the TLS version range — a change there is rejected and asks for a restart, it is never applied halfway.
Signals
| Signal | Effect |
|---|---|
SIGINT | Ordered shutdown: stops accepting, drains what is live until shutdown_grace and exits with code 0 |
SIGHUP | Hot reload of the configuration |
The binary installs handlers for those two signals; there is no handler of its own for SIGTERM, so its default action terminates the process without draining. If you need an ordered shutdown, send SIGINT.
Limits and typical errors
| Symptom | Probable cause | Where to look |
|---|---|---|
503 with no_available_server | All servers down, drained or at their maxconn | GET /api/v1/status, eqproxy_no_available_server_total |
Immediate 502 | connect_timeout or connection refused by the backend | eqproxy_connect_failures_total |
504 | server_header_timeout or max_stream_duration | eqproxy_header_timeouts_total |
408 and close | The client does not send the header in time | client_header_timeout |
413 | limits.max_body_bytes exceeded | config explain |
429 | Rate limit of the rule | eqproxy_rate_limited_total |
400 on a request valid for another proxy | Ambiguous framing: EQProxy rejects what does not have a single reading | Process log |
| Reload that changes nothing | Invalid candidate or 409 in the API | eqproxy_config_reload_failures_total and the log |
| Missing access log lines | Sink backpressure | eqproxy_access_log_dropped_total |
Checklist before approving a change
eqproxy config validate --config eqproxy.tomlreturnsconfig OK.eqproxy config diff actual.toml candidata.tomlcan be read and there are no surprises.eqproxy doctor --config eqproxy.tomlends withdoctor: PASS.eqproxy config explainon the critical routes picks the expected backend and the timeouts are the ones you think.GET /api/v1/statusreports the new revision and the servers in the correct state.- The rejection counters (
no_available_server,connect_failures,rate_limited) do not move after the change.