diff --git a/src/content/docs/privacy-gateway/reference/metrics.mdx b/src/content/docs/privacy-gateway/reference/metrics.mdx
index 8c4e4199cdf..699341a130e 100644
--- a/src/content/docs/privacy-gateway/reference/metrics.mdx
+++ b/src/content/docs/privacy-gateway/reference/metrics.mdx
@@ -3,117 +3,651 @@ title: Privacy Gateway Metrics
pcx_content_type: reference
sidebar:
order: 1
-
---
-Privacy Gateway now supports enhanced monitoring through our GraphQL API, providing detailed insights into your gateway traffic and performance. To access these metrics, ensure you have:
-
-* A relay gateway proxy implementation where Cloudflare acts as the oblivious relay party.
-* An API token with Analytics Read permissions.
- We offer two GraphQL nodes to retrieve metrics: `ohttpMetricsAdaptive` and `ohttpMetricsAdaptiveGroups`. The first node provides comprehensive request data, while the second facilitates grouped analytics.
-
-## ohttpMetricsAdaptive
-
-The `ohttpMetricsAdaptive` node is designed for detailed insights into individual OHTTP requests with adaptive sampling. This node can help in understanding the performance and load on your server and client setup.
-
-### Key Arguments
-
-
-
-* `filter` required
- * Apply filters to narrow down your data set. `accountTag` is a required filter.
-* `limit` optional
- * Specify the maximum number of records to return.
-* `orderBy` optional
- * Choose how to sort your data, with options for various dimensions and metrics.
-
+import { Details } from "~/components";
-### Available Fields
+Privacy Gateway exposes metrics through Cloudflare's [GraphQL Analytics API](/analytics/graphql-api/). All metrics are queryable via a single endpoint:
+```
+POST https://api.cloudflare.com/client/v4/graphql
+```
+To access your metrics, you need a Cloudflare API token with **Analytics Read** permissions. Refer to [Get started with the Analytics API](/analytics/graphql-api/getting-started/) for authentication details.
-* `bytesToClient` int optional
- * The number of bytes returned to the client.
-* `bytesToGateway` int optional
- * Total bytes received from the client.
-* `colo` string optional
- * Airport code of the Cloudflare data center that served the request.
-* `datetime` Time optional
- * The date and time when the event was recorded.
-* `gatewayStatusCode` int optional
- * Status code returned by the gateway.
-* `relayStatusCode` int optional
- * Status code returned by the relay.
-
-
-This node is useful for a granular view of traffic, helping you identify patterns, performance issues, or anomalies in your data flow.
+---
-## ohttpMetricsAdaptiveGroups
+
Making a request
+
+The following example shows a complete request with authentication headers. Replace `` with your token and update the query body as needed.
+
+```bash
+curl https://api.cloudflare.com/client/v4/graphql \
+ --header "Authorization: Bearer " \
+ --header "Content-Type: application/json" \
+ --data '{
+ "query": "query DailyRequestVolume($accountTag: string!, $startDate: Date!, $endDate: Date!, $endpoint: string!) { viewer { accounts(filter: { accountTag: $accountTag }) { ohttpRelayRequestMetricsAdaptiveGroups(filter: { date_geq: $startDate, date_leq: $endDate, endpoint: $endpoint }, limit: 10000, orderBy: [date_ASC]) { sum { requests bytesToGateway bytesToClient } dimensions { date endpoint } } } } }",
+ "variables": {
+ "accountTag": "",
+ "startDate": "",
+ "endDate": "",
+ "endpoint": "https://"
+ }
+ }'
+```
-The `ohttpMetricsAdaptiveGroups` node allows for aggregated analysis of OHTTP request metrics with adaptive sampling. This node is particularly useful for identifying trends and patterns across different dimensions of your traffic and operations.
+---
-### Key Arguments
+Available nodes
+Three GraphQL nodes are available, each corresponding to a different event type. All three nodes are aggregated-only — no raw per-connection records are exposed.
+| Node | Source event | Use for |
+| ---- | ------------ | ------- |
+| `ohttpRelayRequestMetricsAdaptiveGroups` | Per OHTTP request | Traffic volume and error analysis |
+| `ohttpRelayIngressConnMetricsAdaptiveGroups` | Per client-to-relay connection | Client latency and protocol distribution |
+| `ohttpRelayEgressConnMetricsAdaptiveGroups` | Per relay-to-gateway connection | Relay-to-gateway latency |
-* `filter` required
- * Apply filters to narrow down your data set. `accountTag` is a required filter.
-* `limit` optional
- * Specify the maximum number of records to return.
-* `orderBy` optional
- * Choose how to sort your data, with options for various dimensions and metrics.
+:::note[Adaptive sampling]
+Each node is backed by an independent adaptive bit-rate (ABR) sampling pipeline. Cloudflare automatically scales the sampling rate based on traffic volume to keep query performance consistent. All returned values are extrapolated totals, not raw counts. Because each node has its own sampling rate, figures across nodes are not directly comparable.
-### Available Fields
+:::
+Arguments
+All three nodes share the same argument signature.
-* `count` int optional
- * The number of records that meet the criteria.
-* `dimensions` optional
- * Specifies the grouping dimensions for your data.
-* `sum` optional
- * Aggregated totals for various metrics, per dimension.
+| Argument | Required | Description |
+| -------- | -------- | ----------- |
+| `filter` | Yes | Filter your data. `accountTag` is always required. |
+| `limit` | Yes | Maximum number of records to return. |
+| `orderBy` | No | Sort order for results. |
+Shared dimensions
-**Dimensions**
+The following dimensions are available on all three nodes.
-You can group your metrics by various dimensions to get a more segmented view of your data:
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `accountTag` | string | Your Cloudflare account identifier |
+| `date` | Date | Calendar date (day granularity) |
+| `datetimeMinute` | Time | Timestamp truncated to the minute |
+| `datetimeFiveMinutes` | Time | Timestamp truncated to 5-minute intervals |
+| `datetimeFifteenMinutes` | Time | Timestamp truncated to 15-minute intervals |
+| `datetimeHour` | Time | Timestamp truncated to the hour |
+| `colo` | string | Cloudflare data center that handled the request |
+| `endpoint` | string | The URL of the Privacy Gateway endpoint provided to you by Cloudflare |
+---
-* `colo` string optional
- * The airport code of the Cloudflare data center.
-* `date` Date optional
- * The date of OHTTP request metrics.
-* `datetimeFifteenMinutes` Time optional
- * Timestamp truncated to fifteen minutes.
-* `datetimeFiveMinutes` Time optional
- * Timestamp truncated to five minutes.
-* `datetimeHour` Time optional
- * Timestamp truncated to the hour.
-* `datetimeMinute` Time optional
- * Timestamp truncated to the minute.
-* `endpoint` string optional
- * The appId that generated traffic.
-* `gatewayStatusCode` int optional
- * Status code returned by the gateway.
-* `relayStatusCode` int optional
- * Status code returned by the relay.
+ohttpRelayRequestMetricsAdaptiveGroups
+
+**Sum fields**
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `requests` | uint64 | Total request count |
+| `bytesToGateway` | uint64 | Total bytes sent from client to gateway |
+| `bytesToClient` | uint64 | Total bytes sent from gateway back to client |
+
+**Additional dimensions**
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `relayStatusCode` | uint16 | HTTP status code returned by the relay to the client |
+| `gatewayStatusCode` | uint16 | HTTP status code returned by the gateway to the relay. A value of `0` means the request never reached the gateway (relay-side failure or timeout). |
+
+**Status code reference**
+
+*relayStatusCode*
+
+| Code | Description |
+| ---- | ----------- |
+| **2xx — Success** | |
+| `200` | Request relayed and response returned to client |
+| **4xx — Client errors** | |
+| `400` | Bad Request — malformed OHTTP capsule or invalid headers |
+| `401` | Unauthorized — missing or invalid credentials on the outer request |
+| `403` | Forbidden — client is not permitted to use this endpoint |
+| `405` | Method Not Allowed — only POST is accepted for OHTTP |
+| `413` | Payload Too Large — encapsulated request exceeds the size limit |
+| `429` | Too Many Requests — client has been rate limited by the relay |
+| **5xx — Server errors** | |
+| `500` | Internal Server Error — relay failed to process the request |
+| `502` | Bad Gateway — relay received an invalid response from the gateway |
+| `503` | Service Unavailable — relay is temporarily unable to handle requests |
+| `504` | Gateway Timeout — gateway did not respond within the configured timeout |
+
+*gatewayStatusCode*
+
+A value of `0` means the request never reached the gateway — the relay failed or timed out before forwarding it.
+
+| Code | Description |
+| ---- | ----------- |
+| **0 — No gateway response** | |
+| `0` | Request never reached the gateway (relay-side failure or timeout) |
+| **2xx — Success** | |
+| `200` | Gateway processed the inner OHTTP request |
+| **4xx — Client errors** | |
+| `400` | Bad Request — gateway could not parse the decapsulated inner request |
+| `401` | Unauthorized — missing or invalid credentials on the inner request |
+| `403` | Forbidden — gateway rejected the request based on policy |
+| `404` | Not Found — target resource not found at the origin |
+| `429` | Too Many Requests — gateway rate limited the request |
+| **5xx — Server errors** | |
+| `500` | Internal Server Error — gateway encountered a processing failure |
+| `502` | Bad Gateway — gateway received an invalid response from the origin |
+| `503` | Service Unavailable — gateway or origin is temporarily unavailable |
+
+**Sample queries**
+
+Total requests by day
+
+Get a high-level view of daily request volume and bytes transferred over a time range.
+
+
+
+```graphql
+query DailyRequestVolume(
+ $accountTag: string!
+ $startDate: Date!
+ $endDate: Date!
+ $endpoint: string!
+) {
+ viewer {
+ accounts(filter: { accountTag: $accountTag }) {
+ ohttpRelayRequestMetricsAdaptiveGroups(
+ filter: {
+ date_geq: $startDate
+ date_leq: $endDate
+ endpoint: $endpoint
+ }
+ limit: 10000
+ orderBy: [date_ASC]
+ ) {
+ sum {
+ requests
+ bytesToGateway
+ bytesToClient
+ }
+ dimensions {
+ date
+ endpoint
+ }
+ }
+ }
+ }
+}
+```
+
+```json
+{
+ "accountTag": "",
+ "startDate": "",
+ "endDate": "",
+ "endpoint": "https://"
+}
+```
+
+
+
+Client request errors
+
+Find client-side failures — such as malformed requests or auth failures — that never reached the gateway. These appear as relay `4xx` with `gatewayStatusCode: 0`.
+
+
+
+```graphql
+query ClientRequestErrors(
+ $accountTag: string!
+ $start: Time!
+ $end: Time!
+ $endpoint: string!
+) {
+ viewer {
+ accounts(filter: { accountTag: $accountTag }) {
+ ohttpRelayRequestMetricsAdaptiveGroups(
+ filter: {
+ datetime_geq: $start
+ datetime_leq: $end
+ endpoint: $endpoint
+ relayStatusCode_geq: 400
+ relayStatusCode_leq: 499
+ gatewayStatusCode: 0
+ }
+ limit: 10000
+ orderBy: [datetimeMinute_ASC]
+ ) {
+ sum {
+ requests
+ }
+ dimensions {
+ datetimeMinute
+ datetimeFiveMinutes
+ datetimeHour
+ relayStatusCode
+ colo
+ }
+ }
+ }
+ }
+}
+```
+
+```json
+{
+ "accountTag": "",
+ "start": "",
+ "end": "",
+ "endpoint": "https://"
+}
+```
+
+
+
+Gateway response errors
+
+Identify errors originating from the upstream gateway to determine if it is unhealthy.
+
+
+
+```graphql
+query GatewayResponseErrors(
+ $accountTag: string!
+ $start: Time!
+ $end: Time!
+ $endpoint: string!
+) {
+ viewer {
+ accounts(filter: { accountTag: $accountTag }) {
+ ohttpRelayRequestMetricsAdaptiveGroups(
+ filter: {
+ datetime_geq: $start
+ datetime_leq: $end
+ endpoint: $endpoint
+ gatewayStatusCode_geq: 500
+ gatewayStatusCode_leq: 599
+ }
+ limit: 10000
+ orderBy: [datetimeMinute_ASC]
+ ) {
+ sum {
+ requests
+ }
+ dimensions {
+ datetimeMinute
+ datetimeFiveMinutes
+ datetimeHour
+ gatewayStatusCode
+ colo
+ }
+ }
+ }
+ }
+}
+```
+
+```json
+{
+ "accountTag": "",
+ "start": "",
+ "end": "",
+ "endpoint": "https://"
+}
+```
+
+
+
+Gateway timeouts
+
+Detect incidents where the gateway did not respond within the configured timeout. These appear as `relayStatusCode: 504` with `gatewayStatusCode: 0`.
+
+
+
+```graphql
+query GatewayTimeouts(
+ $accountTag: string!
+ $start: Time!
+ $end: Time!
+ $endpoint: string!
+) {
+ viewer {
+ accounts(filter: { accountTag: $accountTag }) {
+ ohttpRelayRequestMetricsAdaptiveGroups(
+ filter: {
+ datetime_geq: $start
+ datetime_leq: $end
+ endpoint: $endpoint
+ relayStatusCode: 504
+ gatewayStatusCode: 0
+ }
+ limit: 10000
+ orderBy: [datetimeMinute_ASC]
+ ) {
+ sum {
+ requests
+ }
+ dimensions {
+ datetimeMinute
+ datetimeFiveMinutes
+ datetimeHour
+ colo
+ }
+ }
+ }
+ }
+}
+```
+
+```json
+{
+ "accountTag": "",
+ "start": "",
+ "end": "",
+ "endpoint": "https://"
+}
+```
+
+
+
+Relay internal errors
+
+Isolate relay-internal failures that are not caused by the gateway. These appear as relay `5xx` with `gatewayStatusCode: 0`.
+
+
+
+```graphql
+query RelayInternalErrors(
+ $accountTag: string!
+ $start: Time!
+ $end: Time!
+ $endpoint: string!
+) {
+ viewer {
+ accounts(filter: { accountTag: $accountTag }) {
+ ohttpRelayRequestMetricsAdaptiveGroups(
+ filter: {
+ datetime_geq: $start
+ datetime_leq: $end
+ endpoint: $endpoint
+ relayStatusCode_geq: 500
+ relayStatusCode_leq: 599
+ gatewayStatusCode: 0
+ }
+ limit: 10000
+ orderBy: [datetimeMinute_ASC]
+ ) {
+ sum {
+ requests
+ }
+ dimensions {
+ datetimeMinute
+ datetimeFiveMinutes
+ datetimeHour
+ relayStatusCode
+ colo
+ }
+ }
+ }
+ }
+}
+```
+
+```json
+{
+ "accountTag": "",
+ "start": "",
+ "end": "",
+ "endpoint": "https://"
+}
+```
+
+
+---
-**Sum Fields**
+ohttpRelayIngressConnMetricsAdaptiveGroups
+
+**Quantile fields**
+
+| Field | Type | Unit | Description |
+| ----- | ---- | ---- | ----------- |
+| `clientRelayRttMsP50` | float64 | ms | Median round trip time between client and relay |
+| `clientRelayRttMsP95` | float64 | ms | 95th percentile RTT between client and relay |
+| `clientRelayRttMsP99` | float64 | ms | 99th percentile RTT between client and relay |
+
+**Additional dimensions**
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `httpVersion` | string | HTTP protocol version used for the client-to-relay connection |
+| `tlsVersion` | string | TLS version negotiated on the client-to-relay connection |
+
+**Sample queries**
+
+Client-to-relay latency by data center
+
+Compare latency across Cloudflare data centers to identify geographic performance issues.
+
+
+
+```graphql
+query ClientRelayLatencyByColo(
+ $accountTag: string!
+ $start: Time!
+ $end: Time!
+ $endpoint: string!
+) {
+ viewer {
+ accounts(filter: { accountTag: $accountTag }) {
+ ohttpRelayIngressConnMetricsAdaptiveGroups(
+ filter: {
+ datetime_geq: $start
+ datetime_leq: $end
+ endpoint: $endpoint
+ }
+ limit: 10000
+ ) {
+ quantiles {
+ clientRelayRttMsP50
+ clientRelayRttMsP95
+ clientRelayRttMsP99
+ }
+ dimensions {
+ datetimeMinute
+ datetimeFiveMinutes
+ datetimeHour
+ colo
+ }
+ }
+ }
+ }
+}
+```
+
+```json
+{
+ "accountTag": "",
+ "start": "",
+ "end": "",
+ "endpoint": "https://"
+}
+```
+
+
+
+Protocol distribution
+
+Monitor HTTP and TLS version adoption across your connections over time. Each row in the result represents a distinct combination of `httpVersion` and `tlsVersion` observed in the selected time window.
+
+
+
+```graphql
+query ProtocolDistribution(
+ $accountTag: string!
+ $start: Time!
+ $end: Time!
+ $endpoint: string!
+) {
+ viewer {
+ accounts(filter: { accountTag: $accountTag }) {
+ ohttpRelayIngressConnMetricsAdaptiveGroups(
+ filter: {
+ datetime_geq: $start
+ datetime_leq: $end
+ endpoint: $endpoint
+ }
+ limit: 10000
+ orderBy: [datetimeMinute_ASC]
+ ) {
+ quantiles {
+ clientRelayRttMsP50
+ }
+ dimensions {
+ datetimeMinute
+ datetimeFiveMinutes
+ datetimeHour
+ httpVersion
+ tlsVersion
+ }
+ }
+ }
+ }
+}
+```
+
+```json
+{
+ "accountTag": "",
+ "start": "",
+ "end": "",
+ "endpoint": "https://"
+}
+```
+
+
-Sum fields offer a cumulative view of various metrics over your selected time period:
+---
+ohttpRelayEgressConnMetricsAdaptiveGroups
+
+**Quantile fields**
+
+| Field | Type | Unit | Description |
+| ----- | ---- | ---- | ----------- |
+| `relayGatewayRttMsP50` | float64 | ms | Median round trip time between relay and gateway |
+| `relayGatewayRttMsP95` | float64 | ms | 95th percentile RTT between relay and gateway |
+| `relayGatewayRttMsP99` | float64 | ms | 99th percentile RTT between relay and gateway |
+
+**Sample queries**
+
+Relay-to-gateway latency by data center
+
+Compare relay-to-gateway latency across data centers to detect routing or peering issues.
+
+
+
+```graphql
+query RelayGatewayLatencyByColo(
+ $accountTag: string!
+ $start: Time!
+ $end: Time!
+ $endpoint: string!
+) {
+ viewer {
+ accounts(filter: { accountTag: $accountTag }) {
+ ohttpRelayEgressConnMetricsAdaptiveGroups(
+ filter: {
+ datetime_geq: $start
+ datetime_leq: $end
+ endpoint: $endpoint
+ }
+ limit: 10000
+ ) {
+ quantiles {
+ relayGatewayRttMsP50
+ relayGatewayRttMsP95
+ relayGatewayRttMsP99
+ }
+ dimensions {
+ datetimeMinute
+ datetimeFiveMinutes
+ datetimeHour
+ colo
+ }
+ }
+ }
+ }
+}
+```
+
+```json
+{
+ "accountTag": "",
+ "start": "",
+ "end": "",
+ "endpoint": "https://"
+}
+```
+
+
+
+Relay-to-gateway latency over time
+
+Track relay-to-gateway latency trends to detect gradual degradation.
+
+
+
+```graphql
+query RelayGatewayLatencyOverTime(
+ $accountTag: string!
+ $start: Time!
+ $end: Time!
+ $endpoint: string!
+) {
+ viewer {
+ accounts(filter: { accountTag: $accountTag }) {
+ ohttpRelayEgressConnMetricsAdaptiveGroups(
+ filter: {
+ datetime_geq: $start
+ datetime_leq: $end
+ endpoint: $endpoint
+ }
+ limit: 10000
+ orderBy: [datetimeMinute_ASC]
+ ) {
+ quantiles {
+ relayGatewayRttMsP50
+ relayGatewayRttMsP95
+ relayGatewayRttMsP99
+ }
+ dimensions {
+ datetimeMinute
+ datetimeFiveMinutes
+ datetimeHour
+ }
+ }
+ }
+ }
+}
+```
+
+```json
+{
+ "accountTag": "",
+ "start": "",
+ "end": "",
+ "endpoint": "https://"
+}
+```
+
+
-* `bytesToClient` int optional
- * Total bytes sent from the gateway to the client.
-* `bytesToGateway` int optional
- * Total bytes from the client to the gateway.
-* `clientRequestErrors` int optional
- * Total number of client request errors.
-* `gatewayResponseErrors` int optional
- * Total number of gateway response errors.
+---
+
-Utilize the ohttpMetricsAdaptiveGroups node to gain comprehensive, aggregated insights into your traffic patterns, helping you optimize performance and user experience.
+- [GraphQL Analytics API — getting started](/analytics/graphql-api/getting-started/) — authentication, pagination, and rate limits
+- [GraphQL Analytics API — filtering](/analytics/graphql-api/features/filtering/) — full filter syntax reference
+- [Create an API token](/fundamentals/api/get-started/create-token/) — set up a token with Analytics Read permissions
diff --git a/src/content/docs/privacy-proxy/concepts/authentication.mdx b/src/content/docs/privacy-proxy/concepts/authentication.mdx
index 1a15b12e024..cea2f6f5820 100644
--- a/src/content/docs/privacy-proxy/concepts/authentication.mdx
+++ b/src/content/docs/privacy-proxy/concepts/authentication.mdx
@@ -9,12 +9,13 @@ Privacy Proxy requires clients to authenticate before proxying traffic. This pag
## Authentication methods
-Privacy Proxy supports two authentication methods:
+Privacy Proxy supports three authentication methods:
| Method | Use case | Privacy level |
| -------- | ---------- | --------------- |
| Pre-shared key (PSK) | Proof of concept, testing | Lower |
-| Privacy Pass tokens | Production deployments | Higher |
+| Privacy Pass tokens | Client to server | High |
+| mTLS | Server to server | Higher |
---
@@ -134,6 +135,24 @@ For production deployments using Privacy Pass:
---
+## Mutual TLS (mTLS)
+
+[Mutual TLS (mTLS) authentication](https://www.cloudflare.com/learning/access-management/what-is-mutual-tls/) ensures that traffic is both secure and trusted in both directions. The client presents a certificate to the proxy, and the proxy validates it before allowing the connection.
+
+### How it works
+
+The client includes a TLS client certificate during the TLS handshake. The proxy validates the certificate against a configured certificate authority (CA) and allows the connection if the certificate is trusted.
+
+### Limitations
+
+- **Certificate management**: You must provision and manage certificates for each client or service.
+- **Not suitable for end users**: mTLS is designed for server-to-server communication, not for authenticating individual users.
+- **No unlinkability**: The proxy can identify the client by its certificate, which reduces privacy compared to Privacy Pass.
+
+Use mTLS for server-to-server integrations where both parties are trusted services.
+
+---
+
## Authentication in double-hop deployments
In [double-hop deployments](/privacy-proxy/concepts/deployment-models/#double-hop), authentication occurs at two levels:
diff --git a/src/styles/table.css b/src/styles/table.css
index 6639aa0481d..1efca08602b 100644
--- a/src/styles/table.css
+++ b/src/styles/table.css
@@ -18,9 +18,4 @@ table * {
padding-inline-end: 1rem;
}
-/*
- Alternating row shading
-*/
-.sl-markdown-content tr:nth-child(2n):not(:where(.not-content *)) {
- background-color: var(--sl-color-gray-7, #1f1f1f);
-}
+