Getting startedGuidesReferenceChangelog
Apoxy:// Docs / Reference / Metrics API

Metrics API

Read request rates, error counts, latency percentiles, and byte volumes for your project through typed time-series and snapshot endpoints, or scrape a self-hosted gateway pod with Prometheus.

The metrics API serves traffic measurements for a project from the project API server, backed by aggregated telemetry. It has two shapes: a snapshot on an object (current totals for one Gateway, route, Proxy, Service, VPC network, or tunnel) and a series query (one or more lines over time). Both read the same measurements, so a tile and the chart under it agree.

The rule for picking one: one object and you want its current numbers, read /metrics on that object. Many objects, or any view over time, use metrics/<name>/series.

This API measures traffic. For the health of the proxy process itself, read the envoy.* recipes: see Proxy health in the metrics API. A gateway you run yourself publishes the same signals as Prometheus series on the pod as well, covered in Gateway pod metrics (self-hosted) at the bottom of this page.

Endpoint and authentication

All requests go to your project API server:

$terminalTXT
https://<project-id>.api.apoxy.dev

Authenticate with an API key in the X-Apoxy-API-Key header, the same as every other call to this API:

$terminalTXT
X-Apoxy-API-Key: <api-key>

Authorization: Bearer <api-key> is also accepted. To create a key, see Authentication in the MCP server reference. Requests without a valid key receive 401 Unauthorized.

Resources

The group is metrics.apoxy.dev/v1alpha1. All resources are cluster-scoped, and no project identifier appears in any path or parameter because the API server serves exactly one project.

ResourceKindVerbs
metricsMetricget, list, watch, create, update, delete
metricsourcesMetricSourceget, list
metrics/<name>/seriesMetricSeriesSetget

A Metric is a named recipe: a query fragment plus display preferences. The built-in recipes are managed for you; you can also save your own. A MetricSource describes what a recipe can read and group by.

Built-in metrics

Every project starts with these recipes. They carry the metrics.apoxy.dev/managed: "true" label.

MetricTypeMeasuresUnit
http.requestscountertotal, status_2xx, status_4xx, status_5xx-
http.errorscountererrors_4xx, errors_5xx-
http.latencyhistogramp50, p95, p99ms
http.bytescounterbytes_in, bytes_outBy
log.severitycountertotal, errors, warnings-
upstream.connectionsgaugeactive, peak-
tls.handshakescounterhandshakes-
envoy.bandwidthcounterdownstream_in, downstream_out, upstream_out, upstream_inBy
network.bytescounterbytesBy
network.packetscounterpackets-
network.dropscounterdrops-
network.keepalivescounterkeepalives-
network.tunnelsgaugeactive, peak-
tunnel.rttgaugertt, rtt_peaks

http.requests and http.errors are plain counts and carry no unit, so they contribute nothing to the units map in a response. upstream.connections reports live upstream connections, and tls.handshakes counts downstream TLS connections established in the window. http.latency percentiles come from fixed latency buckets, and the largest bound is 60 s, so a percentile that falls past that bound reads as 60000 ms.

The collector stores metrics whose names start with envoy. or apoxy. when it can identify their project. Raw samples remain in ClickHouse for 72 hours, including new metric names under either prefix.

upstream.connections requires a recognized route or Backend. tls.handshakes requires a Gateway and listener. Proxy health metrics require both a Proxy name and a replica name. Samples without this identity remain in raw storage and do not contribute to the API results.

envoy.bandwidth counts the bytes Envoy moved on the wire, which is why it carries traffic that http.bytes cannot see: http.bytes counts request and response bodies at the end of a request, so a WebSocket or another long-lived stream stays at zero until it closes.

The catalog also holds the other envoy.* recipes, which measure the proxy process rather than the traffic through it. See Proxy health in the metrics API.

The network.* and tunnel.* recipes measure the tunnels of a VPC network rather than HTTP traffic. network.bytes and network.packets count every packet of payload a tunnel carries, so east-west traffic and non-HTTP protocols show up in them and not in http.*. network.keepalives counts the keep-alive frames that keep the tunnel path active; they carry no payload, so they are counted apart and a tunnel that carries no traffic reports keep-alives with zero bytes and zero packets. network.tunnels reports how many tunnels a network had connected, counted across every relay that serves it; its peak adds the largest count each relay saw, so it is an upper bound. tunnel.rtt reports the round trip time between an agent and the relay it connects to. network.tunnels is reported on the network alone, so it is read with scopeKind=VPCNetwork; each of the others is readable with scopeKind=VPCNetwork or scopeKind=Tunnel.

List the catalog with the resolved source and unit for each recipe:

$terminalSH
kubectl get metrics

You should see NAME, TYPE, UNIT, and SOURCE columns. A GET on one name returns the full recipe, including the measures and groupable keys the server derived from it.

Two catalogs

CLRK has its own fleet metrics catalog under metrics.clrk.apoxy.dev, with different recipe names and its own scope kinds. It is a separate product surface. See Query fleet metrics for that one.

Proxy health in the metrics API

The envoy.* recipes measure the proxy process rather than the traffic through it, so a series query and a snapshot answer for its health the way they do for traffic. They are in the catalog of every project on the managed platform. On a gateway you run yourself they come with the apoxy-gateway-ee chart, which installs the metrics pipeline and stores measurements in ClickHouse inside your cluster. Apoxy gives you a pull key. It reads the chart and its images from oci://us-west1-docker.pkg.dev/apoxy-internal/ee.

Two values decide the install: projectID names the project the measurements belong to, and the ClickHouse choice is either the single node the chart brings up or an external DSN pointing at one you already run. The images run on amd64 and arm64 nodes. The single node keeps its own query and metric logs for clickhouse.systemLogs.retentionDays days, 7 by default, and its trace and text logs are off.

RecipeMeasuresReports
envoy.exitsexits, totalEnvoy process exits, by reason and exit status: exits counts the exits inside the window, total is the count since the replica started.
envoy.connectionsactive, peak, limitDownstream connections open, their peak, and the ceiling they are measured against.
envoy.pendingactive, peak, overflowRequests waiting for an upstream connection, their peak, and the ones a full queue rejected.
envoy.latencyp50_ms, p99_ms, upstream_p50_ms, upstream_p99_msRequest latency through the proxy and the share the upstream owns, p50 and p99. Only the gateway listeners and the route clusters count, so the admin listener and the proxy's own internal clusters do not move the numbers.
envoy.bandwidthdownstream_in, downstream_out, upstream_out, upstream_inBytes on the wire in the window, in By. The downstream pair is the client side of the gateway listeners, so a Gateway scope narrows it to that Gateway. The upstream pair is the route clusters, which only a Proxy or a replica names, so a Gateway scope reports zero for it. The admin listener and the proxy's own internal clusters do not move the numbers.
envoy.watchdogmisses, mega_missesThreads that missed the watchdog deadline. A mega miss is the long one and says the process was blocked.
envoy.overloadheap_pressure, cx_pressure, stop_acceptingHeap and connection pressure, and whether the proxy stopped accepting requests.
envoy.dns_cachehosts, limit, overflowHosts in a dynamic proxy DNS cache, its limit, and the hosts it could not hold.
envoy.fdsopen, limitOpen file descriptors and the soft limit.

All nine read envoy_1m and group by replica by default, so a single replica in trouble stays visible in a fleet reading. envoy.dns_cache groups by cache instead, because a project can run several.

envoy.bandwidth counts connection bytes rather than request bodies, so a long-lived stream shows up while it runs instead of only when it closes. Read it with scopeKind=Gateway for the client side and with scopeKind=Proxy for the upstream side.

ProxyMetrics reports the same recipes per replica in perReplica, next to the replicas counts it takes from the Proxy status. Each entry names one replica and carries that replica's measures alone. A replica that reported nothing in the window is absent from the list rather than present with zeros, and the list is empty until measurements arrive.

In the console

The console reads these recipes on the Proxy tab of a gateway, next to Telemetry. The tab shows how many replicas are connected, the Envoy exits in the selected window, the p99 request latency through the proxy, the active downstream connections against their summed ceiling, the open file descriptors against their summed limit, and the highest heap use of a replica. Under that, one row per replica gives its status, which reads Gone for a replica the Proxy no longer lists, how long it has been connected, its exits in the selected window, its queue overflows, its file descriptors, its DNS cache and its watchdog misses. Telemetry and Proxy share one window, so the range you pick on one tab opens on the other.

The Telemetry tab of the same gateway draws a Bandwidth chart from envoy.bandwidth: the client side for the gateway, the upstream side for its proxy.

Four charts draw the window: active downstream connections against the ceiling they are measured against, request latency with the downstream and the upstream distributions drawn apart, heap pressure as a percent, and pending upstream requests. Every Envoy exit is marked on all four, so a jump in one number lines up with the restart behind it. A filter narrows the charts to the replicas you pick, matching on name, locality, address or status, with presets for the replicas that need attention, the ones that restarted Envoy in the window, and each locality. An empty selection means every replica, and the table always lists them all.

When the numbers are missing or old, the tab says why:

  • Proxy health is not available - this apiserver does not serve the metrics API, so there is no per-replica health to read. Read the replicas from the Proxy object instead.
  • No Proxy attached - the gateway names no Proxy, so there is no data plane to report on.
  • Shared data plane - the gateway runs on the shared data plane. Per-replica health is reported for dedicated Proxies and for gateways you run yourself.
  • No replicas reported - no replica is connected, or no replica reported a measurement in the selected window.
  • Metrics backend not configured - the API is served but no backend is behind it, and the card names what the install is missing. Replica status keeps coming from the Proxy object.
  • Could not read - one of the reads the page needs came back with an error, or the network did. Nothing is drawn under the message, because a failed read says nothing about the replicas. Select Retry to read again.
  • Stale data - the newest measurement is old, so the numbers on the page have stopped moving. Select Retry to read again.

Metric sources

A source is what a recipe reads. kubectl get metricsources lists them with their granularity and retention.

SourceGranularityRetentionNotes
http_1m1m7dHTTP traffic aggregated into one-minute buckets.
http_1h1h400dThe same measurements in one-hour buckets.
otel_logsrow90dIndividual log records.
envoy_1m1m30dProxy connection and TLS stats in one-minute buckets.
relay_1m1m30dTunnel traffic in one-minute buckets, per network, per relay, and per tunnel.

The http.* recipes read http_1m. Series and snapshot reads over longer windows are served at coarser granularity automatically, so history stays available past the finer source's retention without any change to your request.

A snapshot evaluates recipes from several sources, and the sources keep different amounts of history. A window wider than a source's retention drops that source's recipes from the snapshot and reports the rest: vpcnetworks/{name}/metrics?window=1440h answers with the http.* recipes, which the 400 day rollup serves, and without the network.* and tunnel.* recipes, which are kept for 30 days. A window no evaluated source can reach is a 400 instead - tunnels/{name}/metrics reads relay_1m alone, so a 60 day window there names the source and its maximum. Naming such a recipe explicitly with metric= is a 400 as well, rather than a quiet drop.

log.severity reads raw log records, which is why it is series-only: snapshots evaluate the aggregated recipes and skip raw-log ones. A grouped read over raw records is limited to a 24 hour window.

MetricSource.status.fields lists each field with a role:

  • time - the bucket column.
  • key - a groupable dimension. These are exactly the values groupBy accepts.
  • measure - a value column a recipe aggregates.

Each field also reports discovered, which is true for a field found by sampling log attributes (so it can disappear) and false for a fixed column.

Series

GET/apis/metrics.apoxy.dev/v1alpha1/metrics/{name}/series

Runs one recipe over a window and returns a MetricSeriesSet.

Parameters

ParameterDefaultDescription
scopeKindProjectOwner kind to scope to: Project, Gateway, HTTPRoute, Proxy, Service, VPCNetwork, or Tunnel. Takes a kind, not a resource name (Gateway, not gateways).
scopeName-Name of the owner object. Omit it for Project, which reads the whole project.
scopeListener-Narrows a Gateway scope to one listener.
since-1hStart of the window. An RFC3339 instant or a relative duration.
untilend of the last complete bucketEnd of the window. Same formats as since.
window-Shorthand for since=-<duration>. Rejected together with since.
stepsource granularityBucket width. Rounded up to the source granularity and echoed in the response.
groupBy-One key field to split the result by.
orderBythe recipe's first default columnMeasure to rank groups by.
top50Maximum number of series to return. 50 is also the ceiling.

The window is half-open, [since, until). Relative durations accept h, m, s, and d: -6h, -7d, -1.5d, and +30m all parse. A bare 6h with no sign does not, and neither does -1w.

Buckets count whole steps forward from since rather than from the clock, so the first point carries since, and a step equal to the window returns exactly one point per series.

Group-by keys

groupBy takes exactly one key:

KeyDescription
gatewayGateway that served the request.
listenerGateway listener that served the request.
route_kindRoute object kind.
route_nameRoute object name.
route_ruleRule index inside the route.
backend_kindBackend object kind.
backend_nameBackend object name.
backend_revisionCompute revision that served the request. Empty for traffic that is not served by a revision.
status_classResponse status class (2, 4, 5).
methodHTTP request method.

The network.* and tunnel.* recipes read a different source and take their own keys:

KeyDescription
networkVPC network the traffic crossed.
tunnelTunnel that carried it. One tunnel is one agent connection.
relayRelay that carried it. A network is served by more than one relay. Empty on a metric that is not counted per relay.
directionrx or tx. Empty on a metric that counts no traffic.

A key that the recipe's source does not carry returns 400 with the valid keys listed. The authoritative list for any recipe is its status.keys.

Response

$terminalJSON
{ "kind": "MetricSeriesSet", "apiVersion": "metrics.apoxy.dev/v1alpha1", "metric": "http.requests", "scopeKind": "Gateway", "scopeName": "prod", "since": "2026-08-20T15:00:00Z", "until": "2026-08-20T21:00:00Z", "step": "5m0s", "dataUpTo": "2026-08-20T21:00:00Z", "truncated": false, "totalCount": 7, "units": {}, "series": [ { "labels": { "route_name": "api" }, "points": [ { "timestamp": "2026-08-20T15:00:00Z", "values": { "total": 5210, "status_4xx": 140, "status_5xx": 12 } }, { "timestamp": "2026-08-20T15:05:00Z", "values": { "total": 5188, "status_4xx": 131, "status_5xx": 9 } } ] } ] }
FieldDescription
metric, scopeKind, scopeNameEcho the resolved query. scopeName is empty for a project-wide read.
since, untilResolved window bounds.
stepApplied bucket width, serialized as a duration string ("5m0s").
dataUpToEnd of the last complete bucket.
truncatedtrue when more groups matched than top returned.
totalCountHow many groups had data in the window.
unitsMeasure name to display unit, echoed from the catalog. Empty when no measure carries a unit.
series[].labelsGroup key and value. Empty for an ungrouped read, which returns exactly one series.
series[].points[]Buckets in timestamp order, each with the recipe's measures.

Points are sparse: a bucket with no traffic is omitted rather than returned as zero. Compare a missing bucket against dataUpTo to tell missing data from a partial trailing bucket.

Fleet views

A one-bucket series is how you get one row per object. Set step equal to the window and group by the owner key:

$terminalTXT
metrics/http.requests/series?scopeKind=Project&groupBy=gateway&since=-1h&step=1h

That returns one point per Gateway, ranked by orderBy and bounded by top. The same shape with scopeKind=Gateway&groupBy=route_name gives one row per route.

Snapshots

A snapshot is every applicable recipe evaluated over one window for one object, in a single call. It mounts as a metrics subresource on the owner.

OwnerPathResponse kindinclude tokens
Gateway/apis/gateway.apoxy.dev/v1/gateways/{name}/metricsGatewayMetricsroutes
HTTPRoute/apis/gateway.apoxy.dev/v1/httproutes/{name}/metricsHTTPRouteMetricsrules, backends
Proxy/apis/core.apoxy.dev/v1alpha2/proxies/{name}/metricsProxyMetricsnone
Service/apis/compute.apoxy.dev/v1alpha1/services/{name}/metricsServiceMetricsrevisions
VPCNetwork/apis/vpc.apoxy.dev/v1alpha1/vpcnetworks/{name}/metricsVPCNetworkMetricsservices, tunnels
Tunnel/apis/vpc.apoxy.dev/v1alpha1/tunnels/{name}/metricsTunnelMetricsnone

Every snapshot carries timestamp, window, since, until, dataUpTo, a metrics map keyed by recipe name, and a units map. The window parameters are the same as for series.

$terminalJSON
{ "kind": "GatewayMetrics", "apiVersion": "metrics.apoxy.dev/v1alpha1", "metadata": { "name": "prod" }, "timestamp": "2026-08-20T21:00:00Z", "window": "24h0m0s", "since": "2026-08-19T21:00:00Z", "until": "2026-08-20T21:00:00Z", "dataUpTo": "2026-08-20T21:00:00Z", "metrics": { "http.requests": { "total": 1842113, "status_2xx": 1790220, "status_4xx": 48120, "status_5xx": 3773 }, "http.latency": { "p50": 38, "p95": 190, "p99": 412 } }, "units": { "p50": "ms", "p95": "ms", "p99": "ms" }, "listeners": [ { "name": "https", "metrics": { "http.requests": { "total": 1839900, "status_5xx": 3773 } }, "truncated": true, "totalCount": 37 } ] }

Nesting

The default response is the owner totals plus the first nesting level, with no leaf rows. Add them with include:

  • include=routes on a Gateway adds routes[] under each listener.
  • include=rules, include=backends on an HTTPRoute add rules[] and backends[].
  • include=revisions on a Service.
  • include=services, include=tunnels on a VPC network.
  • include=all means every token for that kind.

include may repeat or take a comma-separated list. An unknown token returns 400.

truncated and totalCount sit on whichever container was cut. A Gateway cuts routes per listener, so they appear on each listener. HTTPRouteMetrics, ServiceMetrics, and VPCNetworkMetrics cut their own leaf lists, so they appear on the object itself and count every list the read asked for.

A VPC network reports its services and its tunnels from different measurements: services[] covers gateway traffic to the network's VPC services, and tunnels[] covers everything the tunnels carried. The two do not add up to each other. network.tunnels counts the network rather than one connection, so it is reported in the network's own metrics map and never as an entry of tunnels[]; every entry of tunnels[] names a connection, and totalCount counts those entries alone.

For complete per-route coverage instead of the top slice, use a one-bucket series grouped by route_name.

orderBy names the measure a nested list is ranked by. A snapshot can nest by levels that come from different sources, and a level is ranked by the named measure only when a recipe of that level reports it; a level whose recipes do not report it keeps its own default order, which is the request count where there is one. include=all&orderBy=bytes on a VPC network therefore ranks tunnels[] by bytes and leaves services[] in its own order. A measure no evaluated recipe reports anywhere is a 400.

Restricting recipes

metric=<name> limits which recipes are evaluated. The parameter repeats and does not take a comma-separated list:

$terminalTXT
?metric=http.requests&metric=http.latency

Snapshots evaluate the managed recipes by default. Naming a recipe of your own with metric= evaluates that one too.

ProxyMetrics additionally carries replicas, which reports connected replica counts from the Proxy's own status rather than from traffic. Proxy metrics cover traffic on the Gateways attached to that Proxy.

ServiceMetrics revision rows appear as traffic arrives for each revision. Traffic recorded before revision attribution existed is grouped under an empty revision name.

TunnelMetrics covers one agent connection. A tunnel's history belongs to that connection: when an agent reconnects it gets a new Tunnel with a new name, and its metrics start over with it. Read vpcnetworks/{name}/metrics?include=tunnels for a view that spans reconnects, and tunnels/{name}/metrics for one connection.

The Tunnel object is removed when the agent disconnects, but its measurements are kept for the retention of relay_1m. tunnels/{name}/metrics keeps answering for a disconnected connection for that long, so the history of a connection outlives the connection itself. A name with no measurements left, and one that never existed, are both a 404.

Custom metrics

A Metric you create is a saved query. It appears in kubectl get metrics, works with series, and can be pulled into a snapshot with metric=.

$metric.yamlYAML
apiVersion: metrics.apoxy.dev/v1alpha1 kind: Metric metadata: name: v2.errors spec: source: otel_logs type: counter description: Server errors on the /v2 path prefix. prql: | filter (url.path | text.starts_with "/v2/") filter http.response.status_code >= 500 aggregate { n = count this }

Apply it with apoxy apply -f metric.yaml.

The fragment is aggregate-only: zero or more filter and derive steps followed by one aggregate. It states no from, no group, and no time_bucket, because the source comes from spec.source, the grouping from groupBy, the bucket from step, and the scope from the scope parameters. A fragment carrying any of those steps is rejected.

The server compiles the fragment when you write it and fills in status: the resolved source, the output measures with their types and units, the groupable keys, and a Compiled condition. You never write status yourself. A fragment that does not compile is never stored, so a broken recipe cannot surface later as a snapshot silently missing a measure.

Leave spec.source unset to have the server resolve it from the fields the fragment uses.

Reserved names

The prefixes http., log., upstream., tls., network., and tunnel. are reserved for built-in recipes, as is the exact name series. A write to a reserved name is rejected with 403, so a recipe of yours can never shadow a built-in. The envoy_1m and relay_1m sources are reserved the same way: their schema moves with the built-in recipes, so a recipe of yours cannot read them.

Limits

LimitValue
Series per response (top)50
Buckets per series1500
Total points (buckets x series x measures)20000
Minimum stepthe source granularity, rounded up
Maximum lookback on raw records31d
Maximum window for a grouped raw-record read24h

Exceeding the point budget returns 400 naming all three factors, so you can lower top, widen step, or ask for fewer measures with metric=. Exceeding top is not an error: the response returns the top slice and sets truncated with totalCount.

Errors

Failures are standard Kubernetes Status objects.

StatusMeaning
400Guardrail violation, or an unknown groupBy key, include token, or scopeKind. The message lists the valid values.
403Write to a reserved recipe name.
404The owner object does not exist.
422The recipe does not compile against the current schema.
429Concurrent query limit reached. Retry after the interval in the Retry-After header.
503The metrics backend is unavailable.

A project with no data yet returns 200 with an empty window rather than an error, so an empty result is not a failure.

Caching and polling

Responses carry:

$terminalTXT
Cache-Control: max-age=60

Poll on the interval the response states rather than faster. Window bounds are aligned to step before a response is cached, so two reads a few seconds apart over the same window return the same result.

Treat dataUpTo as the edge of settled data. A bucket after it is still filling, and a client that charts it will show an apparent dip that recovers on the next poll.

Gateway pod metrics (self-hosted)

A gateway you run in your own cluster exposes Prometheus metrics on the gateway pod. They describe the Envoy process rather than the traffic through it, so they answer what the API above cannot: whether Envoy restarted, how close it is to a limit, and where a request spent its time. The series carry the apoxy_backplane_ prefix, after the process that supervises Envoy in the pod.

Endpoint

Container port8888 (named metrics)
ProtocolHTTP, cluster-internal
/envoy/metricsEnvoy's own stats and the apoxy_backplane_* series in one scrape.
/metricsThe apoxy_backplane_* series alone.

/envoy/metrics answers 200 while Envoy is restarting. Envoy's own stats are missing from that response and apoxy_backplane_envoy_up reads 0, so alert on that gauge rather than on a failed scrape.

Chart values:

  • backplane.envoyPodMonitor - creates the PodMonitor for /envoy/metrics.
  • backplane.prometheusRule - creates the PrometheusRule holding the alerts below.
  • backplane.grafanaDashboard - creates the dashboard ConfigMap.
  • backplane.autoscaling.podsMetrics - adds type: Pods metrics to the gateway HPA, so you can scale on a series such as envoy_http_downstream_cx_active. Your cluster needs prometheus-adapter to publish the named series as a pods metric.

Envoy restarts

MetricTypeDescription
apoxy_backplane_envoy_upGauge1 while the Envoy process runs, 0 between an exit and the next start.
apoxy_backplane_envoy_infoGauge (1)Build info, carried on the release and envoy_version labels.
apoxy_backplane_envoy_start_time_secondsGaugeUnix seconds of the current start.
apoxy_backplane_envoy_restarts_totalCounterStarts that followed an exit. Resets to 0 when the pod restarts. Alert on this one rather than on exits_total, because it is published from 0.
apoxy_backplane_envoy_exits_totalCounterExits by reason and code. A pair appears only once an exit carries it, so its first value is already 1.
apoxy_backplane_envoy_last_exit_timestamp_secondsGaugeUnix seconds of the last exit. Absent until the first one.
apoxy_backplane_envoy_last_exit_requests_in_flightGaugeRequests Envoy was serving at the last exit. None of them reach an access log.
apoxy_backplane_envoy_last_exit_connectionsGaugeDownstream connections open at the last exit.
apoxy_backplane_envoy_last_exit_connections_abortedGaugeEstablished connections reset at the last exit.
apoxy_backplane_envoy_last_exit_connections_refusedGaugeConnection attempts answered with a reset while Envoy was down.
apoxy_backplane_envoy_admin_scrape_errors_totalCounterScrapes that could not read Envoy's own stats. A rising count while apoxy_backplane_envoy_up reads 1 means Envoy is alive but not answering.

reason takes four values, and code qualifies it:

  • exit - the process returned an exit status, which code carries. exit with code="0" is a clean shutdown, and code="1" is usually a configuration Envoy refused at start.
  • signal - a signal ended the process, and code carries the signal name.
  • oom_kill - the container reached its memory limit and the kernel killed the process.
  • start_failed - the process never started, and code is empty.

The same exit appears in two other places. Proxy.status.replicas[] carries envoyRestarts and lastEnvoyExit for each connected replica:

$terminalSH
apoxy proxy get my-proxy -o yaml

And the gateway logs one structured line per exit, Envoy exited, with the reason, the exit status or signal name, and how long the process ran.

Limits and the series that show pressure

Every limit below has a ceiling you can read, a current value, and usually a pressure signal that moves before the limit bites.

LimitCeilingCurrent usePressure
Active downstream connectionsapoxy_backplane_envoy_limit_max_active_downstream_connectionsenvoy_server_total_connectionsenvoy_overload_envoy_resource_monitors_global_downstream_max_connections_pressure
Envoy heapapoxy_backplane_envoy_limit_max_heap_bytesenvoy_server_memory_allocatedenvoy_overload_envoy_resource_monitors_fixed_heap_pressure
Container memoryapoxy_backplane_envoy_limit_memory_bytesapoxy_backplane_envoy_rss_bytes-
File descriptorsapoxy_backplane_envoy_max_fdsapoxy_backplane_envoy_open_fds-
Pending requests per clusterunlimited unless the Backend sets oneenvoy_cluster_upstream_rq_pending_activeenvoy_cluster_circuit_breakers_default_remaining_pending, envoy_cluster_upstream_rq_pending_overflow
Circuit breakers per clusterset per Backendenvoy_cluster_circuit_breakers_default_remaining_cx, _remaining_rq, _remaining_pending hold the headroom leftenvoy_cluster_circuit_breakers_default_cx_open, _rq_open, _rq_pending_open
Dynamic proxy DNS cacheapoxy_backplane_envoy_limit_dns_cache_max_hosts, label cacheenvoy_dns_cache_<name>_num_hostsenvoy_dns_cache_<name>_host_overflow
Worker thread progress--envoy_server_watchdog_miss, envoy_server_watchdog_mega_miss
Kernel accept queue--apoxy_backplane_net_tcp_listen_overflows_total, apoxy_backplane_net_tcp_listen_drops_total
Connections the kernel cut--apoxy_backplane_net_tcp_out_rsts_total, apoxy_backplane_net_tcp_estab_resets_total, apoxy_backplane_net_tcp_abort_on_close_total, apoxy_backplane_net_tcp_abort_on_data_total

Reading that table:

  • The two Envoy pressure gauges are percentages from 0 to 100, not ratios. Compare against 80, not 0.8.
  • The heap and container memory rows need a memory limit on the gateway container. Without one there is no ceiling to publish, apoxy_backplane_envoy_limit_memory_bytes is absent, and Envoy's heap monitor has no threshold to measure pressure against.
  • Heap and resident memory are different numbers, and neither covers the other. envoy_server_memory_allocated counts Envoy's own allocator and misses every Edge Function library loaded into the same process, so resident memory can climb while heap pressure stays flat. apoxy_backplane_envoy_rss_bytes is the Envoy process alone, while apoxy_backplane_envoy_limit_memory_bytes is the container limit for the whole pod, so the headroom that ratio shows is optimistic. The container limit is the one the kubelet kills against.
  • Pending requests are unbounded unless a Backend sets a limit, so a slow upstream grows the queue instead of overflowing it. Watch the level with envoy_cluster_upstream_rq_pending_active, which is what EnvoyPendingQueueHigh alerts on; upstream_rq_pending_overflow and rq_pending_open stay at zero until a Backend sets a finite limit.
  • <name> in the DNS cache series is the Backend the dynamic proxy serves, so a Backend named dynamic-proxy gives envoy_dns_cache_dynamic_proxy_num_hosts.
  • The apoxy_backplane_net_tcp_* counters cover connections the kernel dropped or reset before Envoy saw them, which is what a client experiences during a restart or a worker stall. No Envoy stat records those.
  • The watchdog series are the totals over all threads. A per-thread breakdown is published beside them, either on an envoy_thread_name label or in the metric name.

Time inside the proxy

Three histograms split where a request spends its time:

  • envoy_http_downstream_rq_time - the whole request, from the first downstream byte to the last response byte.
  • envoy_cluster_upstream_rq_time - the part your upstream owns.
  • envoy_cluster_upstream_cx_connect_ms - connection setup to the upstream, which upstream_rq_time does not include.

The difference between the first two is the proxy's own share: filters, Edge Functions, and time waiting in the pending queue. Three access-log fields split that share per request, so a slow tail can be attributed to a single call rather than to a percentile:

  • filter_duration - from the first downstream byte to the first byte sent upstream.
  • request_tx_duration - from the first downstream byte to the last byte sent upstream.
  • response_duration - from the first downstream byte to the first upstream response byte.

In the hosted logs the same values are the attributes http.request.filter_duration_ms, http.request.tx_duration_ms and http.response.duration_ms, next to http.request.duration_ms.

The chart's PrometheusRule ships these. Tune the thresholds to your traffic.

AlertFires when
EnvoyExitedEnvoy exited at least once in the last 10 minutes.
EnvoyDownapoxy_backplane_envoy_up has read 0 for 2 minutes.
EnvoyCrashLoopEnvoy restarted three times in 30 minutes.
EnvoyPendingQueueHighMore than 512 requests wait for a connection to one cluster. This is the one that fires on a default configuration.
EnvoyPendingOverflowA cluster rejects requests with a 503 because its pending queue is full. Needs a Backend that sets a pending limit.
EnvoyCircuitBreakerOpenA default-priority circuit breaker has been open for 2 minutes.
EnvoyWatchdogMegaMissA worker thread stalled past the watchdog mega-miss timeout.
EnvoyOverloadStopAcceptingRequestsThe overload manager is refusing new requests.
EnvoyDownstreamCxNearLimitDownstream connections are above 80% of the configured ceiling.
EnvoyFdNearLimitOpen file descriptors are above 80% of the soft limit.
EnvoyRssNearLimitResident memory is above 85% of the container memory limit.
DfpDnsCacheNearLimitThe dynamic proxy DNS cache is near max_hosts, or it is evicting hosts.
EnvoyConfigRejectedEnvoy rejected a cluster, listener, or route update and keeps serving the previous one.
EnvoyReloadStormListeners reload repeatedly while older ones are still draining.
EnvoyListenOverflowThe kernel drops new connections because the accept queue is full.

EnvoyConfigRejected is the one with no other symptom. Envoy keeps the last good configuration when it rejects an update, so the pod stays healthy and serves stale routes until someone notices.

The same health signals are readable through the metrics API as the envoy.* recipes. See Proxy health in the metrics API.

Scraping the pipeline itself

The apoxy-gateway-ee chart also watches the pipeline that stores those measurements. With monitoring.enabled (the default) it creates three ServiceMonitor objects, <release>-apoxy-ee-apiserver, <release>-apoxy-ee-collector and <release>-apoxy-ee-clickhouse, and a PrometheusRule named <release>-apoxy-ee-pipeline holding the apoxy-gateway-ee.pipeline alert group. Its alerts fire when a rollup falls behind, when the collector refuses or drops datapoints, and when ClickHouse is down or low on disk. All four render only on a cluster that serves monitoring.coreos.com/v1, so an install without the Prometheus operator skips them.

Where to next