> ## Documentation Index
> Fetch the complete documentation index at: https://tyk.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Metrics in Tyk Gateway

> Define custom metric instruments in Tyk Gateway with your own dimensions sourced from request headers, JWT claims, session data, endpoint paths, and more.

## Availability

| Component | Version | Edition |
| :- | :- | :- |
| Tyk Gateway | Available since [v5.13.0](/docs/nightly/developer-support/release-notes/gateway#5-13-0-release-notes) | Community & Enterprise |

## Custom Metrics

By default, Tyk Gateway exports [standard RED metrics](/docs/nightly/api-management/metrics/default-metrics) with a fixed set of dimensions. Custom metrics let you define additional instruments with dimensions drawn from request headers, session data, JWT claims, and other context, enabling use cases such as:

* **Multi-tenant billing:** Count requests per customer ID from a header or JWT claim
* **Tier-based SLOs:** Track latency separately for premium vs. standard API tiers
* **Business KPIs:** Measure domain-specific signals (e.g., successful transactions per payment provider)

## The api\_metrics Array

Custom metrics are defined in the `api_metrics` array inside `opentelemetry.metrics`:

```json theme={null}
{
  "opentelemetry": {
    "metrics": {
      "enabled": true,
      "api_metrics": [
        { ... }
      ]
    }
  }
}
```

The behavior of this field depends on its value:

| Value | Behavior |
| - | - |
| Field omitted or `null` | [Default RED metrics](/docs/nightly/api-management/metrics/default-metrics) are exported automatically. |
| Empty array `[]` | API-level metrics are disabled entirely; no request metrics are exported. |
| Populated array | Only the instruments you define are exported. Default RED metrics are not exported unless you explicitly define them. |

<Tip>
  **Preserving default RED metrics**

  When you populate `api_metrics`, the built-in RED instruments are no longer exported automatically. Use the block below as a starting point, it re-creates all four default instruments, then append your custom instruments after them.

  ```json expandable theme={null}
  {
    "opentelemetry": {
      "metrics": {
        "enabled": true,
        "api_metrics": [
          {
            "name": "http.server.request.duration",
            "type": "histogram",
            "description": "End-to-end request latency",
            "histogram_source": "total",
            "dimensions": [
              { "source": "metadata", "key": "method",        "label": "http.request.method" },
              { "source": "metadata", "key": "response_code", "label": "http.response.status_code" },
              { "source": "metadata", "key": "api_id",        "label": "tyk.api.id" },
              { "source": "metadata", "key": "response_flag", "label": "tyk.response_flag" }
            ]
          },
          {
            "name": "tyk.gateway.request.duration",
            "type": "histogram",
            "description": "Gateway processing time",
            "histogram_source": "gateway",
            "dimensions": [
              { "source": "metadata", "key": "method",        "label": "http.request.method" },
              { "source": "metadata", "key": "api_id",        "label": "tyk.api.id" },
              { "source": "metadata", "key": "response_flag", "label": "tyk.response_flag" }
            ]
          },
          {
            "name": "tyk.upstream.request.duration",
            "type": "histogram",
            "description": "Upstream response time",
            "histogram_source": "upstream",
            "dimensions": [
              { "source": "metadata", "key": "method",        "label": "http.request.method" },
              { "source": "metadata", "key": "api_id",        "label": "tyk.api.id" },
              { "source": "metadata", "key": "response_flag", "label": "tyk.response_flag" }
            ]
          },
          {
            "name": "tyk.api.requests.total",
            "type": "counter",
            "description": "Request count with identity dimensions",
            "dimensions": [
              { "source": "metadata", "key": "method",        "label": "http.request.method" },
              { "source": "metadata", "key": "response_code", "label": "http.response.status_code" },
              { "source": "metadata", "key": "api_id",        "label": "tyk.api.id" }
            ]
          }
        ]
      }
    }
  }
  ```
</Tip>

## Instrument Types

Each entry in `api_metrics` defines one instrument:

| Type | Description |
| - | - |
| `counter` | A monotonically increasing count. Use for request counts, error counts, etc. |
| `histogram` | A distribution of values. Use for latency measurements. |

Histograms require a `histogram_source` field that selects which latency to measure:

| `histogram_source` | Measures |
| - | - |
| `total` | End-to-end request latency (client to Gateway to upstream) |
| `gateway` | Gateway processing time only (excludes upstream response time) |
| `upstream` | Upstream service response time only |

## Dimension Sources

Dimensions let you slice your metrics by any signal available at request time. Tyk can source dimension values from six places; the sections below describe common use cases and which source and key to use for each.

| Source | What It Provides | Example Keys |
| - | - | - |
| `metadata` | Request metadata always in scope | `method`, `response_code`, `api_id`, `api_name`, `org_id`, `response_flag`, `ip_address`, `api_version`, `host`, `scheme`, `listen_path`, `endpoint`. MCP APIs also expose `mcp_method`, `mcp_primitive_type`, `mcp_primitive_name`, `mcp_error_code`, see [MCP dimensions](#mcp-dimensions). |
| `session` | Authenticated session fields | `api_key`, `oauth_id`, `alias`, `portal_app`, `portal_org` |
| `header` | Any HTTP request header | `X-Customer-ID`, `X-Tenant-ID`, `Authorization` |
| `context` | Tyk [context variables](/docs/nightly/api-management/traffic-transformation/request-context-variables) | `jwt_claims_tier`, `request_id`, `path_parts` |
| `response_header` | Any upstream response header | `X-Cache-Status`, `X-Backend-Version` |
| `config_data` | API definition metadata from the `config_data` map | Any key set on the API definition |

### Response status

Two `metadata` keys capture response status, and they answer different questions:

| Key | Source | What it captures |
| - | - | - |
| `response_code` | `metadata` | The numeric HTTP status code returned to the client (`200`, `429`, `502`, etc.). Always populated. Bounded cardinality (\~100 values). |
| `response_flag` | `metadata` | A 3-letter error classification code set by the gateway, e.g. `URS` (upstream returned 5XX), `AKI` (API key invalid), `CBO` (circuit breaker open). Falls back to the HTTP status code string (e.g. `"200"`) when no error classification applies. See the [full list of response flags](/docs/nightly/api-management/logs/access-logs#error-classification). |

Use `response_code` to break down traffic by HTTP status. Use `response_flag` to understand Gateway-level error causes.

The `status_codes` filter in the `filters` block decides *whether* a request is recorded at all. The `response_code` dimension attaches the actual code as a label on the recorded data points. A common pattern is to combine both: filter to error responses, then break them down by exact code:

```json theme={null}
{
  "name": "tyk.api.errors.by_status",
  "type": "counter",
  "dimensions": [
    { "source": "metadata", "key": "response_code", "label": "http_status_code" },
    { "source": "metadata", "key": "api_id",        "label": "api_id" }
  ],
  "filters": { "status_codes": ["4xx", "5xx"] }
}
```

### Request path

Four options are available, at different cardinality levels:

| | Source | Key | Cardinality | Notes |
| - | - | - | - | - |
| API listen path | `metadata` | `listen_path` | Bounded (one value per API) | The configured listen path (e.g. `/api/v1/`). Safe to use in any metric. |
| Endpoint template | `metadata` | `endpoint` | Bounded (one per track endpoint) | The matched path template (e.g. `/user/{id}`). Requires `track_endpoints` configured on the API. Empty string for unmatched requests. |
| Raw request path | `context` | `path` | ⚠️ Potentially unbounded | The URL path as requested by the client (e.g. `/api/v1/users/12345`). Avoid unless paths have known low cardinality. |
| Path segments | `context` | `path_parts` | ⚠️ Potentially unbounded | The path split by `/` into a list of segments (e.g. `/api/v1/users/123` → `["api", "v1", "users", "123"]`). Access a specific segment with `{{ index ._tyk_context.path_parts N }}`. Carries the same cardinality risk as `path` if any segment varies per request. |

Prefer `listen_path` or `endpoint` (with `track_endpoints`) over raw `path` or `path_parts` in most cases.

### Client and auth identity

Several sources expose client identity, with different cardinality trade-offs:

* **Session fields** (`session` source): `alias` (the human-readable key alias), `portal_app` (Developer Portal app ID), and `portal_org` (Developer Portal org ID) are typically bounded and safe to use. `api_key` and `oauth_id` are ⚠️ high cardinality (one value per key/client); only use these with [cardinality control](/docs/nightly/api-management/logs-metrics#cardinality-control) enabled.
* **JWT claims** (`context` source): any claim is available as `jwt_claims_<name>` (e.g. `jwt_claims_tier`, `jwt_claims_tenant_id`). See [Request context variables](#request-context-variables) for setup requirements and cardinality notes.
* **Request headers** (`header` source): use when client identity is passed as a header (e.g. `X-Tenant-ID`). Cardinality depends on what the header contains.

### Custom API metadata

If you want to attach static, API-level metadata (such as team ownership, service tier, cost centre, or criticality) to your metrics without touching request headers or tokens, use the `config_data` source.

The [config\_data](/docs/nightly/api-management/gateway-config-tyk-oas#pluginconfigdata) field in the API definition is a free-form key-value map you can populate per API:

```json theme={null}
{
  "config_data": {
    "environment": "staging",
    "team": "platform",
    "criticality": "high"
  }
}
```

A dimension like `{"source": "config_data", "key": "team"}` will carry `team="platform"` on every metric recorded for that API; no request modification required.

**Fallback behaviour:** If `config_data_disabled` is `true` on the API, or the key is absent, the dimension falls back to the `default` value on the dimension definition. If no default is set, an empty string is used. All values are treated as strings.

### Request context variables

The `context` source reads from Tyk's [request context variables](/docs/nightly/api-management/traffic-transformation/request-context-variables), a set of values extracted from the incoming request and enriched during middleware processing. The `context` source covers two kinds of variables: those populated automatically by Tyk, and those written by custom plugins.

<Note>
  Context variables must be enabled on the API definition (`enable_context_vars: true`) for the `context` source to work. Without this setting, all `context` dimensions will be empty.
</Note>

#### Default context variables (no plugin required)

When context variables are enabled, Tyk automatically populates the following variables for every request:

| Key | Description | Cardinality |
| - | - | - |
| `request_id` | A generated correlation ID for the request. | ⚠️ Unbounded, unique per request. Not suitable as a metric dimension. |
| `path` | The raw request path (e.g. `/api/v1/users/123`). | ⚠️ Potentially unbounded. See [Request path](#request-path). |
| `path_parts` | The request path split by `/` into a list of segments. E.g. `/api/v1/users/123` → `["api", "v1", "users", "123"]`. Access individual segments with `{{ index ._tyk_context.path_parts N }}`. | ⚠️ Potentially unbounded. |
| `remote_addr` | The connecting client IP address. | ⚠️ High cardinality. |
| `jwt_claims_<name>` | Individual JWT claims, e.g. `jwt_claims_tier`, `jwt_claims_tenant_id`. Available when JWT auth is in use. | Bounded if the claim takes a fixed set of values (e.g. subscription tier). Safe to use. |
| `cookies_<name>` | Cookie values by name (hyphens replaced with underscores). | Depends on cookie values. |
| `headers_<name>` | Request header values by name (capitalised, hyphens replaced with underscores, e.g. `headers_User_Agent`). | Depends on header values. |
| `token` | The raw inbound bearer token. | ⚠️ Extremely high cardinality. Do not use as a dimension. |

Usually the most useful defaults for metrics are **JWT claims**: they let you segment traffic by tenant, subscription tier, or any other bounded claim without requiring a plugin:

```json theme={null}
{ "source": "context", "key": "jwt_claims_tier", "label": "tier", "default": "standard" }
```

#### Custom context variables (via plugin)

Go plugins and [JQ request transforms](/docs/nightly/api-management/traffic-transformation/jq-transforms) can write arbitrary variables into the request context, which are then accessible as `context` source dimensions. This is useful for computed signals, such as enriched tenant IDs, feature flags, routing decisions, or values decoded from opaque tokens.

**Go plugin:**

```go theme={null}
import "github.com/TykTechnologies/tyk/ctx"

func MyPlugin(w http.ResponseWriter, r *http.Request) {
    if ctxData, ok := r.Context().Value(ctx.ContextData).(map[string]interface{}); ok {
        ctxData["my_custom_var"] = "value"
    }
}
```

**JQ transform**: return a `tyk_context` object alongside the transformed body:

```json theme={null}
{
  "body": "<transformed-body>",
  "tyk_context": { "my_custom_var": "value" }
}
```

The variable is then available as a dimension:

```json theme={null}
{ "source": "context", "key": "my_custom_var", "label": "my_variable" }
```

<Note>
  Python, gRPC (coprocess), and JavaScript (JSVM) plugins cannot write to request context variables. To pass data from these plugins into metrics dimensions, use session metadata (`session` source, via `session.meta_data`) or inject a custom HTTP request header (`header` source) instead.
</Note>

### MCP dimensions

When a request is handled by an MCP API, Tyk populates four additional `metadata` keys derived from the JSON-RPC payload. These are available as dimensions in any custom metric instrument.

| Key | Description | Example Values | Cardinality |
| - | - | - | - |
| `mcp_method` | JSON-RPC method invoked | `tools/call`, `initialize`, `resources/read`, `prompts/get` | Bounded |
| `mcp_primitive_type` | MCP primitive category | `tool`, `resource`, `prompt` | Bounded (3 values) |
| `mcp_primitive_name` | Name of the specific tool, resource, or prompt | `get_current_weather`, `search_docs` | Bounded per API |
| `mcp_error_code` | JSON-RPC error code on failure; empty string on success | `-32001`, `-32002`, \`\` | Bounded |

<Note>
  These keys are only populated for MCP APIs. For non-MCP requests all four keys resolve to an empty string; use the `default` field on the dimension definition to set a fallback value. There is no performance overhead on non-MCP metric configurations.
</Note>

**Example: count tool calls by tool name:**

```json theme={null}
{
  "name": "tyk.mcp.tool_calls.total",
  "type": "counter",
  "description": "MCP tool invocations by tool name and API",
  "dimensions": [
    { "source": "metadata", "key": "mcp_primitive_name", "label": "tool_name", "default": "" },
    { "source": "metadata", "key": "mcp_primitive_type", "label": "primitive_type", "default": "" },
    { "source": "metadata", "key": "api_id",             "label": "api_id" }
  ],
  "filters": { "methods": ["POST"] }
}
```

For a complete set of MCP monitoring use cases, see [MCP Observability](/docs/nightly/ai-management/mcp-gateway/mcp-metrics).

### Other request metadata

Remaining `metadata` keys available for general request context:

| Key | Description | Cardinality |
| - | - | - |
| `method` | HTTP method (`GET`, `POST`, etc.) | Bounded |
| `api_id` | Tyk API ID | Bounded |
| `api_name` | API display name | Bounded |
| `org_id` | Organisation ID | Bounded |
| `api_version` | API version name | Bounded |
| `host` | Request host | Typically bounded |
| `scheme` | URL scheme (`http`, `https`) | Bounded |
| `ip_address` | Client IP address | ⚠️ High cardinality |

Each dimension definition has four fields:

| Field | Required | Description |
| - | - | - |
| `source` | Yes | One of the source names above. |
| `key` | Yes | The specific field or header name to read from. |
| `label` | Yes | The dimension name as it appears in your metrics backend. |
| `default` | No | Fallback value when the key is not present on the request. |

## Filters

Each instrument can restrict which requests it records using the `filters` block. All filter conditions use AND logic (a request must match all specified filters):

| Filter Field | Type | Description |
| - | - | - |
| `api_ids` | string array | Only record requests to these API IDs. |
| `methods` | string array | Only record requests with these HTTP methods (e.g., `["GET", "POST"]`). |
| `status_codes` | string array | Only record responses with these status codes. Supports exact values (`"200"`) and class patterns (`"2xx"`, `"4xx"`, `"5xx"`). |

## Example Configuration

The following example defines two custom instruments:

1. A counter that tracks requests by customer ID and subscription tier
2. A histogram that tracks latency for premium-tier requests only

```json expandable theme={null}
{
  "opentelemetry": {
    "metrics": {
      "enabled": true,
      "api_metrics": [
        {
          "name": "tyk.requests.by_customer",
          "type": "counter",
          "description": "Request count by customer ID and subscription tier",
          "dimensions": [
            {
              "source": "header",
              "key": "X-Customer-ID",
              "label": "customer_id",
              "default": "unknown"
            },
            {
              "source": "context",
              "key": "jwt_claims_tier",
              "label": "tier",
              "default": "standard"
            },
            {
              "source": "metadata",
              "key": "api_id",
              "label": "api_id"
            }
          ],
          "filters": {
            "api_ids": ["payments-api", "orders-api"]
          }
        },
        {
          "name": "tyk.latency.premium_tier",
          "type": "histogram",
          "description": "End-to-end latency for premium tier requests",
          "histogram_source": "total",
          "histogram_buckets": [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
          "dimensions": [
            {
              "source": "context",
              "key": "jwt_claims_tier",
              "label": "tier"
            },
            {
              "source": "metadata",
              "key": "api_id",
              "label": "api_id"
            }
          ],
          "filters": {
            "methods": ["POST", "PUT", "PATCH"],
            "status_codes": ["2xx"]
          }
        }
      ]
    }
  }
}
```

## Full Field Reference

| Field | Type | Required | Description |
| - | - | - | - |
| `name` | string | Yes | Metric instrument name. Use lowercase with dots or underscores (e.g., `tyk.requests.by_customer`). |
| `type` | string | Yes | `"counter"` or `"histogram"`. |
| `description` | string | No | Human-readable description included in metric metadata. |
| `dimensions` | array | Yes | List of dimension definitions. See [Dimension Sources](#dimension-sources). |
| `filters` | object | No | Restricts which requests are recorded. See [Filters](#filters). |
| `histogram_source` | string | Histograms only | `"total"`, `"gateway"`, or `"upstream"`. |
| `histogram_buckets` | float array | No | Custom histogram bucket boundaries in seconds. If omitted, OTel default buckets are used. |

## Cardinality Considerations

Custom dimensions can significantly increase cardinality. Before adding a dimension, estimate the number of unique values it can take:

* `api_id`: bounded (tens or hundreds of APIs)
* `method`: bounded (fewer than 10 values)
* `customer_id` from a header: potentially unbounded if unique per user

The `cardinality_limit` setting (default: 2,000 unique combinations per instrument) protects against unbounded cardinality. When the limit is reached, new combinations are tracked in an overflow bucket. See [Cardinality Control](/docs/nightly/api-management/logs-metrics#cardinality-control) for details.

<Note>
  On Tyk Cloud, custom metric configuration is not currently self-service. Contact Tyk support to request custom dimensions for your Cloud deployment.
</Note>
