Metrics
Counter, gauge, and distribution metrics sent as batched trace_metric envelope items.
This spec defines the format used by Sentry to ingest metrics, as well as the SDK API and behavior that create and send metrics to Sentry. Metrics are sent as trace_metric envelope items containing JSON-encoded payloads.
Note
The envelope item type is named trace_metric for internal usage to avoid naming collisions with other metric systems within Sentry's infrastructure. From an SDK perspective, these are simply referred to as "metrics".
Related specs:
- Envelopes — transport format
- Attributes — attribute types and structure
- Tracing without Performance — required before adding metrics support
- Telemetry Processor — buffer data forwarding scenarios
A metric has a type that determines how its values are interpreted:
- Counter — tracks the number of times an event occurs. Only increases. Useful for requests, errors, or any countable event.
- Gauge — tracks a value that can arbitrarily increase or decrease over time, such as memory usage, queue depth, or connection pool size.
- Distribution — tracks the statistical distribution of a set of values. Useful for understanding percentiles, averages, and other statistical properties of measurements like response times or file sizes.
Metrics carry typed key-value pairs called attributes for dimensional filtering and grouping. Each attribute declares its type (string, boolean, integer, double). See Attributes for the full type specification.
Multiple metrics are batched into a single trace_metric envelope item for efficient transport. SDKs buffer metrics locally and flush them periodically or when size thresholds are reached.
Note
An SDK SHOULD implement Tracing without Performance before adding support for metrics. This is required to ensure that metrics are associated with traces and that the correct trace context is sent to Sentry.
Metric processing MUST follow this order:
- Capture metric via Public APIs (e.g.
Sentry.metrics.count). - Check if metrics are disabled as per
enableMetrics/enable_metricsconfiguration — if so, skip the rest of the steps. - Pass the metric to Hub/Scope via a generic method (e.g.,
captureMetric(metric)). - Pass the metric to the Client via a generic method (e.g.,
captureMetric(metric)). - Process captured metric (attach default attributes as per Default Attributes).
- Run
beforeSendMetric/before_send_metricto filter or modify the metric. - Add metric to buffer/batch processor as detailed in Buffering.
- At time of flushing buffer, send array of metrics to Sentry via
trace_metricenvelope, apply rate limiting as per Data Category and Rate Limiting.
SDKs MUST attach the following attributes to every metric by default:
sentry.environment— the environment set in the SDK, if defined.sentry.release— the release set in the SDK, if defined.sentry.sdk.name— the name of the SDK that sent the metric.sentry.sdk.version— the version of the SDK that sent the metric.
SDKs SHOULD minimize the number of default attributes. Metrics cardinality can explode quickly with too many attributes. New default attributes SHOULD only be added after significant feedback from users and discussion internally with the SDK and ingest teams.
SDKs MAY attach user information as attributes, guarded by sendDefaultPii (unless manually set by user, since 2.5.0):
By default, Relay parses the user agent attached to an incoming metric envelope to extract browser and os information. These attributes are attached by Relay, but client-side SDKs MAY attach them if they do not forward a user agent when sending metrics to Sentry.
browser.name— display name of the browser application. Maps tonamein the Browser Context.browser.version— version string of the browser. Maps toversionin the Browser Context.
Backend SDKs (Node.js, Python, PHP, etc.) SHOULD attach the following attribute:
server.address— the address of the server that sent the metric. Equivalent toserver_nameattached to errors and transactions.
SDKs MUST buffer metrics before sending them (since 2.2.0, tightened from SHOULD). SDKs SHOULD flush the buffer when specific conditions are met. For initial implementation, a simple strategy is fine, for example: flushing if the buffer length exceeds 100 items or if 5 seconds have passed.
To prevent data loss, the buffer SHOULD forward metrics to the transport in the scenarios outlined in the telemetry buffer data forwarding scenarios (since 2.2.0).
Furthermore:
- The aggregation window SHOULD be time and size based.
- SDKs SHOULD implement safeguards to prevent excessive memory usage from metric buffering.
Metrics SHOULD be associated with traces if possible. If a metric is recorded during an active span, the SDK SHOULD set the span_id property of the metric to the span ID of the span that was active when the metric was collected.
The trace_id field is REQUIRED on every metric payload and MUST be taken from the current propagation context.
Whenever possible, metrics SHOULD be linked to replays. If a metric is recorded while an active, sampled replay is in progress, the SDK SHOULD attach:
sentry.replay_id— the ID of the replay that was active when the metric was collected. This MUST NOT be set if there was no active replay.sentry._internal.replay_is_buffering— indicates that the metric is associated with a replay that is currently buffering. This ensures correct handling of scenarios where areplay_idis present but the corresponding replay was never actually sent.
Some runtimes (notably Cloudflare Workers) freeze timer APIs (Date.now(), performance.now()) during synchronous execution, causing multiple metrics to share identical timestamps. SDKs that target runtimes where timestamps may be frozen or lack sub-millisecond precision MUST attach a sentry.timestamp.sequence integer attribute to every metric. SDKs that only target runtimes with reliable sub-millisecond timestamps MAY omit it.
When sent, the sequence integer MUST:
- Start at
0when the SDK initializes. - Increment by
1for each metric that is captured. - Reset to
0when:- The SDK is re-initialized.
- The current metric's integer millisecond differs from the previous metric's integer millisecond (i.e.,
floor(timestamp_seconds * 1000)changes).
The sequence provides deterministic ordering within a single SDK instance. It does not guarantee ordering across independent processes or workers, which have separate counters. The reset behavior ensures the sequence only increments for consecutive metrics that share the same timestamp.
The data category for metrics is trace_metric. Rate limiting applies as usual. SDKs SHOULD track client outcomes to monitor how often metrics are dropped. If the SDK receives a rate limit, it MUST NOT send any trace metrics until the limit expires (based on the communicated end timestamp).
If debug is set to true in SDK init, calls to the Sentry metrics API SHOULD also print to the console with appropriate details. This will help debugging metrics setups.
Metrics emitted via the public API are opt-out. The enableMetrics/enable_metrics option MUST act as a general kill switch for all metrics sent to Sentry — when set to false, no metrics MUST be sent, regardless of whether they originate from the public API, auto-emitting integrations, or third-party bindings.
However, metrics that are automatically emitted by integrations or libraries without an explicit user call follow different rules because they can generate unexpected volume and cost.
Integrations that auto-emit metrics — An SDK integration that automatically captures metrics (e.g., recording HTTP request durations, database query counts, or runtime resource usage) MUST be opt-in. The integration MUST NOT emit any metrics unless the user explicitly enables it. The opt-in mechanism MAY be a dedicated integration-level option or requiring the user to explicitly add the integration.
Third-party metrics bindings — An SDK feature that binds to an external library's metrics API and mirrors those metrics as Sentry metrics in the background (e.g., syncing from a framework's built-in metrics or an OpenTelemetry metrics pipeline) MUST also be opt-in. Users MUST explicitly enable the binding before any metrics are forwarded to Sentry.
These rules exist because auto-emitted metrics can cause a high volume of telemetry that directly impacts a user's quota. Unlike spans or errors — where users have prior intuition about volume — silently emitting metrics on their behalf could lead to unexpected costs. Requiring opt-in ensures users remain in control of their metrics volume.
Metrics are sent as a trace_metric envelope item containing a JSON object with an items array. See the Examples section for a complete envelope.
An envelope MUST contain at most one trace_metric envelope item — all metrics for a flush are batched into a single item's items array. Metrics from different traces MAY be mixed into the same trace_metric item, but if they are, the envelope MUST NOT include a DSC (dynamic sampling context) header.
Item Headers:
| Field | Type | Required | Description |
|---|---|---|---|
type | String | REQUIRED | MUST be "trace_metric". |
item_count | Number | REQUIRED | Number of metric items in the items array. MUST match the actual count. |
content_type | String | REQUIRED | MUST be "application/vnd.sentry.items.trace-metric+json". |
{
"type": "trace_metric",
"item_count": 10,
"content_type": "application/vnd.sentry.items.trace-metric+json"
}
{
"items": [{..metric..}, {..metric..}, {..metric..}]
}
Each metric in the items array is a JSON object with the following fields:
{
"timestamp": 1544719860.0,
"trace_id": "5b8efff798038103d269b633813fc60c",
"span_id": "b0e6f15b45c36b12",
"name": "api.response_time",
"value": 125.5,
"unit": "millisecond",
"type": "distribution",
"attributes": {
"endpoint": { "value": "api/users", "type": "string" },
"method": { "value": "GET", "type": "string" },
"status_code": { "value": 200, "type": "integer" }
}
}
| Field | Type | Required | Since | Description |
|---|---|---|---|---|
timestamp | Number | REQUIRED | 2.0.0 | Timestamp in seconds (epoch time) indicating when the metric was recorded. |
type | String | REQUIRED | 2.0.0 | The type of metric. One of counter, gauge, distribution. |
name | String | REQUIRED | 2.0.0 | The name of the metric. SHOULD follow a hierarchical naming convention using dots as separators (e.g., api.response_time, db.query.duration). |
value | Number | REQUIRED | 2.0.0 | The numeric value. For counter: the count to increment by (default 1). For gauge: the current value. For distribution: a single measured value. Integers MUST be 64-bit signed; doubles MUST be 64-bit floating point. |
trace_id | String | REQUIRED | 2.0.0 | 16 random bytes encoded as a hex string (32 characters). Taken from the current propagation context. |
span_id | String | OPTIONAL | 2.3.0 | 8 random bytes encoded as a hex string (16 characters). The span ID of the span that was active when the metric was emitted. |
unit | String | OPTIONAL | 2.0.0 | The unit of measurement for the metric value. |
attributes | Object | OPTIONAL | 2.0.0 | Typed key-value pairs. See Attributes for supported types and structure. |
SDKs MUST expose the following configuration options:
enableMetrics/enable_metrics— a boolean flag to control if metric envelopes will be generated and sent to Sentry. Iffalse, the SDK MUST NOT send metrics. Defaults totrue.beforeSendMetric/before_send_metric— an OPTIONAL function that takes a metric object and returns a metric object. Called before sending the metric to Sentry. Can be used to modify the metric or prevent it from being sent (by returningnull/None).
While metrics functionality is in an experimental state, SDKs SHOULD put these options in an experimental namespace to avoid breaking changes:
Sentry.init({
// stable
enableMetrics: true,
// experimental
_experiments: { enableMetrics: true },
})
SDKs MUST expose metrics methods in a metrics module or namespace. At minimum, the SDK MUST implement the following methods:
Sentry.metrics.count(name, value, options)— increment a counterSentry.metrics.gauge(name, value, options)— set a gauge valueSentry.metrics.distribution(name, value, options)— add a distribution value
Parameters:
nameString, required — the name of the metric.valueNumber, required — the value of the metric.optionsObject, optional — containing:unitString, optional — the unit of measurement (only used fordistributionandgauge). SDKs MUST NOT restrict what can be sent in (e.g. by using an enum) but SHOULD offer constants or similar that help customers send in units we support (since 2.4.0).attributesObject, optional — a dictionary of attributes (key-value pairs with type information).scopeScope, optional — the scope to capture the metric with.
The Hub/Scope and Client MUST exclusively provide a generic method for capturing metrics (e.g., captureMetric(metric)). They MUST NOT replicate the static Sentry.metrics.X API methods to avoid increasing our API surface. Users utilizing multiple Hub/Scope or Clients will need to rely on the generic method for capturing metrics.
The threading and concurrency considerations for metrics are the same as for logs. See the Threading and Concurrency Considerations section in the Logs documentation for more information.
{
"name": "api.requests",
"value": 1,
"type": "counter",
"attributes": {
"endpoint": { "value": "/api/users", "type": "string" },
"method": { "value": "POST", "type": "string" }
}
}
{
"name": "db.connection_pool.active",
"value": 42,
"unit": "connection",
"type": "gauge",
"attributes": {
"pool_name": { "value": "main_db", "type": "string" },
"max_size": { "value": 100, "type": "integer" },
"database": { "value": "postgres", "type": "string" }
}
}
{
"name": "page.load_time",
"value": 245.7,
"unit": "millisecond",
"type": "distribution",
"attributes": {
"page": { "value": "/dashboard", "type": "string" },
"browser": { "value": "chrome", "type": "string" }
}
}
// Increment a counter
Sentry.metrics.count("button.clicks", 1, {
attributes: {
button_id: "submit",
page: "checkout",
},
});
// Set a gauge value
Sentry.metrics.gauge("db.connection_pool.active", 42, {
unit: "connection",
attributes: {
pool_name: "main_db",
database: "postgres",
},
});
// Record a distribution
Sentry.metrics.distribution("page.load_time", 245.7, {
unit: "millisecond",
attributes: {
page: "/dashboard",
browser: "chrome",
},
});
{
"attributes": {
"sentry.environment": { "value": "production", "type": "string" },
"sentry.release": { "value": "1.0.0", "type": "string" },
"sentry.sdk.name": {
"value": "sentry.javascript.browser",
"type": "string"
},
"sentry.sdk.version": { "value": "10.17.0", "type": "string" },
"sentry.replay_id": {
"value": "36b75d9fa11f45459412a96c41bdf691",
"type": "string"
}
}
}
{
"attributes": {
"user.id": { "value": "123", "type": "string" },
"user.name": { "value": "john.doe", "type": "string" },
"user.email": { "value": "john.doe@example.com", "type": "string" }
}
}
{ "sdk": { "name": "sentry.javascript.browser", "version": "10.17.0" } }
{
"type": "trace_metric",
"item_count": 5,
"content_type": "application/vnd.sentry.items.trace-metric+json"
}
{
"items": [
{
"timestamp": 1746456149.019,
"trace_id": "624f66e93a04469f9992c7e9f1485056",
"span_id": "b0e6f15b45c36b12",
"name": "api.requests",
"value": 1,
"type": "counter",
"attributes": {
"endpoint": { "value": "/api/users", "type": "string" },
"method": { "value": "POST", "type": "string" },
"status_code": { "value": 201, "type": "integer" },
"sentry.sdk.name": { "value": "sentry.javascript.browser", "type": "string" },
"sentry.sdk.version": { "value": "10.17.0", "type": "string" },
"sentry.environment": { "value": "production", "type": "string" },
"sentry.release": { "value": "1.0.0", "type": "string" },
"sentry.timestamp.sequence": { "value": 0, "type": "integer" }
}
},
{
"timestamp": 1746456149.019,
"trace_id": "624f66e93a04469f9992c7e9f1485056",
"span_id": "b0e6f15b45c36b12",
"name": "api.response_time",
"value": 125.5,
"unit": "millisecond",
"type": "distribution",
"attributes": {
"endpoint": { "value": "/api/users", "type": "string" },
"method": { "value": "POST", "type": "string" },
"sentry.sdk.name": { "value": "sentry.javascript.browser", "type": "string" },
"sentry.sdk.version": { "value": "10.17.0", "type": "string" },
"sentry.timestamp.sequence": { "value": 1, "type": "integer" }
}
},
{
"timestamp": 1746456149.02,
"trace_id": "624f66e93a04469f9992c7e9f1485056",
"span_id": "b0e6f15b45c36b12",
"name": "cache.hit_rate",
"value": 0.95,
"unit": "ratio",
"type": "gauge",
"attributes": {
"cache_name": { "value": "user_sessions", "type": "string" },
"region": { "value": "us-west-1", "type": "string" },
"sentry.sdk.name": { "value": "sentry.javascript.browser", "type": "string" },
"sentry.sdk.version": { "value": "10.17.0", "type": "string" },
"sentry.timestamp.sequence": { "value": 0, "type": "integer" }
}
},
{
"timestamp": 1746456149.02,
"trace_id": "624f66e93a04469f9992c7e9f1485056",
"span_id": "b0e6f15b45c36b12",
"name": "database.query.duration",
"value": 42.3,
"unit": "millisecond",
"type": "distribution",
"attributes": {
"query_type": { "value": "SELECT", "type": "string" },
"table": { "value": "users", "type": "string" },
"sentry.sdk.name": { "value": "sentry.javascript.browser", "type": "string" },
"sentry.sdk.version": { "value": "10.17.0", "type": "string" },
"sentry.origin": { "value": "auto.db.graphql", "type": "string" },
"sentry.timestamp.sequence": { "value": 1, "type": "integer" }
}
},
{
"timestamp": 1746456149.021,
"trace_id": "624f66e93a04469f9992c7e9f1485056",
"span_id": "b0e6f15b45c36b12",
"name": "active.users",
"value": 12345,
"type": "gauge",
"attributes": {
"cohort": { "value": "beta", "type": "string" },
"sentry.sdk.name": { "value": "sentry.javascript.browser", "type": "string" },
"sentry.sdk.version": { "value": "10.17.0", "type": "string" },
"sentry.replay_id": { "value": "36b75d9fa11f45459412a96c41bdf691", "type": "string" },
"sentry.timestamp.sequence": { "value": 0, "type": "integer" }
}
}
]
}
- Experimental JS SDK PR #17883 - Metrics API Implementation
- Experimental Python SDK PR #4898 - Metrics API Implementation
- Batch Processor Specification
| Version | Date | Summary |
|---|---|---|
2.7.0 | 2026-03-10 | Auto-emitted metrics and third-party metrics bindings MUST be opt-in |
2.6.0 | 2026-03-04 | Add sentry.timestamp.sequence attribute for deterministic metric ordering |
2.5.0 | 2026-02-12 | Clarified sendDefaultPii gating for user attributes — allowed when user manually sets data |
2.4.1 | 2026-01-19 | Fixed example — replaced unsupported set type with gauge |
2.4.0 | 2026-01-14 | SDKs MUST NOT restrict unit values, SHOULD offer constants for supported units |
2.3.1 | 2026-01-09 | Fixed Swift API examples (syntax correction) |
2.3.0 | 2025-12-15 | Added span_id field, added replay association (sentry.replay_id, sentry._internal.replay_is_buffering) |
2.2.0 | 2025-11-19 | Tightened buffering from SHOULD to MUST, added telemetry buffer forwarding scenarios |
2.1.0 | 2025-11-17 | Added backend SDK default attributes (server.address) |
2.0.0 | 2025-11-13 | Breaking — new trace_metric JSON envelope format, attribute system replacing tags |
1.0.0 | 2023-12-07 | Initial metrics spec (statsd-based format, now superseded) |
Our documentation is open source and available on GitHub. Your contributions are welcome, whether fixing a typo (drat!) or suggesting an update ("yeah, this would be better").