EQProxy Load balancer and reverse proxy in Rust

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=…}.

CounterWhat it measures
eqproxy_http_requests_proxied_totalHTTP exchanges served (h1, h2, h3, gRPC)
eqproxy_connections_accepted_total, _rejected_totalAccepted and rejected by maxconn
eqproxy_no_available_server_totalRequests with no eligible server
eqproxy_connect_failures_totalFailures connecting to the backend
eqproxy_header_timeouts_totalHeader timeouts
eqproxy_rate_limited_total, eqproxy_auth_denied_totalRejections by rate limit and by authentication
eqproxy_config_reloads_total, eqproxy_config_reload_failures_totalReloads applied and rejected
eqproxy_backend_connections_reused_totalConnections reused from the pool
eqproxy_cache_hits_total, _misses_total, _revalidations_totalCache performance
eqproxy_map_reload_failures_totalMap updates rejected
eqproxy_access_log_dropped_totalAccess 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.

{"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

WhatHow
Full configurationSIGHUP to the process: re-reads the file, validates, resolves and switches generation atomically
TLS certificatesPOST /api/v1/certificates/reload or SIGHUP; new handshakes use the new certificate without touching established connections
Data mapsOn their own, every reload_interval, without reloading the configuration
Specific backends and listenersPUT /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

SignalEffect
SIGINTOrdered shutdown: stops accepting, drains what is live until shutdown_grace and exits with code 0
SIGHUPHot 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

SymptomProbable causeWhere to look
503 with no_available_serverAll servers down, drained or at their maxconnGET /api/v1/status, eqproxy_no_available_server_total
Immediate 502connect_timeout or connection refused by the backendeqproxy_connect_failures_total
504server_header_timeout or max_stream_durationeqproxy_header_timeouts_total
408 and closeThe client does not send the header in timeclient_header_timeout
413limits.max_body_bytes exceededconfig explain
429Rate limit of the ruleeqproxy_rate_limited_total
400 on a request valid for another proxyAmbiguous framing: EQProxy rejects what does not have a single readingProcess log
Reload that changes nothingInvalid candidate or 409 in the APIeqproxy_config_reload_failures_total and the log
Missing access log linesSink backpressureeqproxy_access_log_dropped_total

Checklist before approving a change

  1. eqproxy config validate --config eqproxy.toml returns config OK.
  2. eqproxy config diff actual.toml candidata.toml can be read and there are no surprises.
  3. eqproxy doctor --config eqproxy.toml ends with doctor: PASS.
  4. eqproxy config explain on the critical routes picks the expected backend and the timeouts are the ones you think.
  5. GET /api/v1/status reports the new revision and the servers in the correct state.
  6. The rejection counters (no_available_server, connect_failures, rate_limited) do not move after the change.