<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Felix Barnsteiner - Elastic Observability Labs]]></title>
    <description><![CDATA[Trusted security news & research from the team at Elastic.]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Felix Barnsteiner - Elastic Observability Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltad972c1c27dbefc6/6a88d9782904ea5e8511d473/observability-labs-thumbnail.png</url>
      <link>https://www.elastic.co/observability-labs/author/felix-barnsteiner</link>
    </image>
    <link>https://www.elastic.co/observability-labs/author/felix-barnsteiner</link>
    <atom:link href="https://www.elastic.co/observability-labs/rss/author/felix-barnsteiner.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <lastBuildDate>Fri, 11 Sep 2026 20:30:16 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Use Elasticsearch as a Drop-In Prometheus Backend for Grafana]]></title>
    <description><![CDATA[Use Elasticsearch as a Prometheus backend for Grafana dashboards, autocomplete, Metrics Drilldown, and alerting without changing PromQL workflows.]]></description>
    <content:encoded><![CDATA[<p>Elasticsearch is already one of the most popular plugins in the Grafana ecosystem, and we have now made it much more powerful for metrics usage.
If you run Prometheus today and use Grafana to visualize your metrics, you can now point Grafana's Prometheus data source directly at Elasticsearch.
No sidecars, no adapters, no pipeline changes required.</p>
<p>Elasticsearch now implements a native Prometheus-compatible API layer, which covers <a href="https://www.elastic.co/blog/prometheus-remote-write-elasticsearch">ingestion via Remote Write</a> and <a href="https://www.elastic.co/blog/elasticsearch-supports-promql">querying via PromQL</a>.
This post shows the Grafana setup end to end.
Companion posts also cover <a href="https://www.elastic.co/blog/promql-queries-run-in-kibana">PromQL in Kibana</a> and the <a href="https://www.elastic.co/blog/prometheus-remote-write-elasticsearch-architecture">Remote Write architecture</a>.</p>
<h2 id="whyuseelasticsearchasaprometheusbackend">Why use Elasticsearch as a Prometheus backend?</h2>
<p>Over the last year, Elasticsearch has become a state-of-the-art metrics store: <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/time-series-data-stream-tsds">time series data streams</a>, ES|QL's <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/ts"><code>TS</code> command</a>, and storage and query optimizations that deliver strong <a href="https://www.elastic.co/search-labs/blog/elasticsearch-columnar-metrics-engine-30x-faster-prometheus">metrics performance for Prometheus-style workloads</a>.
The Prometheus-compatible API layer makes that engine reachable through the tools your team already uses.</p>
<p>Many teams have invested heavily in Prometheus-based tooling: dashboards, runbooks that reference PromQL queries, on-call workflows built around Grafana panels.
Elasticsearch's Prometheus-compatible endpoints let you move metrics storage while keeping those Grafana workflows.</p>
<p>This is particularly relevant if you already use Elasticsearch for logs or traces and want to consolidate your observability data into a single platform, while keeping your Grafana-based workflows intact.</p>
<h2 id="whattheelasticsearchprometheusapiincludes">What the Elasticsearch Prometheus API includes</h2>
<p>The Elasticsearch Prometheus API exposes three endpoint groups.</p>
<h3 id="queryapis">Query APIs</h3>
<p>The core query endpoints allow Grafana to evaluate PromQL expressions against data stored in Elasticsearch:</p>
<ul>
<li><code>GET</code> and <code>POST /_prometheus/api/v1/query_range</code> evaluate a PromQL expression over a time window and return matrix results.
This is what powers most Grafana dashboard panels.</li>
<li><code>GET</code> and <code>POST /_prometheus/api/v1/query</code> evaluate a PromQL expression at a single point in time and return vector results.</li>
</ul>
<p>Both endpoints implement the standard Prometheus response envelope, including result types (vector, matrix, scalar, string), status codes, and error handling.
For <code>POST</code>, send parameters in an <code>application/x-www-form-urlencoded</code> body, matching Prometheus client behavior.</p>
<h3 id="metadataapis">Metadata APIs</h3>
<p>Grafana's metric explorer, autocomplete, and variable dropdowns rely on metadata endpoints to discover what's available.
Elasticsearch supports:</p>
<ul>
<li><code>GET</code> and <code>POST /_prometheus/api/v1/series</code> return time series matching label selectors.</li>
<li><code>GET</code> and <code>POST /_prometheus/api/v1/labels</code> return all available label names.</li>
<li><code>GET /_prometheus/api/v1/label/{name}/values</code> returns all values for a given label.</li>
<li><code>GET /_prometheus/api/v1/metadata</code> returns type and help text for each metric name.</li>
</ul>
<p>These endpoints power autocomplete and the metric browser in Grafana.
The <code>/metadata</code> endpoint additionally enables Grafana's <a href="https://grafana.com/docs/grafana/latest/explore/explore-metrics/">Metrics Drilldown</a>: an interactive metric explorer that displays all available metrics as a grid of live sparklines and lets you drill into any metric without writing a PromQL query.</p>
<h3 id="indexprefiltering">Index pre-filtering</h3>
<p>All query and metadata endpoints accept an optional <code>{index}</code> path segment immediately after <code>/_prometheus/</code>, for example:</p>
<pre><code>GET /_prometheus/metrics-prod-*/api/v1/query_range
</code></pre>
<p>This pre-filters the Elasticsearch indices that the PromQL query runs against before any expression evaluation happens.
Scoping queries to the relevant data can reduce query work for dashboards that span large volumes of metrics across different data streams.</p>
<p>You can configure a separate Grafana data source per index pattern to give teams scoped access to their own metrics.</p>
<h3 id="remotewriteingestion">Remote Write ingestion</h3>
<p>Elasticsearch also implements the <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/tsds-ingest-prometheus-remote-write">Prometheus Remote Write protocol</a>, which lets you ship metrics from Prometheus to Elasticsearch using the standard <code>remote_write</code> configuration.
Adding Elasticsearch as a remote write destination requires a single block in your existing Prometheus config:</p>
<pre><code>remote_write:
  - url: "&lt;es_endpoint&gt;/_prometheus/api/v1/write"
    authorization:
      type: ApiKey
      credentials: &lt;api_key&gt;
</code></pre>
<p>Metrics are stored in the <code>metrics-generic.prometheus-default</code> data stream by default.
You can route metrics from different Prometheus instances or environments into separate data streams using the dataset and namespace path segments:</p>
<ul>
<li><code>POST /_prometheus/metrics/{dataset}/api/v1/write</code> stores metrics in <code>metrics-{dataset}.prometheus-default</code></li>
<li><code>POST /_prometheus/metrics/{dataset}/{namespace}/api/v1/write</code> stores metrics in <code>metrics-{dataset}.prometheus-{namespace}</code></li>
</ul>
<h2 id="howtoconnectgrafanatoelasticsearch">How to connect Grafana to Elasticsearch</h2>
<h3 id="step1createaserverlessproject">Step 1: Create a serverless project</h3>
<p>Sign in to <a href="https://cloud.elastic.co">cloud.elastic.co</a> and create a new <strong>Observability</strong> serverless project.
Once the project is ready, you will land directly in Kibana.
To find the Elasticsearch endpoint, go back to the Elastic Cloud console, open <strong>Manage &gt; Application endpoints, cluster and component IDs</strong>, and click the copy icon next to <strong>Elasticsearch</strong>.
The endpoint looks like:</p>
<pre><code>https://&lt;project-id&gt;.es.&lt;region&gt;.&lt;provider&gt;.elastic.cloud
</code></pre>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt37ae7707a46a689f/6a7f1a1c42a11755a895c2f5/elasticsearch-endpoint.png" alt="Elastic Cloud console showing the Application endpoints panel with the Elasticsearch endpoint and copy button" /></p>
<h3 id="step2createapikeys">Step 2: Create API keys</h3>
<p>Create two API keys with scoped privileges: one for ingestion, one for querying.
Using separate keys means a leaked Grafana key cannot be used to write data, and a leaked ingest key cannot be used to read it.</p>
<p>In your project, open <strong>Admin and settings</strong> (the ⚙️ icon at the bottom left of the side nav), go to <strong>API keys</strong>, and create the first key.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0c7a05f18adefd4d/6a7f1a1f448e4e7e355c0b5a/create-api-key.png" alt="Create API key dialog showing the name, type, and role descriptor for the ingest key" /></p>
<p><strong>Ingest key</strong> (<code>prometheus-remote-write</code>): restricts access to writing metrics data streams only.
In the <strong>Control security privileges</strong> section, paste the following role descriptor:</p>
<pre><code>{
  "ingest": {
    "indices": [
      {
        "names": ["metrics-*"],
        "privileges": ["auto_configure", "create_doc"]
      }
    ]
  }
}
</code></pre>
<p>Create a second key for Grafana in the same section.</p>
<p><strong>Query key</strong> (<code>prometheus-grafana</code>): restricts access to reading metrics data streams only.</p>
<pre><code>{
  "query": {
    "indices": [
      {
        "names": ["metrics-*"],
        "privileges": ["read", "view_index_metadata"]
      }
    ]
  }
}
</code></pre>
<p>Copy both key values before closing. You will not be able to retrieve them again.</p>
<h3 id="step3runprometheusandgrafana">Step 3: Run Prometheus and Grafana</h3>
<p>Create a <code>prometheus.yml</code> that scrapes Prometheus itself and forwards those metrics to Elasticsearch.
Replace <code>&lt;es_endpoint&gt;</code> with the endpoint from Step 1 and <code>&lt;ingest_api_key&gt;</code> with the ingest key from Step 2:</p>
<pre><code>global:
  scrape_interval: 15s

scrape_configs:
  - job_name: "prometheus"
    static_configs:
      - targets: ["localhost:9090"]

remote_write:
  - url: "&lt;es_endpoint&gt;/_prometheus/api/v1/write"
    authorization:
      type: ApiKey
      credentials: &lt;ingest_api_key&gt;
</code></pre>
<p>Next, create the Grafana provisioning directories:</p>
<pre><code>mkdir -p grafana/provisioning/datasources grafana/provisioning/dashboards
</code></pre>
<p>Then create a Grafana data source configuration that points at the Elasticsearch Prometheus API.
Create <code>grafana/provisioning/datasources/datasource.yml</code>, replacing <code>&lt;es_endpoint&gt;</code> and <code>&lt;query_api_key&gt;</code> with the values from Steps 1 and 2:</p>
<pre><code>apiVersion: 1

datasources:
  - name: Elasticsearch
    type: prometheus
    access: proxy
    url: "&lt;es_endpoint&gt;/_prometheus"
    uid: elasticsearch-prometheus
    isDefault: true
    jsonData:
      httpHeaderName1: Authorization
    secureJsonData:
      httpHeaderValue1: "ApiKey &lt;query_api_key&gt;"
</code></pre>
<p>This configures a Prometheus-type data source backed by Elasticsearch.
Grafana sends Prometheus queries with <code>POST</code> by default, which Elasticsearch accepts on authenticated HTTPS endpoints such as Serverless.</p>
<p>Create <code>grafana/provisioning/dashboards/dashboards.yml</code> to tell Grafana where to find provisioned dashboards:</p>
<pre><code>apiVersion: 1

providers:
  - name: default
    type: file
    options:
      path: /var/lib/grafana/dashboards
</code></pre>
<p>Finally, create a <code>docker-compose.yml</code> to start everything:</p>
<pre><code>services:
  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro

  download-dashboard:
    image: curlimages/curl:latest
    user: root
    volumes:
      - dashboards:/dashboards
    command: &gt;
      sh -c 'curl -fsSL https://grafana.com/api/dashboards/3662/revisions/2/download
      | sed "s/\$${DS_THEMIS}/elasticsearch-prometheus/g"
      &gt; /dashboards/prometheus-overview.json'

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    depends_on:
      download-dashboard:
        condition: service_completed_successfully
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=grafana
    volumes:
      - ./grafana/provisioning:/etc/grafana/provisioning:ro
      - dashboards:/var/lib/grafana/dashboards:ro

volumes:
  dashboards:
</code></pre>
<p>The <code>download-dashboard</code> service fetches the <a href="https://grafana.com/grafana/dashboards/3662-prometheus-2-0-overview/">Prometheus 2.0 Overview</a> dashboard from the Grafana marketplace and patches it to use the Elasticsearch data source.
The <code>sed</code> replaces the dashboard's <code>${DS_THEMIS}</code> data source placeholder with our data source UID.
This is needed because Grafana's provisioning does not resolve these placeholders on its own (<a href="https://github.com/grafana/grafana/issues/10786">grafana#10786</a>).
Grafana waits for the download to finish before starting.</p>
<p>Start both with:</p>
<pre><code>docker compose up -d
</code></pre>
<p>Prometheus will start scraping its own metrics and shipping them to Elasticsearch every 15 seconds.
Give it one or two scrape intervals before opening the dashboard.</p>
<h3 id="step4openthedashboard">Step 4: Open the dashboard</h3>
<p>Open Grafana at <code>http://localhost:3000</code> and log in with <code>admin</code> / <code>grafana</code>.
Go to <strong>Dashboards</strong> and open <strong>Prometheus 2.0 Overview</strong>.</p>
<p>The dashboard shows your Prometheus self-monitoring metrics, pulled from Elasticsearch via PromQL queries.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte3f55bf517eea926/6a7f1a22bd21988de17584a9/grafana-dashboard.png" alt="Grafana dashboard with Elasticsearch as the Prometheus data source, showing Prometheus self-monitoring metrics rendered by PromQL queries" /></p>
<h3 id="step5exploremetricswithgrafanasmetricsdrilldown">Step 5: Explore metrics with Grafana's Metrics Drilldown</h3>
<p>Because Elasticsearch implements the Prometheus metadata and discovery endpoints, Grafana's <a href="https://grafana.com/docs/grafana/latest/explore/explore-metrics/">Metrics Drilldown</a> works out of the box.</p>
<p>In Grafana, go to <strong>Drilldown &gt; Metrics</strong> in the left-hand navigation and select <strong>Elasticsearch</strong> as the data source.
Grafana loads all available metrics from Elasticsearch and displays them as a grid of live sparklines.
From there you can filter by label, search by name, and drill into any metric without writing PromQL.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8e51ae030a51c63e/6a7f1a26eab5be665d20aaf0/grafana-drilldown.png" alt="Grafana Metrics Drilldown showing all Prometheus metrics from Elasticsearch as a grid of sparklines" /></p>
<h2 id="currentlimitationsandwhatsnext">Current limitations and what's next</h2>
<p>This is the first implementation and updates should be expected.
All of the following are actively being worked on:</p>
<h3 id="promqlcoverageisnotyetcomplete">PromQL coverage is not yet complete</h3>
<p>Queries using group modifiers (for example, <code>on(instance, job)</code>), set operators (<code>or</code>, <code>and</code>, <code>unless</code>), and certain functions like <code>topk</code> are not yet supported.</p>
<h3 id="formencodedposthasdeploymentrequirements">Form-encoded POST has deployment requirements</h3>
<p><code>POST</code> requests with <code>application/x-www-form-urlencoded</code> bodies require security enabled, TLS on the Elasticsearch HTTP interface, and an authenticated request.
Serverless meets these requirements out of the box.
If TLS terminates before Elasticsearch and the node sees plain HTTP, use <code>GET</code> with query-string parameters instead.</p>
<h3 id="onlyremotewritev1issupported">Only Remote Write v1 is supported</h3>
<p>Remote Write v2 support is planned.</p>
<h3 id="instantqueriesarenotpointintimeyet">Instant queries are not point-in-time yet</h3>
<p>The instant query endpoint currently runs a short range query under the hood and returns the last sample.
It will be replaced with a proper point-in-time evaluation.</p>
<p>Coming next: broader PromQL function and operator coverage, Remote Write v2, and exemplar endpoints.</p>
<h2 id="frequentlyaskedquestions">Frequently asked questions</h2>
<p><strong>Can Grafana query Prometheus metrics stored in Elasticsearch?</strong>
Yes.
Grafana can use Elasticsearch as a Prometheus data source when the URL points to <code>/_prometheus</code>.
Queries use PromQL and return the standard Prometheus response format for Grafana dashboards, variables, Metrics Drilldown, and alerting.</p>
<p><strong>Do I need to change Prometheus or Grafana dashboards to use Elasticsearch?</strong>
You do not need to rewrite PromQL queries or dashboard panels for common Grafana use cases.
Configure Prometheus Remote Write to send metrics to Elasticsearch, then point Grafana's Prometheus data source at the Elasticsearch <code>/_prometheus</code> endpoint.</p>
<p><strong>Why use Elasticsearch instead of a separate Prometheus long term storage backend?</strong>
Using Elasticsearch as a Prometheus backend lets you store metrics with logs and traces under the same access controls and retention model.
Recent work on the Elasticsearch metrics engine also delivers strong performance for Prometheus-style workloads.
For the benchmark details, see the <a href="https://www.elastic.co/search-labs/blog/elasticsearch-columnar-metrics-engine-30x-faster-prometheus">Elasticsearch metrics performance post</a>.</p>
<p><strong>What PromQL features are supported in Elasticsearch today?</strong>
Elasticsearch supports common PromQL query patterns used by Grafana dashboards.
Advanced group modifiers, set operators, and <code>topk</code> are not yet supported.</p>
<p><strong>Can I limit Grafana queries to specific Elasticsearch indices?</strong>
Yes.
Add an index pattern after <code>/_prometheus/</code>, such as <code>/_prometheus/metrics-prod-*/api/v1/query_range</code>.
This pre-filters the Elasticsearch indices before PromQL evaluation and can reduce query work for large metrics deployments.</p>
<h2 id="prometheusapiavailability">Prometheus API availability</h2>
<p>The Prometheus-compatible API is available now on <a href="https://cloud.elastic.co/serverless-registration?onboarding_token=observability">Elasticsearch Serverless</a> with no additional configuration.</p>
<p>If you run into issues or have feedback, open an issue on the <a href="https://github.com/elastic/elasticsearch">Elasticsearch repository</a>.</p>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/query-prometheus-metrics-grafana-elasticsearch</link>
    <guid isPermaLink="false">query-prometheus-metrics-grafana-elasticsearch</guid>
    <category><![CDATA[Metrics]]></category>
    <category><![CDATA[Infrastructure Monitoring]]></category>
    <dc:creator><![CDATA[Felix Barnsteiner]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta5c8286ee6ba2a92/6a7f1a28ead8ec2d18baac4c/header.jpg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Don't leave metrics on the table: query them with the ES|QL TS command ]]></title>
    <description><![CDATA[Recalibrate your mental model for time series queries: learn why FROM can produce inaccurate results for metrics, how TS fixes that, and when to use each command.]]></description>
    <content:encoded><![CDATA[<p>If you use ES|QL for logs and traces, <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/from"><code>FROM</code></a> is probably second nature, but on metrics it can return numerically wrong answers.
A query like <code>FROM metrics-* | STATS SUM(request_count)</code> adds up cumulative counter values across every sample on every host.
The result grows without bound and isn't a rate, a count, or anything else useful.
<a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/ts"><code>TS</code></a> fixes that by grouping samples into time series first, then exposing functions like <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/time-series-aggregation-functions#esql-rate"><code>RATE</code></a>, <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/time-series-aggregation-functions#esql-avg_over_time"><code>AVG_OVER_TIME</code></a>, and <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/time-series-aggregation-functions#esql-last_over_time"><code>LAST_OVER_TIME</code></a> that operate per series.</p>
<p>For a high-level tour of metrics analytics across ES|QL and Discover, see <a href="https://www.elastic.co/observability-labs/blog/metrics-explore-analyze-with-esql-discover">Explore and Analyze Metrics with Ease in Elastic Observability</a>.
This post zooms in on the mechanics.</p>
<p>Here is the mental model in five bullets:</p>
<ul>
<li><code>FROM</code> treats every document as an independent row.
That is right for events, but metric aggregations often need the time series that each row belongs to.</li>
<li><code>TS</code> adds that time series context: it groups and aggregates data points by time series before any other aggregation runs, and enables functions like <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/time-series-aggregation-functions#esql-rate"><code>RATE</code></a>, <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/time-series-aggregation-functions#esql-avg_over_time"><code>AVG_OVER_TIME</code></a>, and <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/time-series-aggregation-functions#esql-last_over_time"><code>LAST_OVER_TIME</code></a>.</li>
<li>A <code>TS | STATS</code> query normally has two aggregation phases.
The inner phase reduces samples inside each time series; the outer phase groups and combines those per-series results.</li>
<li>The default inner aggregation is <code>LAST_OVER_TIME</code>, which is why <code>TS metrics | STATS AVG(cpu_usage)</code> and <code>FROM metrics | STATS AVG(cpu_usage)</code> can return different numbers.</li>
<li>Use <code>TS</code> to query a time series data stream (<a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/time-series-data-stream-tsds">TSDS</a>).
Use <code>FROM</code> for events and raw document inspection.</li>
</ul>
<h2 id="whatisatimeseriesreally">What is a time series, really?</h2>
<p>A time series is a sequence of <code>(timestamp, value)</code> data points identified by the metric name and a unique set of dimension values.</p>
<p>For example, <code>request_count</code> reported every 30 seconds by host <code>h1</code> in data center <code>dc1</code> is one time series.
The same metric on host <code>h2</code> in <code>dc1</code> is a different time series.</p>
<p>In a time series data stream, every metric document carries an internal <code>_tsid</code> field that uniquely identifies a time series.
Samples that share a <code>_tsid</code> belong to the same time series and are stored sequentially, sorted by timestamp.</p>
<p>That storage layout enables efficient per-series aggregations.
It also explains why <code>TS</code> only works on time series data streams.
Other index modes have no notion of a time series, so the per-series operations <code>TS</code> relies on have no such identifier to attach to.
<code>FROM</code> does not support those operations, which is what the next section is about.</p>
<h2 id="whyfromleavesmetricsonthetable">Why FROM leaves metrics on the table</h2>
<p>Consider a counter named <code>request_count</code> collected every 30 seconds from three hosts.</p>
<p>A counter is a cumulative metric: each sample is the running total since the process started reporting it.
For <code>request_count</code>, a value of <code>1,000</code> means "this time series has observed 1,000 requests so far", not "1,000 requests happened since the previous sample".
Counters reset to zero on process restart, so a sample of <code>4</code> right after <code>1,004</code> is a fresh count, not negative traffic.
The ES|QL <code>RATE</code> function computes the per-second change within a time series and handles resets without glitches.</p>
<p>You want to calculate the total request rate across all hosts, bucketed by 5 minutes.</p>
<p>If you are used to writing ES|QL over event data, you might start with this query:</p>
<pre><code>FROM metrics-*
| WHERE TRANGE(1h)
| STATS SUM(request_count) BY BUCKET(@timestamp, 5m)
</code></pre>
<p>The chart it produces looks plausible at first: a line that goes up over time.
But the number on the y-axis is the sum of every cumulative counter value reported in the bucket.
Each host contributes its own running total, repeatedly, once per sample.
Because the query uses <code>SUM</code> on those cumulative values, the result is not a rate, it is not the number of requests in the bucket, and it grows without bound even if the application stops receiving requests.</p>
<p><code>request_count</code> is a monotonically increasing counter, so its raw values represent "how many requests have ever happened on this host", not how many happened in the bucket.
The right computation is "how much did this counter increase per second on each host, then sum across hosts."
<code>FROM</code> cannot express that operation directly.
It can group rows by fields, but it has no built-in notion of "the same time series over time" and no way to ask for the change of a counter within each time series.
It also cannot use sliding-window time series functions such as <code>RATE(request_count, 5m)</code>, which we will come back to below.</p>
<p><code>TS</code> was introduced for this purpose, providing a succinct syntax to express time series aggregations:</p>
<pre><code>TS metrics-*
| WHERE TRANGE(1h)
| STATS SUM(RATE(request_count)) BY TBUCKET(5m)
</code></pre>
<p><code>RATE(request_count)</code> runs per time series and produces a per-second rate that handles counter resets correctly.
<code>SUM</code> then adds those rates across hosts.</p>
<h2 id="twoaggregationphasesinnerandouter">Two aggregation phases: inner and outer</h2>
<p>Every <code>TS | STATS</code> query has two distinct aggregation phases.</p>
<p>Let's make that concrete with a query that calculates the request rate per data center:</p>
<pre><code>TS metrics-*
| WHERE TRANGE(1h)
| STATS SUM(RATE(request_count)) BY datacenter, TBUCKET(5m)
</code></pre>
<p>The diagram below shows how <code>TS</code> evaluates this query.
It first reduces samples inside each time series, then groups and combines those per-series values into one result per <code>datacenter</code> and time bucket.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4a8d11ebfa5d7f90/6a859a659bf994ed1609faf0/aggregation-phases.png" alt="Inner and outer aggregation phases of a TS|STATS query" /></p>
<p>The phases are:</p>
<p><strong>Inner (within a time series).</strong>
Runs separately for each time series.
It collapses many <code>(timestamp, value)</code> data points within a bucket into a single value per time series per bucket by applying the inner aggregation function, such as <code>RATE</code> in the example above.
Functions: <code>RATE</code>, <code>AVG_OVER_TIME</code>, <code>MAX_OVER_TIME</code>, <code>LAST_OVER_TIME</code>, <code>STDDEV_OVER_TIME</code>, and so on.
The full list is on the <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/time-series-aggregation-functions">time series aggregation functions</a> page.</p>
<p><strong>Outer (across time series, the "grouping" phase).</strong>
Combines the per-series values into a single value per group per bucket.
Functions: <code>SUM</code>, <code>AVG</code>, <code>MAX</code>, <code>MIN</code>, percentiles, and the rest of the <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/aggregation-functions">regular ES|QL aggregates</a>.</p>
<p>In <code>SUM(RATE(request_count)) BY datacenter, TBUCKET(5m)</code>:</p>
<ul>
<li><code>RATE(request_count)</code> is the inner aggregation.
It runs per time series.</li>
<li><code>SUM(...)</code> is the outer aggregation.
It combines time series within the same <code>datacenter</code> and bucket.</li>
<li><a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/grouping-functions#esql-tbucket"><code>TBUCKET(5m)</code></a> defines the bucket boundaries (equivalent to <code>BUCKET(@timestamp, 5m)</code>).</li>
</ul>
<p>The outer aggregation is optional.
If you only need the per-time-series result, use the time series aggregation function directly:</p>
<pre><code>TS metrics-*
| WHERE TRANGE(1h)
| STATS request_rate = RATE(request_count) BY TBUCKET(5m)
</code></pre>
<p>That query keeps the per-series rate for each bucket instead of wrapping it in <code>SUM</code>, <code>AVG</code>, or another aggregate across time series.</p>
<h2 id="thedefaultinneraggregationlast_over_time">The default inner aggregation: LAST_OVER_TIME</h2>
<p><code>TS</code> has to reduce raw samples inside each time series before it can run the outer aggregation.
That means every metric field in a <code>TS | STATS</code> aggregation needs an inner aggregation, even when the query does not spell one out.</p>
<p>Consider a metric named <code>cpu_usage</code>.
It is a gauge: a metric that captures a value at a point in time and can move up and down freely.
A sample of <code>0.42</code> means "this host is at 42% CPU at this time".
For a gauge, the natural "value in this bucket" is the most recent sample.</p>
<p>That is what ES|QL fills in for you.
If you write <code>TS metrics | STATS AVG(cpu_usage) BY host.name, TBUCKET(5m)</code>, the implicit inner aggregation is <code>LAST_OVER_TIME(cpu_usage)</code> and the query is equivalent to:</p>
<pre><code>TS metrics
| WHERE TRANGE(1h)
| STATS AVG(LAST_OVER_TIME(cpu_usage)) BY host.name, TBUCKET(5m)
</code></pre>
<p>For each time series, <code>LAST_OVER_TIME</code> picks the latest sample in the bucket.
Then <code>AVG</code> averages across time series.</p>
<p>It is also why the same-looking query against <code>FROM</code> and <code>TS</code> can return different numbers.
<code>FROM</code> averages every individual document.
<code>TS</code> averages one value per time series per bucket.
If your hosts publish at slightly different rates, those averages diverge.
For example, in a five-minute bucket, a host that publishes every second contributes 300 documents while a host that publishes every two minutes contributes only two or three.
With <code>FROM | STATS AVG(cpu_usage)</code>, the chatty host dominates the average.
With <code>TS</code>, each time series is reduced to one bucket value first, so the outer average gives each host one value to contribute.</p>
<p>If you want the average value during the bucket instead of the latest value, make the inner aggregation explicit:</p>
<pre><code>TS metrics-*
| WHERE TRANGE(1h)
| STATS AVG(AVG_OVER_TIME(cpu_usage)) BY host.name, TBUCKET(5m)
</code></pre>
<p><code>AVG_OVER_TIME</code> averages all CPU utilization samples within each time series.
The outer <code>AVG</code> then averages those per-series values across matching hosts.
That makes the result sample-weighted within each time series, then equally weighted across time series.
Use this when you care about how the value behaved during the bucket, not just where it ended up.</p>
<p>The same rule applies to peaks and troughs.
For a peak CPU chart, use <code>MAX(MAX_OVER_TIME(cpu_usage))</code>, not just <code>MAX(cpu_usage)</code>.
The inner <code>MAX_OVER_TIME</code> finds the peak within each time series; the outer <code>MAX</code> finds the peak across matching time series.</p>
<p>Counters work the other way around.
Their sample value is a running total, so the latest sample on its own is rarely meaningful.
For a counter, the inner aggregation you almost always want is <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/time-series-aggregation-functions#esql-rate"><code>RATE</code></a> for a per-second rate, or <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/time-series-aggregation-functions"><code>INCREASE</code></a> for the total change in the bucket.
Falling back on the default <code>LAST_OVER_TIME</code> gives you the most recent cumulative value, which is the trap the FROM query in the previous section walked into.</p>
<p>Pick the inner function deliberately.
The outer function is the easy part.</p>
<h2 id="whentousetswhentousefrom">When to use TS, when to use FROM</h2>
<p>A practical rule of thumb:</p>
<ul>
<li>Use <code>TS</code> for metric aggregations against a <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/time-series-data-stream-tsds">time series data stream</a>.
It is the source command designed for that data, and it applies per-series semantics by default.</li>
<li>Use <code>FROM</code> for events: logs, traces, audit records, transactions.
Each row is independent.
There is no time series context.</li>
</ul>
<p><code>FROM</code> still works on TSDS indices and is occasionally useful, for example when you want to inspect raw metric documents without per-series grouping.
For dashboards, alerts, and any kind of charting, <code>TS</code> is the right default.</p>
<p>If you first need to discover which metrics or time series exist in the data, use <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/metrics-info"><code>METRICS_INFO</code></a> or <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/ts-info"><code>TS_INFO</code></a> after <code>TS</code> and before <code>STATS</code>.
See <a href="https://www.elastic.co/search-labs/blog/esql-metrics-info-ts-info-time-series-catalog">ES|QL METRICS_INFO and TS_INFO: Catalog your time series data</a> for a deeper walkthrough.</p>
<h2 id="postprocesstsresultswithesql">Post-process TS results with ES|QL</h2>
<p>The first <code>STATS</code> command is the boundary between time series processing and regular ES|QL processing.
Before that first <code>STATS</code>, <code>TS</code> needs to keep the data grouped by <code>_tsid</code>, so commands that change row order or shape are not allowed.
After that first <code>STATS</code>, the output is a regular ES|QL table.
You can sort it, limit it, join lookup data, enrich it, or compute derived columns.</p>
<p>For example, this query calculates average CPU per host and bucket, finds the maximum bucketed average for each host, and returns the ratio:</p>
<pre><code>TS metrics-*
| WHERE TRANGE(1h)
| STATS avg_cpu = AVG(AVG_OVER_TIME(cpu_usage)) BY host.name, time_bucket = TBUCKET(5m)
| INLINE STATS max_avg_cpu = MAX(avg_cpu) BY host.name
| EVAL cpu_ratio = avg_cpu / max_avg_cpu
| KEEP host.name, time_bucket, cpu_ratio
| SORT host.name, time_bucket DESC
</code></pre>
<h2 id="slidingwindowsfortheinneraggregation">Sliding windows for the inner aggregation</h2>
<p>Time series aggregation functions accept a second argument: the window size for the inner phase.</p>
<pre><code>TS metrics-*
| WHERE TRANGE(1h)
| STATS AVG(RATE(app.requests, 5m)) BY TBUCKET(1m)
</code></pre>
<p>This computes the rate over a 5-minute sliding window, but reports a value every minute.
It is useful when you want a smoother chart at fine bucket sizes.</p>
<p>The window is the ES|QL counterpart to a PromQL <a href="https://prometheus.io/docs/prometheus/latest/querying/basics/#range-vector-selectors">range vector selector</a>: <code>RATE(app.requests, 5m)</code> serves the same purpose as <code>rate(app_requests[5m])</code>.</p>
<h2 id="gotchasworthknowing">Gotchas worth knowing</h2>
<p>A few things in <code>TS</code> can seem surprising, especially when coming from the events-based <code>FROM</code> mental model.
None of these are bugs; most are direct consequences of the per-series model.
Here is what to watch for.</p>
<p><strong><code>COUNT(*)</code> is rejected.</strong>
Say you want to know how many samples were collected per service in each bucket.
The instinct from <code>FROM</code> is <code>COUNT(*)</code>, but <code>TS</code> rejects it: there is no plain "row" once data is grouped by time series, so a row count has no defined meaning.
Pick what you actually want to count:</p>
<ul>
<li>Number of samples per service: <code>STATS samples = SUM(COUNT_OVER_TIME(cpu_usage)) BY service.name, TBUCKET(5m)</code>.
The inner <code>COUNT_OVER_TIME</code> counts samples per time series; the outer <code>SUM</code> adds them across the time series in the group.</li>
<li>Number of distinct hosts reporting per service: <code>STATS hosts = COUNT_DISTINCT(host.name) BY service.name, TBUCKET(5m)</code>.
This counts unique label values across time series.</li>
</ul>
<p><strong>You cannot sort, limit, lookup join, or enrich before <code>STATS</code>.</strong>
<code>TS metrics | SORT @timestamp | STATS ...</code> will fail.
The grouping by <code>_tsid</code> must happen first, before anything else can run.
Filter with <code>WHERE</code> if you need to narrow the scope.
After the first <code>STATS</code>, the output is regular ES|QL and you can pipe it through any command, as shown in the previous section.</p>
<p><strong>Gauge vs counter mapping.</strong>
Time series functions are sensitive to the metric type set in the field mapping.
<code>RATE</code> only works on counters; <code>*_OVER_TIME</code> functions are intended for gauges.
If you build TSDS mappings by hand, pay special attention to this part.</p>
<p>This can be a source of friction for Prometheus users.
Prometheus metric type metadata is not always available in the data Elasticsearch receives, so the metric type may have to be inferred from naming conventions (<code>_total</code> for counters, and so on).
Those heuristics are imperfect, and a misclassified metric is rejected by the function that should accept it.
The deeper mechanics, including how Prometheus Remote Write maps metric types into TSDS, are covered in <a href="https://www.elastic.co/observability-labs/blog/prometheus-remote-write-elasticsearch-architecture">How Prometheus Remote Write Ingestion Works in Elasticsearch</a>.</p>
<p>Explicit converter functions (gauge-to-counter and counter-to-gauge) are on the roadmap to make these cases easier to recover from at query time.</p>
<p><strong>Kibana charts go empty when you zoom in too far.</strong>
In Kibana, <code>TBUCKET</code> adapts to the date picker, so zooming in shrinks the bucket size.
When the bucket size drops below the data's collection interval, every other bucket has no sample, <code>RATE</code> and the rest return null, and the chart silently goes blank.
Elastic is evaluating mitigations such as a runtime warning when the bucket size is too small, a configurable minimum bucket size, or automatic widening of the window or bucket size.</p>
<h2 id="wrapup">Wrap up</h2>
<p>For metric queries, start with <code>TS</code> unless you specifically need raw documents.
Then choose the inner aggregation based on what the value should mean inside each time series: <code>RATE</code> for counters, <code>LAST_OVER_TIME</code> for current gauge values, and explicit <code>*_OVER_TIME</code> functions for peaks, averages, minimum values, or distributions.</p>
<p>Once the per-series value is right, the outer aggregation is the familiar part: group and reduce those time series into the chart, alert, or table you need.</p>
<p>For the full reference, see the <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/ts"><code>TS</code> command docs</a> and the list of <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/time-series-aggregation-functions">time series aggregation functions</a>.</p>
<h2 id="frequentlyaskedquestions">Frequently asked questions</h2>
<p><strong>Why does ES|QL <code>FROM</code> return wrong numbers on counter metrics?</strong>
<code>FROM</code> treats each metric document as an independent row, so <code>SUM(request_count)</code> adds up cumulative counter values across hosts and time.
The result grows without bound and is not a rate or a count of requests.
Use <code>TS</code> with <code>RATE</code> to compute a per-second rate inside each time series, then <code>SUM</code> across hosts.</p>
<p><strong>What is the difference between <code>TS</code> and <code>FROM</code> in ES|QL?</strong>
<code>TS</code> is the source command for time series data streams (TSDS).
It groups samples by time series before aggregating, which enables functions like <code>RATE</code>, <code>AVG_OVER_TIME</code>, and <code>LAST_OVER_TIME</code>.
<code>FROM</code> reads documents as independent rows and has no per-series semantics.
Use <code>TS</code> for querying time series data streams, <code>FROM</code> for events and raw document inspection.</p>
<p><strong>Why do <code>TS metrics | STATS AVG(cpu_usage)</code> and <code>FROM metrics | STATS AVG(cpu_usage)</code> return different averages?</strong>
<code>TS</code> applies an implicit inner aggregation — <code>LAST_OVER_TIME</code> by default — so each time series contributes one value per bucket.
<code>FROM</code> averages every individual document, so a host that publishes 300 samples in a bucket dominates a host that publishes two.
The <code>TS</code> answer weights each time series equally; the <code>FROM</code> answer weights each document equally.</p>
<p><strong>How do I compute a per-second rate from a counter in ES|QL?</strong>
Use <code>TS</code> with the <code>RATE</code> function: <code>TS metrics-* | STATS SUM(RATE(request_count)) BY TBUCKET(5m), host.name</code>.
<code>RATE</code> runs per time series, computes the per-second change, and handles counter resets correctly.
The outer <code>SUM</code> then combines rates across hosts.</p>
<p><strong>When should I add a timestamp filter to an ES|QL metrics query?</strong>
In Kibana apps that run ES|QL with the global date picker, such as Discover and dashboards, you do not need to add this filter yourself.
Kibana applies the selected time range automatically.
Outside Kibana, or in Kibana Dev Tools, always add an explicit timestamp filter so the query only scans the time range you intend:</p>
<pre><code>WHERE @timestamp &gt; NOW() - 1 hour AND @timestamp &lt;= NOW()
</code></pre>
<p>or the equivalent shorthand:</p>
<pre><code>WHERE TRANGE(1h)
</code></pre>
<p><code>TRANGE(1h)</code> is the preferred shorthand for recent time windows.
It is equivalent to filtering <code>@timestamp</code> to the last hour, ending at <code>NOW()</code>.</p>
<p><strong>What are the two aggregation phases of a <code>TS | STATS</code> query?</strong>
The inner phase runs per time series and collapses many <code>(timestamp, value)</code> samples into one value per series per bucket using a time series function (<code>RATE</code>, <code>AVG_OVER_TIME</code>, <code>MAX_OVER_TIME</code>, <code>LAST_OVER_TIME</code>, etc.).
The outer phase runs across time series and combines those per-series values using a regular ES|QL aggregate (<code>SUM</code>, <code>AVG</code>, <code>MAX</code>, percentiles).
Pick the inner function for correctness; the outer function is the familiar part.</p>
<p><strong>Why does my Kibana metrics chart go blank when I zoom in?</strong>
<code>TBUCKET</code> adapts to the date picker, so zooming in shrinks the bucket.
When the bucket size drops below the data's collection interval, some buckets contain no sample, <code>RATE</code> returns null, and the chart silently goes empty.
Widen the time range or set a longer explicit window in the inner aggregation, e.g., <code>RATE(request_count, 5m)</code>.</p>
<p><strong>Can I use <code>SORT</code>, <code>LIMIT</code>, or <code>LOOKUP JOIN</code> before <code>STATS</code> in a <code>TS</code> query?</strong>
No.
<code>TS</code> must keep data grouped by <code>_tsid</code> until the first <code>STATS</code>, so commands that change row order or shape are rejected before that point.
Use <code>WHERE</code> to filter, then <code>STATS</code>, and apply <code>SORT</code>, <code>LIMIT</code>, joins, or enrichment on the regular ES|QL table that comes out of <code>STATS</code>.</p>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/esql-ts-command-querying-metrics</link>
    <guid isPermaLink="false">esql-ts-command-querying-metrics</guid>
    <category><![CDATA[Metrics]]></category>
    <dc:creator><![CDATA[Felix Barnsteiner]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf73255f80b537763/6a859a68f5f1a04da42ebf30/header.jpg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 28 May 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Bringing Fire to Elasticsearch: Adding Native Prometheus API Support]]></title>
    <description><![CDATA[Query Elasticsearch directly from Prometheus-compatible clients via native PromQL, discovery, and metadata endpoints. Send data to Elasticsearch with Prometheus Remote Write.]]></description>
    <content:encoded><![CDATA[<p>Point any Prometheus-compatible client at Elasticsearch and run PromQL directly against your existing metrics.
Elasticsearch is adding native Prometheus query, discovery, and metadata endpoints as a tech preview that work over metrics ingested through Prometheus Remote Write, <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/tsds-ingest-otlp">OpenTelemetry</a>, or the Bulk API.
The API runs on top of Elasticsearch's <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/time-series-data-stream-tsds">time series data streams (TSDS)</a>, so there's no separate Prometheus-specific storage layer to operate.</p>
<p>This post explains how the query, discovery, and metadata endpoints build on the earlier ingest and query work to form that API surface.
Companion posts go deeper on individual pieces:</p>
<ul>
<li><a href="https://www.elastic.co/observability-labs/blog/elasticsearch-supports-promql">Native PromQL support in ES|QL</a> covers how PromQL queries are translated into ES|QL execution plans.</li>
<li><a href="https://www.elastic.co/observability-labs/blog/prometheus-remote-write-elasticsearch">Ship Prometheus Metrics to Elasticsearch with Remote Write</a> covers ingestion setup.</li>
<li><a href="https://www.elastic.co/observability-labs/blog/prometheus-remote-write-elasticsearch-architecture">How Prometheus Remote Write Ingestion Works in Elasticsearch</a> covers the remote write internals.</li>
</ul>
<p>This is still a work in progress.
The sections below call out what is supported today and which parts are still evolving.</p>
<h2 id="theapisurface">The API surface</h2>
<p>Today, the Prometheus-compatible API surface falls into three groups.</p>
<h3 id="queryendpoints">Query endpoints</h3>
<p>The query endpoints let Prometheus-compatible clients evaluate PromQL expressions:</p>
<ul>
<li><code>GET /_prometheus/api/v1/query_range</code> evaluates a PromQL expression over a time window (matrix results).</li>
<li><code>GET /_prometheus/api/v1/query</code> evaluates at a single point in time (vector results).
Currently implemented as a short range query that returns the last sample.</li>
</ul>
<p>Only GET is supported for query endpoints today.
Some clients default to POST, so you may need to configure them to use GET.
The Prometheus POST convention uses <code>application/x-www-form-urlencoded</code> bodies, which Elasticsearch's HTTP layer rejects as a CSRF safeguard before the request ever reaches the handler.</p>
<p>For the full PromQL coverage status, see the <a href="https://www.elastic.co/observability-labs/blog/elasticsearch-supports-promql">companion post on PromQL in ES|QL</a>.</p>
<h3 id="metadataendpoints">Metadata endpoints</h3>
<p>The metadata endpoints serve the discovery information that clients need for autocomplete, variable dropdowns, and metric browsing.</p>
<p>The series, labels, and label values endpoints all accept <code>match[]</code> selectors and a time range (<code>start</code>/<code>end</code>).
The <code>match[]</code> parameter takes a Prometheus series selector like <code>http_requests_total{job="api"}</code> and restricts the response to time series that match.
This keeps responses fast and relevant on clusters with large numbers of metrics.
For example:</p>
<pre><code>GET /_prometheus/api/v1/series?match[]=http_requests_total{job="api"}
GET /_prometheus/api/v1/labels?match[]=http_requests_total
GET /_prometheus/api/v1/label/instance/values?match[]=http_requests_total{job="api"}
</code></pre>
<p>The first returns all series for <code>http_requests_total</code> where <code>job="api"</code>, with their full label sets.
The second returns only the label names that exist on <code>http_requests_total</code> series.
The third returns only the <code>instance</code> values that appear on matching series.</p>
<p><code>GET /_prometheus/api/v1/metadata</code> is different: it returns type and unit for each metric, optionally filtered by name via a <code>metric</code> parameter.</p>
<pre><code>GET /_prometheus/api/v1/metadata?metric=http_requests_total
</code></pre>
<p>It does not accept <code>match[]</code> selectors or a time range.
In Prometheus, metadata is collected from active scrape targets (the <code>HELP</code>, <code>TYPE</code>, and <code>UNIT</code> lines they expose), so the response does not involve a data scan.
Elasticsearch does not have a dedicated metadata store like that, so the current implementation discovers metric metadata by visiting time series data from the last 24 hours.
This keeps the query fast without requiring a full index scan.
That 24-hour lookback is fixed today: the Prometheus metadata API does not expose <code>start</code> or <code>end</code> parameters that Elasticsearch could use to make it user-adjustable.</p>
<p>How the metadata endpoints work under the hood, including the <code>TS_INFO</code> and <code>METRICS_INFO</code> commands that power them, is covered <a href="https://www.elastic.co/observability-labs/blog/elasticsearch-native-prometheus-api#ts_info-and-metrics_info">below</a>.</p>
<h3 id="indexprefiltering">Index pre-filtering</h3>
<p>All query and metadata endpoints accept an optional <code>{index}</code> path segment after <code>/_prometheus/</code>:</p>
<pre><code>GET /_prometheus/metrics-prod-*/api/v1/query_range?query=up&amp;start=...&amp;end=...
</code></pre>
<p>This restricts which Elasticsearch indices the query runs against before any expression evaluation begins.
On clusters with many data streams across teams or environments, this avoids scanning unrelated indices and can significantly reduce query latency.
You can configure separate data sources per index pattern to give teams scoped access to their own metrics.</p>
<h3 id="anoteaboutremotewrite">A note about Remote Write</h3>
<p>For ingestion, Elasticsearch also exposes the standard Prometheus Remote Write endpoint:</p>
<ul>
<li><code>POST /_prometheus/api/v1/write</code> ingests time series via the Prometheus Remote Write v1 protocol.
v2 is not yet supported.</li>
</ul>
<p>Remote Write writes into Elasticsearch's existing time series data streams (TSDS), not a separate Prometheus-specific storage layer.
Prometheus labels become TSDS dimensions, and metric names become fields in the index mapping.
The <a href="https://www.elastic.co/observability-labs/blog/prometheus-remote-write-elasticsearch-architecture">remote write architecture post</a> covers the full mapping in detail, including how metric types are inferred and how labels are stored with a <code>labels.</code> prefix.</p>
<h3 id="howitworks">How it works</h3>
<p>Under the hood, all endpoints work the same way: parse the incoming HTTP parameters, build an ES|QL query plan, execute it against time series data streams, and convert the columnar result back into the JSON format Prometheus clients expect.</p>
<h2 id="ts_infoandmetrics_info">TS_INFO and METRICS_INFO</h2>
<p>The metadata endpoints need to answer questions like "what labels exist?" or "what metric types are defined?" across potentially millions of time series, without scanning every data point.</p>
<p>Internally, the Prometheus metadata endpoints answer those questions by building ES|QL plans around two new processing commands: <code>METRICS_INFO</code> and <code>TS_INFO</code>.
You do not need to use these commands directly to use the Prometheus API, but they are the core execution primitives behind the metadata responses.
Both work by visiting only one document per time series to extract its metadata, rather than scanning all samples.
This means their cost scales with the number of distinct time series, not the number of data points.</p>
<p><code>METRICS_INFO</code> returns one row per distinct metric with its name, type, unit, and associated dimension fields.
<code>TS_INFO</code> is more granular: one row per (metric, time series) combination, including the actual dimension values as a JSON object.</p>
<pre><code>TS metrics-*
| METRICS_INFO
| SORT metric_name
</code></pre>
<p>A dedicated blog post on <code>TS_INFO</code> and <code>METRICS_INFO</code> is coming soon, covering the two-phase execution model, how they scale, and how to use them directly in ES|QL queries beyond the Prometheus API.</p>
<h3 id="howthemetadataendpointsusethem">How the metadata endpoints use them</h3>
<p>Each metadata endpoint constructs an ES|QL plan with one of these commands at its core.</p>
<p><code>/api/v1/labels</code> and <code>/api/v1/series</code> use <code>TS_INFO</code>, since they need per-time-series detail (which labels exist, which dimension values identify each series).
<code>/api/v1/metadata</code> and <code>/api/v1/label/__name__/values</code> use <code>METRICS_INFO</code>, since they only need per-metric information (metric names, types, units).</p>
<p><code>/api/v1/label/{name}/values</code> for regular labels (anything other than <code>__name__</code>) does not use either command.
Regular labels like <code>job</code> or <code>instance</code> are actual dimension fields in the index, so the endpoint can query them directly with a group-by aggregation.
When <code>match[]</code> selectors are provided, they are translated into a <code>WHERE</code> clause that filters the time series before the aggregation runs.</p>
<p>The <code>__name__</code> label needs a different strategy because it is not always present as a dimension field.
Prometheus Remote Write does store <code>labels.__name__</code>, but metrics ingested through other paths (OpenTelemetry, the bulk API) do not have it.
The metric name is encoded in the field name itself (e.g., <code>metrics.http_requests_total</code>).
You could look at the index mappings to enumerate field names, but mappings alone do not tell you which metric has which dimensions, and they cannot be filtered by label values from a <code>match[]</code> selector.
<code>METRICS_INFO</code> can do both: it enumerates metric names across indices while respecting upstream <code>WHERE</code> filters.</p>
<p>In all cases, the API layer handles the translation back to Prometheus conventions: stripping the <code>labels.</code> and <code>metrics.</code> storage prefixes and synthesizing <code>__name__</code> for non-Prometheus metrics that lack it.</p>
<h2 id="inconclusion">In conclusion</h2>
<p>The result: any Prometheus-compatible client can query and explore Elasticsearch metrics through endpoints it already understands.
Remote Write metrics, OpenTelemetry metrics, and metrics indexed through other paths all show up through the same API, backed by the same TSDS indices.</p>
<p>All the Prometheus APIs mentioned here are available as tech preview in Elasticsearch Serverless today.
For self-managed clusters and Elastic Cloud Hosted deployments, they will arrive as tech preview in Elasticsearch 9.4,
with the exception of the <code>GET /_prometheus/api/v1/metadata</code> API which will arrive in 9.5.
To try it locally, use <a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/local-development-installation-quickstart">start-local</a>.</p>
<h2 id="frequentlyaskedquestions">Frequently asked questions</h2>
<p><strong>Do the Prometheus-compatible query and metadata APIs only work with data that is ingested via Remote Write?</strong>
No.
The Prometheus-compatible API runs on top of Elasticsearch any time series data stream (TSDS).
The same indices that hold metrics from Prometheus Remote Write, OpenTelemetry, or the Bulk API are queried directly through PromQL, with no extra storage layer to operate.</p>
<p><strong>Why do my Prometheus clients fail when calling the query endpoints with POST?</strong>
Today only GET is supported on the Prometheus query endpoints.
Some clients default to POST with <code>application/x-www-form-urlencoded</code>, which Elasticsearch's HTTP layer rejects as a CSRF safeguard before the request reaches the handler.
Configure the client to use GET.</p>
<p><strong>Can I scope a Prometheus query to a subset of indices in Elasticsearch?</strong>
Yes.
Every query and metadata endpoint accepts an optional <code>{index}</code> segment, like <code>/_prometheus/metrics-prod-*/api/v1/query_range</code>.
This restricts the query to the matching indices before evaluation, which avoids scanning unrelated data.</p>
<p><strong>How does the native Prometheus API compare to running a dedicated Prometheus server?</strong>
Elasticsearch consolidates Prometheus Remote Write, OpenTelemetry, and other ingestion paths into one TSDS-backed store, queryable through PromQL or ES|QL.
You keep Prometheus client compatibility while reusing existing Elasticsearch capabilities like long-term storage, ILM, and correlation with logs and traces stored in the same cluster.</p>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/elasticsearch-native-prometheus-api</link>
    <guid isPermaLink="false">elasticsearch-native-prometheus-api</guid>
    <category><![CDATA[Metrics]]></category>
    <category><![CDATA[OpenTelemetry]]></category>
    <dc:creator><![CDATA[Felix Barnsteiner]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf6ee85474d7681ec/6a859a56501a856126fba888/header.jpg" length="0" type="image/jpeg"/>
    <pubDate>Tue, 26 May 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[30x faster than Prometheus: how we rebuilt Elasticsearch as a leading columnar metrics datastore]]></title>
    <description><![CDATA[Elasticsearch now stores OTel metrics at 3.75 bytes per data point and queries them up to 30x faster than Prometheus. Here's how we rebuilt TSDS and ES|QL.]]></description>
    <content:encoded><![CDATA[<p>Elasticsearch now stores OTel metrics at <strong>3.75 bytes per data point</strong> — down from 25 bytes a year ago — and queries them up to <strong>30x</strong> faster and with up to <strong>2.5x</strong> better storage efficiency, compared to <strong>Prometheus</strong>, <strong>Mimir</strong> and <strong>ClickHouse</strong>. These gains came from rebuilding <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/time-series-data-stream-tsds">TSDS</a> storage and the ES|QL compute engine into a <strong>fully columnar metrics engine</strong>, with native OTel ingestion added as part of the effort — all while keeping Elasticsearch's ability to store and query logs, traces, and any other data alongside metrics.</p>
<p>Elasticsearch has supported storing metrics in time-series data streams (<a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/time-series-data-stream-tsds">TSDS</a>) since <strong>version 8.7</strong>. This offering mainly focused on storage gains as explained in an earlier <a href="https://www.elastic.co/search-labs/blog/time-series-data-elasticsearch-storage-wins">blog post</a>. Still, performance was not on par with specialized systems for storing and querying metrics, in terms of storage efficiency, indexing throughput and query latency.</p>
<p>In the past year, we revisited the storage layer, optimized ingestion for OTel metrics and extended the ES|QL compute engine with vectorized processing for time series data. These efforts led to substantial performance wins across the board, compared to earlier versions of TSDS:</p>
<ol>
<li>Up to <strong>6.6x</strong> improvement in storage efficiency, reaching 3.75 bytes per data point in OTel metrics</li>
<li>Up to <strong>50%</strong> improvement in indexing throughput for OTel data</li>
<li>Up to <strong>160x</strong> improvement in query latency, including blazing fast counter rate evaluation and window support in time series aggregations</li>
</ol>
<p>Elasticsearch has thus become a <strong>leading columnar metrics engine</strong>, matching or exceeding the competition (like <strong>Prometheus</strong>, <strong>Mimir</strong>, and <strong>ClickHouse</strong>) in indexing throughput and exceeding it by up to <strong>2.5x</strong> in storage efficiency and <strong>30x</strong> in query performance. All while maintaining the ability to store logs and other data and fully use the rich querying capabilities of ES|QL (e.g. <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/inlinestats-by">inline stats</a>, <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/lookup-join">lookup join</a>) — which other PromQL-based systems lack. Elasticsearch can thus serve as a unified storage and query engine for all user data, with no compromises for metrics and observability applications.</p>
<h2 id="howtsdsisorganized">How TSDS is organized</h2>
<p>TSDS has the following properties that help improve the performance of time-series codecs and produce correct results when aggregating data points per time series:</p>
<ul>
<li>The <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/time-series-data-stream-tsds#time-series-metric">metric</a> name and the <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/time-series-data-stream-tsds#time-series-dimension">dimension</a> names and values are used to calculate the <code>_tsid</code>, a unique identifier per time series.</li>
<li>TSDS get sorted by <code>[_tsid ascending, timestamp descending]</code> order. Each time series is thus stored in sequence on disk, with newer data points appearing first. Since the <code>_tsid</code> is calculated over dimension values, the latter are also clustered on disk.</li>
<li>Shard routing is based on <code>_tsid</code>, with each <code>_tsid</code> value appearing in one shard only.</li>
<li>Backing indices are <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/time-bound-tsds">time-bound</a>, with no overlap over time between them.</li>
</ul>
<p>The rest of this post explains how we use these properties to improve storage, indexing, and query performance.</p>
<h2 id="storageoptimizations">Storage optimizations</h2>
<p>TSDS <a href="https://www.elastic.co/search-labs/blog/time-series-data-elasticsearch-storage-wins">already</a> achieved a very competitive storage footprint, reaching <strong>0.9 bytes per data point</strong>, when it is possible to combine many metrics in a single doc, sharing the same dimension values. However, when most data points have a unique set of dimensions (which is typical for OTel or Prometheus metrics), docs end up containing a single data point. In this setup, storage required 25 bytes per data point, with dedicated metrics stores requiring less than 10 bytes per data point.</p>
<p>To further reduce the storage footprint, we applied a series of optimizations over the past year:</p>
<h3 id="replaceinvertedindicesandbkdtreeswithdocvalueskippers">Replace inverted indices and BKD trees with doc value skippers</h3>
<p>Elasticsearch creates inverted indices (for text values) or BKD trees (for numeric values) by default for all non-metric fields, i.e. for <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams#backing-indices">@timestamp</a> and dimensions. These indices improve performance for queries including filters on these fields, but have significant impact to storage — effectively doubling the footprint for each field. More so, they are also processed during <a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/merge">segment merging</a>, increasing the cpu, memory and storage overhead and slowing down the system — especially in high ingest throughput scenarios, as is often the case with metrics.</p>
<p>Lucene has been extended with <a href="https://lucene.apache.org/core/10_1_0/core/org/apache/lucene/index/DocValuesSkipper.html">doc value skippers</a>, a form of hierarchical sparse indices that store the minimum and maximum value of blocks of documents. Range queries can check these min and max values and ignore blocks that don't fall into the requested range. Skippers work particularly well on sorted fields. Since TSDS are sorted by <code>[_tsid, timestamp desc]</code>, dimension values get also clustered on disk. It's therefore possible to replace indices on <code>@timestamp</code> and dimension fields with doc value skippers that <strong>amplify the columnar layout</strong> — each field stored in its own files, with no duplicate tracking of each doc for indexing purposes.</p>
<p>Doc value skippers have negligible storage overhead — replacing indices with them led to a reduction of <strong>10 bytes</strong> out of the initial 25 bytes per data point in OTel. Moreover, they work very well in practice when queries include filters on time ranges or dimension values (including prefixes and regex) — there was no noticeable regression in query performance in our benchmarks when they replaced separate indices. Doc value skippers are enabled for TSDS by default since <strong>version 9.3</strong>.</p>
<h3 id="enablesyntheticids">Enable synthetic ids</h3>
<p>The <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-id-field"><code>_id</code></a> metadata field was another big contributor to the storage footprint. TSDS has already been extended to trim the doc values once they were no longer needed for replication, but the inverted index was kept around to efficiently support the id-based APIs (<a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-get">Get</a>, <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-delete">Delete</a>, <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-update">Update</a>).</p>
<p>The id value for TSDS is synthesized by combining the <code>_tsid</code> and <code>@timestamp</code> values that uniquely identify each data point. Since these fields are configured with doc value skippers, it's possible to replace the inverted index on <code>_id</code> with (a) retrieval of the <code>_tsid</code> and <code>@timestamp</code> value from the <code>_id</code> value, and (b) checks for matches using doc value skippers respectively. Care has to be taken to avoid expensive checks for duplicate ids during metric ingestion, with segment-level bloom-filters keeping the overhead at bay.</p>
<p>Supporting synthetic ids in metrics is a first for Elasticsearch. It led to a reduction of <strong>5 bytes</strong> out of the initial 25 bytes per data point for OTel metrics, with no loss of functionality. Synthetic ids are enabled for TSDS by default in <strong>version 9.4</strong>. We plan to extend their uses in logs and other applications after further evaluation.</p>
<h3 id="trimsequencenumbers">Trim sequence numbers</h3>
<p>Sequence numbers are used as part of replication, but also to provide strong consistency semantics on doc modification operations through <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/optimistic-concurrency-control">Optimistic Concurrency Control</a> (OCC). While such semantics are applicable to certain scenarios, they don't fit in metrics where concurrent updates are very rare, with no practical need for guarding against concurrent operations on data points with matching ids. We therefore decided to <a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/index-modules#index-disable-sequence-numbers">disable the use of sequence numbers</a> in all APIs, along with OCC support, for TSDS, in <strong>version 9.4</strong>. This leads to a substantial storage reduction of <strong>4 bytes</strong> out of the initial 25 bytes per data point for OTel data, as there's no inverted index and sequence numbers get trimmed once no longer needed for replication. <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/update-by-query-api">Update</a> and <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-delete-by-query">delete</a> by query operations are still supported, albeit with weaker consistency semantics.</p>
<p>If OCC is still deemed important for a particular metrics application, the old behavior can be restored by setting <code>index.disable_sequence_numbers: false</code> in the index template of the involved TSDS.</p>
<h3 id="uselargenumericcodecblocks">Use large numeric codec blocks</h3>
<p>TSDS already uses an advanced codec, as explained in an earlier <a href="https://www.elastic.co/search-labs/blog/time-series-data-elasticsearch-storage-wins#specialized-codecs">article</a>. The codec works very well in most cases, but has poor performance in case of repeated sequences of keywords and numbers, leading to an inflated storage footprint for dimensions containing IP and MAC addresses. We identified that the existing logic for identifying repeated sequences requires larger codec blocks to work well, especially as the sequence length increases. After experimentation, the numeric block size was increased from 128 to 512 elements in <strong>version 9.3</strong>, leading to a reduction of <strong>2 bytes</strong> out of the initial 25 bytes per data point for an OTel dataset containing IP and MAC addresses as dimensions. We're also working on a more configurable codec layout that will allow more flexibility with block sizes and other parameters, based on field type and cardinality.</p>
<h2 id="indexingthroughput">Indexing throughput</h2>
<p>Elasticsearch has support for bulk ingestion of documents. This entrypoint has long been optimized for leniency, ensuring that all docs get accepted. This flexibility, however, incurs additional processing cost during indexing. Metric applications proved good candidates for using different approaches to reduce this overhead, as explained below.</p>
<h3 id="introduceotlpprotobufentrypoint">Introduce OTLP protobuf entrypoint</h3>
<p>OTel metrics and Prometheus have established protocols for metrics ingestion, using protocol buffers. In the past, a translation step was required to convert collected protobuf messages to bulk requests that Elasticsearch can consume.</p>
<p>Elasticsearch was recently extended with endpoints accepting messages from OTel metrics collectors and over Prometheus remote write. Parsing and processing these (binary) messages is cheaper, compared to json parsing, while hash operation over dimensions for <code>_tsid</code> calculations get reused and amortized across more data points within a single protobuf message. Furthermore, <code>_tsid</code>s get evaluated once per doc in the coordinator nodes and propagated to data nodes for indexing, thus deduplicating an expensive step per indexed doc. These improvements led to up to a 20% speedup in indexing throughput for OTel metrics. The OTLP entrypoint was added in version 9.2 (tech preview) and reached GA in <strong>version 9.3</strong>. We've added similar entrypoints for <a href="https://www.elastic.co/observability-labs/blog/prometheus-remote-write-elasticsearch">Prometheus remote write</a> in <strong>version 9.4</strong> (tech preview) and are actively working to cover OTel Logs and Traces.</p>
<h3 id="reduceindexingcpuwithdocvalueskippers">Reduce indexing CPU with doc value skippers</h3>
<p>In addition to a substantial storage footprint, inverted indices require a lot of cpu to build and reconstruct during segment merging. The use of doc value skippers in their place helps also reduce cpu load at ingestion and thus improves indexing throughput by 10%, a welcome bonus on top of the aforementioned storage wins.</p>
<h3 id="syntheticrecoverysource">Synthetic recovery source</h3>
<p>The original <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-source-field">source</a> of a document, as provided at index time, is never stored for metrics. Still, Elasticsearch needed to temporarily store it for replication purposes. That changed in <strong>version 9.1</strong>, where the source gets synthesized on demand for replication purposes. This is known as synthetic recovery source and reduces disk I/O by 50%, with a significant impact to metrics indexing performance. Check out this <a href="https://www.elastic.co/search-labs/blog/elastic-logsdb-tsds-enhancements">article</a> for more details.</p>
<h2 id="queryexecution">Query execution</h2>
<p>Replacing inverted indices with doc value skippers leads to a pure columnar storage layout for TSDS, with metric and dimension fields stored as Lucene doc values, each field encoded and compressed in their own file. Combined with the introduction of the <a href="https://www.elastic.co/blog/elasticsearch-query-language-esql#dedicated-query-engine">ES|QL compute engine</a> that uses vectorized execution internally, it became possible to introduce a fully columnar storage and query processing engine for metrics in Elasticsearch. We pushed this idea to the extreme and implemented a <strong>columnar metrics processing engine</strong> that comfortably outperforms dedicated metrics engines and other columnar stores in query performance.</p>
<h3 id="timeseriesintegrationincomputeengine">Time series integration in compute engine</h3>
<p>Time series processing is largely based on applying aggregation functions per time series (or <code>_tsid</code>), such as a <a href="https://opentelemetry.io/docs/specs/otel/metrics/data-model/#gauge">gauge</a> average or a <a href="https://opentelemetry.io/docs/specs/otel/metrics/data-model/#sums">counter</a> rate. These partial results are then reduced by a secondary function to produce results for the grouping dimensions, e.g. per host and process. Observability dashboards are built on top of this execution model, providing summary views of how metrics evolve over time and allowing for quick deep-dives by filtering on dimension values and time ranges.</p>
<p>To support this execution model, we introduced the <a href="https://www.elastic.co/docs/reference/query-languages/esql/commands/ts#description">TS source command</a>, providing a simple yet powerful syntax for executing such queries that combine an inner aggregation function per time series with an outer aggregation over the partial results of the former. For instance, the following query calculates the hourly sum of rate of search requests per host over the last day:</p>
<pre><code>TS metrics
  | WHERE TRANGE(1d)
  | STATS SUM(RATE(search_requests)) BY TBUCKET(1h), host
</code></pre>
<p>To execute this query, the compute engine is aware of how data is stored and applies the inner aggregation function per <code>_tsid</code> value. Since data are sorted by <code>_tsid</code>, time series aggregation functions process metric values as they get fetched, until the <code>_tsid</code> changes or the timestamp belongs to the next time bucket. This leads to vectorized execution of these functions over the fetched columns of metric values, while dimension values are only fetched (once) when the <code>_tsid</code> changes. The evaluation of the secondary aggregation function is also efficient, with partial aggregates stored in arrays of primitive values that get populated when <code>_tsid</code> values change.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf3aa04dd3516c3fb/6a859a248c294428fdb88511/image12.png" alt="Vectorized time series aggregation execution" /></p>
<p>The compute engine has inherent support for parallel query evaluation, taking full advantage of the available processing cores. Time series aggregations fully use this feature and process data points in parallel as applicable, reducing response times through improved cpu utilization.</p>
<p>Time series processing in ES|QL was introduced in version 9.2 as tech preview and reaches GA in <strong>version 9.4</strong>. We expect all metrics applications to adopt it and benefit from the much improved query performance wins.</p>
<h3 id="zerocopydatadecodingandloading">Zero-copy data decoding and loading</h3>
<p>Vectorized processing of time series data delivered immediate performance wins (<strong>8x</strong> for some queries), compared to aggregations through the <code>/_search</code> API, but the performance was still inferior when compared to competitive metrics stores. Benchmarking and profiling showed that there were too many array copies within the compute engine, between data decoding and evaluation of aggregation functions. To that end, the following optimizations were introduced:</p>
<ul>
<li>The codec for TSDS was extended to decode on-disk data directly into primitive arrays inside blocks that the compute engine uses to evaluate time series aggregations. No additional copies required, as the compute engine can bulk-read these blocks and process their arrays, one column at a time.</li>
<li>Blocks containing a single value N times are represented as constant blocks with these 2 values, as opposed to an array with length N, a form of in-memory run-length encoding. Filtering and aggregation operations were extended to efficiently consume these blocks. This reduced memory pressure and cpu overhead for the <code>_tsid</code> and dimension fields, as their values get clustered due to index sorting.</li>
<li>Documents with null values for the aggregated metric fields are filtered out at the Lucene level, before they get decoded and copied into blocks.</li>
<li>All filters and regular expressions on the timestamp and dimension fields get pushed down to Lucene that makes use of doc value skippers to efficiently filter out non-matching docs.</li>
</ul>
<p>Combined, these optimizations led to query execution speedups exceeding <strong>10x</strong> (totaling 80x when combined with the 8x speedup from vectorized execution). They were included since the introduction of the TS source command in <strong>version 9.2</strong>, and fine-tuned ever since.</p>
<h3 id="optimizedcounterrateevaluation">Optimized counter rate evaluation</h3>
<p>While most time series aggregations can be trivially parallelized and evaluated, rate evaluation of cumulative counters is tricky as it requires processing all data points in order to detect counter resets (e.g. when a host restarts). To address this, the compute engine uses the <code>_tsid</code> prefix to shard time series across threads. Care has been taken to assign in-order ranges of <code>_tsid</code> values to each thread, as opposed to hash-partitioning <code>_tsid</code>s, so that each thread can scan on-disk data in order, still making use of efficient decoding and zero-copying into blocks. The performance wins are impressive, with rate evaluation performance far exceeding dedicated metrics stores as we shall see in the next section.</p>
<p>Another interesting problem for cumulative counters is how to properly calculate counter increases for the entire time bucket when there are no data points at the bucket boundary timestamps. Metrics systems often use extrapolation, extending the first and last data points of each time bucket to the boundaries, or calculate the delta between the last data point of adjacent buckets. We posit we can do better, by interpolating between the last data point of each bucket and the first of the next, to get an estimate of the value on each boundary. The delta is then calculated over the interpolated values of the lower and upper boundary of each time bucket.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9f30a4e56924facc/6a859a28e2447a3c268b085b/image10.png" alt="Counter rate interpolation across time bucket boundaries" /></p>
<h3 id="slidingwindowsupport">Sliding window support</h3>
<p>Elasticsearch has long supported aggregations bucketed by time, but it was not possible to extend the window of processed data beyond each time bucket. Using windows larger than the time bucket, e.g. a window of 5 minutes for per-minute bucketing, helps smoothen out spikes and observe the underlying trend per time series with reduced noise:</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta471cf615145ac8d/6a859a2bf61d6e6e8e9c2037/image3.png" alt="Sliding window smoothing example" /></p>
<p>All <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/time-series-aggregation-functions">time-series aggregation functions</a> have been extended with window support, as an optional argument. In case the window is a multiple of the time bucket (e.g. 1h window with <a href="https://www.elastic.co/docs/reference/query-languages/esql/functions-operators/grouping-functions/tbucket"><code>TBUCKET</code></a><code>(5m)</code>), the compute engine first aggregates data points over intervals matching the time bucket span, and then combines these partial results per window span. This 2-phase approach eliminates repeated scans of data points and makes optimal reuse of intermediate results, improving response times. Window support was introduced as tech preview in version 9.3 and reaches GA in <strong>version 9.4</strong>.</p>
<h3 id="efficientdatetimerounding">Efficient datetime rounding</h3>
<p>Queries on time-series data commonly include time bucketing. While data points can be trivially assigned to sub-hour time buckets, larger buckets start interfering with issues like time zones, daylight savings, variable days per month etc. Elasticsearch has elaborate logic for datetime rounding that takes these peculiarities into account, but that has relatively high cpu cost when processing time series data.</p>
<p>To mitigate this, the compute engine has been extended to identify cases where simpler logic can be employed to assign data points to time buckets. For instance, it can identify when the buckets are sub-hour or when timezones and daylight savings don't affect a particular query, and switches to simple modulo operations for datetime rounding. This led to a further <strong>30%</strong> improvement in response times for certain queries. This change is introduced in <strong>version 9.4</strong>.</p>
<h2 id="performanceevaluation">Performance evaluation</h2>
<p>To evaluate the performance of our offering and track how it evolves and improves over time, we focused on OTel metrics since (a) Open Telemetry is the industry standard for collecting metrics, with universal adoption by all cloud providers and (b) they lead to a storage layout with 1 metric per doc, a setup that traditionally hurt performance for Elasticsearch.</p>
<p>We rely on <a href="https://github.com/elastic/metricsgenreceiver">Metricsgenreceiver</a> to generate datasets. This tool is inspired by <a href="https://github.com/timescale/TSBS">TSBS</a>, producing data simulating the data points collected by the OTel <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/hostmetricsreceiver#readme">hostmetricreceiver</a>. We used two datasets:</p>
<ol>
<li>A low-cardinality setup, with 100 hosts sending metrics every 1s, containing 14k time series in total</li>
<li>A high-cardinality setup, with 10k hosts sending metrics every 10s, containing 1.4M time series in total</li>
</ol>
<p>We benchmarked on single-node deployments on EC2, using <a href="https://aws.amazon.com/ec2/instance-types/c6i/">c6i.4xlarge</a> and <a href="https://aws.amazon.com/ec2/instance-types/c8g/">c8g.8xlarge</a> machines for the low- and high-cardinality datasets respectively.</p>
<p>For competitive comparison, we used Prometheus (v.3.11.1), Mimir (v.3.0.6.) and ClickHouse (v26.3.9.8-lts). Prometheus and Mimir have proper time series processing, e.g. for counter rate, whereas ClickHouse <a href="https://clickhouse.com/docs/use-cases/time-series/analysis-functions">lacks such support</a> and only provides approximate values at best (for instance, it can't track counter resets consistently). We still report response times for ClickHouse to showcase that, once we optimize Elasticsearch for columnar query processing, it can exceed competing columnar engines even when they don't process the data per time series as expected.</p>
<p>We strived to use the default configuration for every system (including Elasticsearch), without tweaking them to optimize performance for the particular workload. This helps understand the user experience when systems are deployed by novice users, without much experience (or time) to tweak before receiving metrics traffic and setting up dashboards. We focused on single-node runs to keep noise low and accommodate all systems (Prometheus doesn't offer a multi-node setup out of the box). Elasticsearch performance provably scales well with the number of nodes; we plan to share scalability results in a future post.</p>
<h3 id="storageefficiencyandindexingthroughput">Storage efficiency and indexing throughput</h3>
<p>Our efforts to improve storage efficiency paid big dividends. Performance on OTel metrics dropped <strong>from 25 to 3.75</strong> bytes per data point, in a year. Such an improvement, on top of an offering already optimized for time series, is really impressive and very rare in the industry:</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2f9e6b91c5dcdaa1/6a859a2e98292617ab582db0/image1.png" alt="Storage efficiency improvements over time" /></p>
<p>The competitive picture looks favorable at this point, with Elasticsearch:</p>
<ul>
<li>Slightly outperforming Mimir in storage efficiency and indexing throughput</li>
<li>Outperforming Prometheus by 2.5x in storage efficiency and by a small margin in indexing throughput</li>
<li>Outperforming ClickHouse by 2x in storage efficiency and by 40% in indexing throughput</li>
</ul>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8ccb2947bcbce791/6a859a31f9373d7ace96eb5b/image7.png" alt="Storage efficiency comparison across systems" /></p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt96ed82b099550f20/6a859a344710c6021ad3c055/image11.png" alt="Indexing throughput comparison across systems" /></p>
<h3 id="queryperformance">Query performance</h3>
<p>The novel columnar engine for metrics processing proves very efficient in practice. We used a mix of queries based on gauge averages and counter rates, the most common operations that require different optimization approaches. The queried interval was 4 hours of data, covering all time series per metric.</p>
<p>ClickHouse doesn't support time series aggregations, so the results have limited value and are not directly comparable to Prometheus or Mimir that natively support time series processing. We used the published <a href="https://clickhouse.com/docs/use-cases/time-series/analysis-functions">guidelines</a> to adjust each query to get similar results to the extent possible. The point is to show how our columnar engine compares to generic columnar stores.</p>
<p>Here is a summary of the results:</p>
<p>| Query type | vs Mimir | vs Prometheus | vs ClickHouse †   |
|---|---|-------------------|-------------------|
| Gauge average | up to 30x faster | up to 30x faster  | up to 8x faster   |
| Counter rate | up to 30x faster | up to 30x faster  | up to 3.5x faster |
| Prefix filter on host name | up to 5x faster | up to 5x faster | up to 3x faster   |
| Gauge average with window | up to 25x faster | up to 25x faster | up to 4x faster   |</p>
<p>†ClickHouse lacks native support for time series aggregations and counter reset detection.</p>
<h4 id="gaugeaverage">Gauge average</h4>
<p>We compared performance of evaluating the per-host hourly average of average memory utilization per time series, using the following queries:</p>
<pre><code># PromQL
avg by (host.name) (avg_over_time(system.memory.utilization[1h]))
</code></pre>
<pre><code># ES|QL
TS metrics-hostmetricsreceiver.otel-default
| STATS AVG(AVG_OVER_TIME(system.memory.utilization)) BY host.name, TBUCKET(1h)
</code></pre>
<p>Elasticsearch comfortably outperforms the other systems by up to 30x, in both low and high cardinality datasets:</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf153d92f6c1f2452/6a859a378c2944279ab8851b/image14.png" alt="Gauge average query performance — low cardinality" /></p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta53ce295d2c310ee/6a859a39ba7acca4239916b7/image2.png" alt="Gauge average query performance — high cardinality" /></p>
<h4 id="counterrate">Counter rate</h4>
<p>We next compared performance of evaluating the per-host hourly average of cpu rate, using the following queries:</p>
<pre><code># PromQL
avg by (host.name) (rate(system.cpu.time[1h]))
</code></pre>
<pre><code># ES|QL
TS metrics-hostmetricsreceiver.otel-default
| STATS AVG(RATE(system.cpu.time)) BY host.name, TBUCKET(1h)
</code></pre>
<p>Despite processing data points per time series in order, counter rate performance matches calculating gauge average (the involved time series have 6.6x more docs than the query above). Elasticsearch maintains its wide advantage compared to the other systems and outperforms Mimir and Prometheus by 30x in the low cardinality dataset and by 16x in the high cardinality one:</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt03149d9c0cc613b5/6a859a3cf5f1a0151a2ebf26/image4.png" alt="Counter rate query performance — low cardinality" /></p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2a36f24c1eb3f55d/6a859a3ef61d6e4afb9c203f/image9.png" alt="Counter rate query performance — high cardinality" /></p>
<p>It's really impressive that, for the high cardinality dataset, Elasticsearch is able to process 4 hours of data for half a million time series in less than 2 seconds, while the other systems take more than 30 seconds, leading to unresponsive dashboards for such queries. ClickHouse is also slower, despite having no logic to detect counter resets and extrapolate/interpolate deltas across buckets.</p>
<h4 id="prefixfilteronhostname">Prefix filter on host name</h4>
<p>We next compared performance of filtering on host names based on their prefix, using the following queries:</p>
<pre><code># PromQL
avg by (host_name)
  (avg_over_time(system_cpu_load_average_1m{host_name=~"host-.*"}[5m]))
</code></pre>
<pre><code># ES|QL
TS metrics-hostmetricsreceiver.otel-default
| WHERE host.name LIKE "host-*"
| STATS AVG(AVG_OVER_TIME(system.cpu.load_average.1m)) BY host.name, TBUCKET(5m)
</code></pre>
<p>Elasticsearch manages to maintain an advantage of up to 5x compared to the other systems, despite replacing the inverted index on <code>host.name</code> with a doc value skipper:</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt32cffb96a5847bc2/6a859a418c29443b00b8851f/image5.png" alt="Prefix filter query performance — low cardinality" /></p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt90f3cd1cabc4b8e0/6a859a44f5f1a019892ebf2a/image6.png" alt="Prefix filter query performance — high cardinality" /></p>
<h4 id="gaugeaveragewithwindow">Gauge average with window</h4>
<p>We compared the performance of time series aggregations with a window of 90 minutes and time buckets of 30 minutes, using the following queries:</p>
<pre><code># PromQL
avg by (host.name) (avg_over_time(system.memory.utilization[90m]))&amp;step=30m
</code></pre>
<pre><code># ES|QL
TS metrics-hostmetricsreceiver.otel-default
| STATS AVG(AVG_OVER_TIME(system.memory.utilization, 90m))
    BY host.name, TBUCKET(30m)
</code></pre>
<p>Elasticsearch comfortably outperforms the other systems in both low and high cardinality datasets:</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5b30bd2052cb4c64/6a859a4ad6cf295c04bafe42/image13.png" alt="Gauge average with window — low cardinality" /></p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6c811ebf944ec4fa/6a859a4de2447a44c98b08bd/image8.png" alt="Gauge average with window — high cardinality" /></p>
<p>Elasticsearch maintains an advantage that reaches 25x for the low cardinality dataset and 8x for the high cardinality one. ClickHouse is outperformed by close to 4x, denoting the efficiency of our approach for windowed query operations.</p>
<h2 id="whatsnextforelasticsearchmetrics">What's next for Elasticsearch metrics</h2>
<p>Elasticsearch has been extended with metrics storage and processing capabilities that outperform Prometheus, Mimir, and ClickHouse. We're making fast progress with supporting <a href="https://www.elastic.co/observability-labs/blog/elasticsearch-supports-promql">PromQL</a> and <a href="https://www.elastic.co/observability-labs/blog/prometheus-remote-write-elasticsearch-architecture">Prometheus remote write</a>, also available as tech preview in <strong>version 9.4</strong>. These extensions enable users familiar with Prometheus and relevant systems to switch their applications to Elasticsearch — no need to migrate existing dashboards. Since Prometheus integration reuses the same storage and query engine that has been presented in this article, the same performance wins are also expected for Prometheus. Furthermore, collected metrics can be queried with PromQL and ES|QL, side-by-side or in ES|QL query pipelines, further boosting the analytics capabilities far beyond what was conceivable so far with Prometheus-based systems.</p>
<p>The improvements in storage efficiency, indexing throughput and query performance are already impressive, but we're not done. We'll be introducing more refinements to the codec for time series data, further reducing bytes per data point. Batch processing of ingested metrics will be further improved, reducing synchronization overhead and redundant processing layers that are not needed for well-formatted collected metrics. We're also planning to make wider use of doc value skippers, storing pre-computed aggregates like sum and count per block of values, to shortcut data point loading and processing where applicable, as well as use more cpu-friendly partitioning and grouping operations.</p>
<h2 id="frequentlyaskedquestions">Frequently asked questions</h2>
<p><strong>What is a columnar metrics engine and why does it matter?</strong>
A columnar metrics engine stores each field in its own file rather than row-by-row, then processes queries by reading only the columns needed. For time series data, this means Elasticsearch can decode metric values, dimension fields, and timestamps independently, applying vectorized operations across each column. The result is faster aggregations and lower storage overhead compared to row-oriented stores.</p>
<p><strong>How does Elasticsearch compare to Prometheus for time series metrics storage and querying?</strong>
Elasticsearch stores OTel metrics at 3.75 bytes per data point in version 9.4, roughly 2.5x less than Prometheus. For queries, Elasticsearch outperforms Prometheus and Mimir by up to 30x in gauge average and counter rate benchmarks. For the high-cardinality dataset (1.4M time series), Elasticsearch processes 4 hours of data in under 2 seconds while Prometheus takes over 30 seconds.</p>
<p><strong>What is Elasticsearch TSDS and when should I use it?</strong>
TSDS (time-series data streams) is Elasticsearch's storage format for metrics and time series data. It sorts documents by time series identifier (<code>_tsid</code>) and timestamp, stores fields in columnar doc values, and uses specialized codecs for compression. Use TSDS for any metrics workload, particularly OpenTelemetry or Prometheus data, where storage efficiency and query speed matter.</p>
<p><strong>What is the TS source command in ES|QL?</strong>
<code>TS</code> is an ES|QL source command, GA in version 9.4, that executes time series queries using a two-level model: an inner aggregation per time series (such as <code>RATE()</code> or <code>AVG_OVER_TIME()</code>), then an outer aggregation over the results. The compute engine processes data in time series sort order, enabling vectorized and parallel execution. Example: <code>TS metrics | STATS AVG(RATE(cpu.time)) BY host.name, TBUCKET(1h)</code>.</p>
<p><strong>How did Elasticsearch go from 25 bytes to 3.75 bytes per OTel data point?</strong>
Four storage changes contributed across versions 9.1 through 9.4: replacing inverted indices with doc value skippers (-10 bytes), enabling synthetic IDs (-5 bytes), trimming sequence numbers (-4 bytes), and increasing codec block size from 128 to 512 elements (-2 bytes). The result is a 6.7x reduction in storage footprint in roughly one year.</p>
<p><strong>Can Elasticsearch replace Prometheus without migrating dashboards?</strong>
Elasticsearch supports Prometheus remote write (tech preview, version 9.4) and PromQL queries (tech preview, version 9.4). Existing Grafana dashboards using PromQL can point to Elasticsearch with minor modifications, and we expect to offer a seamless migration experience when our Prometheus offering reaches GA. The same TSDS storage and ES|QL compute engine power both PromQL and ES|QL queries, so the performance improvements apply to both.</p>
<p><strong>What are doc value skippers and why do they matter for metrics?</strong>
Doc value skippers are Lucene index structures that store min/max values for blocks of documents. For TSDS, which sorts by <code>_tsid</code> and timestamp, they replace inverted indices on dimension fields and <code>@timestamp</code>. This reduces storage by up to 10 bytes per data point and cuts indexing CPU by about 10%, with no measured regression in query performance for time range and dimension filters.</p>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/elasticsearch-columnar-metrics-engine-30x-faster-prometheus</link>
    <guid isPermaLink="false">elasticsearch-columnar-metrics-engine-30x-faster-prometheus</guid>
    <category><![CDATA[Metrics]]></category>
    <category><![CDATA[OpenTelemetry]]></category>
    <dc:creator><![CDATA[Kostas Krikellas,Martijn Van Groningen,Nhat Nguyen,Felix Barnsteiner]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc2478264f91421cc/6a859a514710c6cf3fd3c083/header.jpg" length="0" type="image/jpeg"/>
    <pubDate>Tue, 19 May 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Your PromQL queries now run in Kibana!]]></title>
    <description><![CDATA[With PromQL now natively supported in Kibana, write and execute PromQL for analyzing metrics in Discover, in Dashboards visualizations, in alerting rules and wherever else ES|QL is supported. PromQL is currently available in Tech Preview for common metrics analytics use cases.]]></description>
    <content:encoded><![CDATA[<p>Since its initial development in 2012 alongside Prometheus, PromQL has been a cornerstone of time-series monitoring for over a decade.
While Kibana already comprehensively supports time-series analysis via the ES|QL TS command, we are thrilled to introduce native PromQL support for common metrics analytics use cases.
For teams already fluent in PromQL, this support means a near-zero learning curve and significantly easier onboarding directly into the Elastic ecosystem.</p>
<h2 id="runningpromqlqueriesinkibana">Running PromQL queries in Kibana</h2>
<p>In the ES|QL editor in Kibana, enter the <code>PROMQL</code> command, and type your PromQL in that block.
<code>PROMQL</code> marks that segment so Elasticsearch parses it as PromQL inside the wider ES|QL request Kibana sends.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt09b0a036963d9786/6a7f1a0fb6b7341e46e491b8/promql-first-look.png" alt="Discover in ES|QL mode with a PROMQL query in the bar" /></p>
<h2 id="whatyoucanquery">What you can query</h2>
<p>Here are a few patterns to get started.</p>
<p><strong>Raw metric</strong></p>
<pre><code>PROMQL container.cpu.usage
</code></pre>
<p><strong>Average across all containers</strong></p>
<pre><code>PROMQL avg(container.cpu.usage)
</code></pre>
<p><strong><code>rate()</code> on a counter</strong></p>
<pre><code>PROMQL rate(docker.network.inbound.bytes)
</code></pre>
<p><strong>Aggregated rate</strong></p>
<pre><code>PROMQL sum(rate(docker.network.inbound.bytes))
</code></pre>
<p><strong>Group by a label</strong></p>
<pre><code>PROMQL sum by (agent.id) (rate(docker.network.inbound.bytes))
</code></pre>
<p>You may notice that none of these examples include <code>start</code>, <code>end</code>, <code>step</code>, or a lookback window on every <code>rate()</code>.
Those parameters are optional: the time picker and Kibana defaults handle most of it for you.</p>
<p>Optionally, you can include the data stream name using the <code>index=</code> parameter.
For example: <code>PROMQL index=metrics-docker.cpu-default container.cpu.usage</code>.
Adding the parameter helps narrow down the scope of what data the query scans.</p>
<p>The current release of PromQL tech preview has over 80% query coverage benchmarked against top Grafana dashboards.
Advanced modifiers and specific functions are in consideration for future releases.</p>
<h2 id="findyourstreamsandmetricnames">Find your streams and metric names</h2>
<p>If you have existing PromQL queries, you can use them directly in the <code>PROMQL</code> command without changes.
If you are writing a query from scratch and need to find the exact field names, run <code>TS metrics-*</code> in Discover to see every metrics data stream.
Each metric appears as a small chart so you can tell at a glance what is active.
Hover over a metric and click the "View details" action to see the field name and the data stream it belongs to.</p>
<p>For a deeper walkthrough, see <a href="https://www.elastic.co/docs/solutions/observability/infra-and-hosts/discover-metrics">Explore metrics data with Discover in Kibana</a>.</p>
<h2 id="timepickerandquerytimehandling">Time picker and query time handling</h2>
<p>The time picker in Kibana sets the time window for the query.
Dashboard panels and Alerting rules work the same way using their own time range, so you do not need to write <code>start=</code> or <code>end=</code> in the query itself.</p>
<p>Step is the gap between two consecutive data points on the chart.
A smaller step means more data points across the same span.
If you do not set <code>step=</code> or <code>buckets=</code>, the default is <code>buckets=100</code>.
You can set <code>step=</code> to a fixed width such as <code>1m</code>, or set <code>buckets=</code> to a different target maximum number of data points.</p>
<h2 id="discoveranddashboards">Discover and Dashboards</h2>
<p>In Discover, switch to ES|QL mode and run your <code>PROMQL</code> query so you can see how the metric behaves over the range you pick, as a time-series chart.
When you want to save that visualization, choose "Save visualization to dashboard" and add it to a new or existing dashboard.</p>
<p>Or go to Dashboards directly: add a panel, choose ES|QL, and write your <code>PROMQL</code> query.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt224342ac307c9dc8/6a7f1a1242a117add795c2ed/dashboard-promql.png" alt="Dashboard: ES|QL visualization with PromQL" /></p>
<h2 id="alerting">Alerting</h2>
<p>You can create alert rules using PromQL.
Go to Alerts, open Manage rules, and create a rule.
Search for Elasticsearch query and select it.
Choose ES|QL as the query type.</p>
<p>Write your <code>PROMQL</code> query, but assign the metric to a variable so you can use it in a <code>WHERE</code> clause for the alert condition:</p>
<pre><code>PROMQL metric_value=(sum by (agent.id) (rate(docker.network.inbound.bytes)))
| WHERE metric_value &gt;= 500
</code></pre>
<p>Select <code>@timestamp</code> for the time field and continue defining the rest of the rule configuration.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt50854477d5962a3c/6a7f1a15ea068d9643f0a2bf/alert-rule-promql.png" alt="Alert rule: Elasticsearch query with a PROMQL condition" /></p>
<h2 id="tryit">Try it</h2>
<ol>
<li>Open an <a href="https://cloud.elastic.co/serverless-registration">Observability project on Elastic Cloud Serverless</a>, or use Elastic Stack 9.4.</li>
<li>Write your query: in the ES|QL editor in Kibana, run your PromQL via <code>PROMQL</code>.
You can also go to Dashboards, add a panel, choose ES|QL, and write the query there.</li>
<li>If you are writing from scratch and need to find metric names, run <code>TS metrics-*</code> in Discover (see "Find your streams and metric names" above).</li>
<li>Check the results and adapt the query if needed.</li>
</ol>
<p>PromQL support in Elasticsearch and Kibana will continue to evolve.
Follow the Observability Labs feed for follow-up posts as coverage and ergonomics improve.</p>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/promql-queries-run-in-kibana</link>
    <guid isPermaLink="false">promql-queries-run-in-kibana</guid>
    <category><![CDATA[Metrics]]></category>
    <dc:creator><![CDATA[Miguel Sánchez Gómez,Vinay Chandrasekhar,Felix Barnsteiner]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt486d62547cd575db/6a7f1a1842a117335495c2f1/cover.png" length="0" type="image/png"/>
    <pubDate>Wed, 15 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Ship Prometheus Metrics to Elasticsearch with Remote Write]]></title>
    <description><![CDATA[Elasticsearch natively supports Prometheus Remote Write. Add a single remote_write block to your Prometheus config and use Elasticsearch as Prometheus-compatible long-term storage.]]></description>
    <content:encoded><![CDATA[<p>Prometheus has a well-defined protocol for shipping metrics to external storage: <a href="https://prometheus.io/docs/specs/prw/remote_write_spec/">Remote Write</a>.
Elasticsearch now implements this protocol natively, so you can add it as a <code>remote_write</code> destination with a single config block.</p>
<p>This lets you bring your Prometheus metrics into the same cluster which can also store logs, traces, and other data.
One storage backend, one set of access controls, one place to query.</p>
<h2 id="whystoreprometheusmetricsinelasticsearch">Why store Prometheus metrics in Elasticsearch?</h2>
<p>Prometheus local storage is designed for short retention, typically 15 to 30 days.
For anything beyond that, you need a remote storage backend.</p>
<p>Elasticsearch's time series data streams (TSDS) are built for highly efficient long term metrics storage: automatic rollover, time-based partitioning, compression via index sorting, and downsampling to reduce storage costs as data ages.
Your Prometheus scrape configs stay the same.</p>
<p>Recent Elasticsearch releases have significantly reduced the storage footprint for metrics.
A dedicated post with the numbers is coming soon.</p>
<p>On the query side, ES|QL embraces PromQL: a built-in <code>PROMQL</code> function lets your existing queries run unchanged, while the rest of ES|QL is available when you want joins, aggregations, or transformations that span multiple datasets.</p>
<p>And because metrics land in the same store as your logs, traces, and profiling data, correlating signals across types becomes a single query rather than a cross-system investigation.</p>
<h2 id="howitworks">How it works</h2>
<p>For a detailed look at what happens inside Elasticsearch when a Remote Write request arrives — protobuf parsing, metric type inference, TSDS mapping, and data stream routing — see <a href="https://www.elastic.co/blog/prometheus-remote-write-elasticsearch-architecture">How Prometheus Remote Write Ingestion Works in Elasticsearch</a>.</p>
<p>Prometheus sends metrics to Elasticsearch via the standard Remote Write protocol (v1).
The endpoint accepts protobuf-encoded, snappy-compressed <code>WriteRequest</code> payloads.</p>
<p>Each sample becomes an Elasticsearch document in a pre-defined time series data stream.
Prometheus labels become TSDS dimensions.
The metric value is stored in a typed field under <code>metrics.&lt;metric_name&gt;</code>.</p>
<p>Elasticsearch infers the metric type (counter vs gauge) from naming conventions.
Names ending in <code>_total</code>, <code>_sum</code>, <code>_count</code>, or <code>_bucket</code> are treated as counters.
Everything else is treated as a gauge.</p>
<h2 id="settingitup">Setting it up</h2>
<h3 id="step1getanelasticsearchendpoint">Step 1: Get an Elasticsearch endpoint</h3>
<p>You need an Elasticsearch cluster with the Prometheus endpoints enabled.
The simplest option is Elastic Cloud Serverless, where this works out of the box.</p>
<p>For serverless: sign in to <a href="https://cloud.elastic.co">cloud.elastic.co</a>, create an Observability project, and copy the Elasticsearch endpoint from the project settings page.
The endpoint looks like <code>https://&lt;project-id&gt;.es.&lt;region&gt;.&lt;provider&gt;.elastic.cloud</code>.</p>
<h3 id="step2createanapikey">Step 2: Create an API key</h3>
<p>Create an API key scoped to writing metrics data streams only.
In your Elastic Cloud Serverless project, go to <strong>Admin and settings</strong> (the gear icon at the bottom left of the side nav), then <strong>API keys</strong>.</p>
<p>Use the following role descriptor in the <strong>Control security privileges</strong> section:</p>
<pre><code>{
  "ingest": {
    "indices": [
      {
        "names": ["metrics-*"],
        "privileges": ["auto_configure", "create_doc"]
      }
    ]
  }
}
</code></pre>
<p>Copy the key value before closing the dialog.
You will not be able to retrieve it again.</p>
<h3 id="step3configureprometheus">Step 3: Configure Prometheus</h3>
<p>Add the following <code>remote_write</code> block to your <code>prometheus.yml</code>:</p>
<pre><code>remote_write:
  - url: "https://YOUR_ES_ENDPOINT/_prometheus/api/v1/write"
    authorization:
      type: ApiKey
      credentials: YOUR_API_KEY
</code></pre>
<p>That's it.
Prometheus will start shipping metrics to Elasticsearch on the next scrape interval.</p>
<p>If you use <a href="https://grafana.com/docs/alloy/latest/">Grafana Alloy</a> instead of Prometheus, the equivalent configuration is:</p>
<pre><code>prometheus.remote_write "elasticsearch" {
  endpoint {
    url = "https://YOUR_ES_ENDPOINT/_prometheus/api/v1/write"
    headers = {"Authorization" = "ApiKey YOUR_API_KEY"}
  }
}
</code></pre>
<h2 id="routingmetricstoseparatedatastreams">Routing metrics to separate data streams</h2>
<p>By default, all metrics land in <code>metrics-generic.prometheus-default</code>.
You can route metrics from different environments or teams into separate data streams using the dataset and namespace path segments in the URL.</p>
<p>The three URL patterns are:</p>
<ul>
<li><code>/_prometheus/api/v1/write</code> routes to <code>metrics-generic.prometheus-default</code></li>
<li><code>/_prometheus/metrics/{dataset}/api/v1/write</code> routes to <code>metrics-{dataset}.prometheus-default</code></li>
<li><code>/_prometheus/metrics/{dataset}/{namespace}/api/v1/write</code> routes to <code>metrics-{dataset}.prometheus-{namespace}</code></li>
</ul>
<p>For example, using <code>/_prometheus/metrics/infrastructure/production/api/v1/write</code> routes data to <code>metrics-infrastructure.prometheus-production</code>.</p>
<p>This is useful for separating production from staging metrics, or giving different teams their own data streams with independent lifecycle policies.</p>
<h2 id="whatgetsstored">What gets stored</h2>
<p>Here is what a sample document looks like in Elasticsearch:</p>
<pre><code>{
  "@timestamp": "2026-04-02T10:30:00.000Z",
  "data_stream": {
    "type": "metrics",
    "dataset": "generic.prometheus",
    "namespace": "default"
  },
  "labels": {
    "__name__": "prometheus_http_requests_total",
    "handler": "/api/v1/query",
    "code": "200",
    "instance": "localhost:9090",
    "job": "prometheus"
  },
  "metrics": {
    "prometheus_http_requests_total": 42
  }
}
</code></pre>
<p>Labels map to keyword fields that serve as TSDS <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/time-series-data-stream-tsds#time-series-dimension">dimensions</a>.
The metric value is stored under <code>metrics.&lt;metric_name&gt;</code> with the inferred <code>time_series_metric</code> type (counter or gauge).</p>
<p>Elasticsearch installs a built-in index template matching <code>metrics-*.prometheus-*</code> that configures TSDS mode, <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/passthrough">passthrough</a> dimension container objects, and a 10,000 field limit.
The <a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/mapping-limit">field limit</a> is configurable via a custom component template (see the custom metric type inference section below for how to use one).
You do not need to create any templates or mappings yourself.</p>
<h2 id="custommetrictypeinference">Custom metric type inference</h2>
<p>Metric type inference is based on naming conventions.
Metrics that don't follow Prometheus naming best practices may be classified incorrectly.
You can override the defaults by creating a <code>metrics-prometheus@custom</code> component template with your own dynamic templates.
For example, to mark all <code>*_counter</code> metrics as counters:</p>
<pre><code>{
  "template": {
    "mappings": {
      "dynamic_templates": [
        {
          "counter": {
            "path_match": "metrics.*_counter",
            "mapping": {
              "type": "double",
              "time_series_metric": "counter"
            }
          }
        }
      ]
    }
  }
}
</code></pre>
<p>Custom rules are merged with the built-in patterns, so the defaults still apply for metrics you don't override.</p>
<h2 id="currentlimitations">Current limitations</h2>
<p>Only Remote Write v1 is supported.
v2, which brings native histograms and exemplars, is planned.</p>
<p>Staleness markers (special NaN values Prometheus uses to signal a series has disappeared) are not yet stored or respected in queries.</p>
<p>Non-finite values (NaN, Infinity) are silently dropped.</p>
<h2 id="getstarted">Get started</h2>
<p>The Prometheus Remote Write endpoint is available now on <a href="https://cloud.elastic.co/serverless-registration?onboarding_token=observability">Elasticsearch Serverless</a> with no configuration needed.
To get started with a local cluster, <a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/local-development-installation-quickstart">start-local</a> gets you a single-node cluster in minutes.</p>
<p>Once metrics are flowing, you can query them with ES|QL using the built-in <code>PROMQL</code> function for PromQL compatibility, or write native ES|QL queries to join metrics with logs and traces in the same store.</p>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/prometheus-remote-write-elasticsearch</link>
    <guid isPermaLink="false">prometheus-remote-write-elasticsearch</guid>
    <category><![CDATA[Metrics]]></category>
    <category><![CDATA[Infrastructure Monitoring]]></category>
    <dc:creator><![CDATA[Felix Barnsteiner]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt17c564dfb7ca5dd6/6a7f19d65967e538655dd6b1/header.jpg" length="0" type="image/jpeg"/>
    <pubDate>Tue, 14 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[How Prometheus Remote Write Ingestion Works in Elasticsearch]]></title>
    <description><![CDATA[A look under the hood at Elasticsearch's Prometheus Remote Write implementation: protobuf parsing, metric type inference, TSDS mapping, and data stream routing.]]></description>
    <content:encoded><![CDATA[<p>Elasticsearch recently added native support for the Prometheus Remote Write protocol.
You can point Prometheus (or Grafana Alloy) at an Elasticsearch endpoint and ship metrics without any adapter in between.</p>
<p>This post looks at what happens inside Elasticsearch when a Remote Write request arrives.</p>
<p>If you want to understand the implementation, evaluate how Elasticsearch compares to other Prometheus-compatible backends, or contribute, this is the post for you.
A companion post, <a href="https://www.elastic.co/blog/prometheus-remote-write-elasticsearch">Ship Prometheus Metrics to Elasticsearch with Remote Write</a>, covers the setup and configuration side.</p>
<h2 id="requestlifecyclefromhttptoindexeddocuments">Request lifecycle: from HTTP to indexed documents</h2>
<p>A quick note on the Prometheus data model before we dive in: Prometheus stores all metric values as 64-bit floats and treats the metric name as just another label (<code>__name__</code>).
The storage engine itself is agnostic of whether a value is a counter or a gauge.
Keep this in mind as we walk through how Elasticsearch maps these concepts.</p>
<p>Here is the full path of a Remote Write request through Elasticsearch:</p>
<ol>
<li><strong>HTTP layer</strong> — The endpoint receives a compressed protobuf payload, checks indexing pressure, decompresses with Snappy, and parses the protobuf <code>WriteRequest</code>.</li>
<li><strong>Document construction</strong> — Each sample in each time series becomes an Elasticsearch document with <code>@timestamp</code>, <code>labels.*</code>, and <code>metrics.*</code> fields.</li>
<li><strong>Bulk indexing</strong> — All documents from a single request are written to the target data stream via a single bulk call.</li>
</ol>
<p>The sections below walk through each stage in detail.</p>
<h3 id="httplayer">HTTP layer</h3>
<p>The endpoint accepts <code>application/x-protobuf</code> POST requests.
The incoming request body is tracked against the same <a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/pressure">indexing pressure limits</a> that protect the bulk indexing API.
If the cluster is already under heavy indexing load, the request gets rejected with a 429 before any parsing happens.</p>
<p>Prometheus compresses Remote Write payloads with Snappy.
Elasticsearch decompresses the body in a streaming fashion without materializing it into a single contiguous allocation, and validates the declared uncompressed size against a configurable maximum to guard against decompression bombs.</p>
<p>The decompressed body is then deserialized as a protobuf <code>WriteRequest</code>.
Each <code>WriteRequest</code> contains a list of <code>TimeSeries</code> entries, and each <code>TimeSeries</code> contains a set of labels (key-value pairs) and a list of samples (timestamp + float64 value).</p>
<h3 id="documentconstruction">Document construction</h3>
<p>For each sample in each time series, Elasticsearch builds an index request.
Here is what a single document looks like:</p>
<pre><code>{
  "@timestamp": "2026-04-01T12:00:00.000Z",
  "data_stream": {
    "type": "metrics",
    "dataset": "generic.prometheus",
    "namespace": "default"
  },
  "labels": {
    "__name__": "http_requests_total",
    "job": "prometheus",
    "instance": "localhost:9090",
    "method": "GET",
    "status": "200"
  },
  "metrics": {
    "http_requests_total": 1027.0
  }
}
</code></pre>
<p>All labels from the Prometheus time series (including <code>__name__</code>) end up in the <code>labels.*</code> fields.
The metric value goes into <code>metrics.&lt;metric_name&gt;</code>, where <code>&lt;metric_name&gt;</code> is the value of the <code>__name__</code> label.</p>
<p>Time series without a <code>__name__</code> label are dropped entirely, and the samples are counted as failures.
Non-finite values (NaN, Infinity, negative Infinity) are silently skipped.
This includes Prometheus staleness markers, which use a special NaN bit pattern (<code>0x7ff0000000000002</code>) to signal that a series has disappeared.</p>
<h3 id="onesampleonedocument">One sample, one document</h3>
<p>You might wonder whether storing each individual sample as its own document creates significant storage overhead, especially for labels.
A common pattern to reduce that overhead was to group all metrics sharing the same labels and timestamp into a single document.</p>
<p>With recent TSDB improvements, that optimization is no longer necessary.
Elasticsearch has trimmed the per-document storage overhead to the point where there is negligible difference between packing many metrics in a single document and writing each sample separately.
A dedicated post covering these TSDB storage improvements in detail is coming soon.</p>
<h3 id="bulkindexing">Bulk indexing</h3>
<p>All documents from a single Remote Write request are sent to Elasticsearch via a single bulk request.
Each document targets the data stream <code>metrics-{dataset}.prometheus-{namespace}</code> and is indexed as an append-only create operation.</p>
<h2 id="metrictypeinference">Metric type inference</h2>
<p>Remote Write v1 does not reliably transmit metric types alongside samples.
Prometheus sends metadata (type, help text, unit) in separate requests roughly once per minute, and those requests may land on a different node than the samples.
Buffering samples until metadata arrives is not practical in a distributed system, so Elasticsearch infers the type from naming conventions instead.</p>
<p>Metric names ending in <code>_total</code>, <code>_sum</code>, <code>_count</code>, or <code>_bucket</code> are mapped as counters.
Everything else defaults to gauge.
This is a well-established convention that other Prometheus-compatible backends use as well.</p>
<pre><code>http_requests_total             → counter
request_duration_seconds_sum    → counter
request_duration_seconds_count  → counter
request_duration_seconds_bucket → counter
process_resident_memory_bytes   → gauge
go_goroutines                   → gauge
</code></pre>
<p>The heuristic can be wrong.
A metric like <code>temperature_total</code> (if someone named a gauge that way) would be misclassified as a counter.
The main consequence today is that some ES|QL functions like <code>rate()</code> require the metric type to be a counter and will reject a misclassified gauge.
For PromQL, we plan to lift this restriction so that <code>rate()</code> works regardless of the declared type, which will make incorrect inference less consequential.</p>
<p>You can override the inference by creating a <code>metrics-prometheus@custom</code> component template with custom dynamic templates.
For example, to treat all <code>*_counter</code> fields as counters:</p>
<pre><code>PUT /_component_template/metrics-prometheus@custom
{
  "template": {
    "mappings": {
      "dynamic_templates": [
        {
          "counter": {
            "path_match": "metrics.*_counter",
            "mapping": {
              "type": "double",
              "time_series_metric": "counter"
            }
          }
        }
      ]
    }
  }
}
</code></pre>
<p>Custom dynamic templates are merged with the built-in ones, so the default naming-convention rules still apply for metrics you don't explicitly override.</p>
<h2 id="theindextemplate">The index template</h2>
<p>Elasticsearch installs a built-in index template that matches <code>metrics-*.prometheus-*</code>.
This template is what makes field type inference work without manual mapping configuration.</p>
<p><strong>TSDS mode</strong> is enabled, which gives you time-based partitioning, optimized storage, <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/time-series-data-stream-tsds#time-series-dimension">deduplication</a>, and the ability to downsample data as it ages.</p>
<p><strong><a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/passthrough">Passthrough</a> object fields</strong> are used for both the <code>labels</code> and <code>metrics</code> namespaces.
This serves three purposes:</p>
<ol>
<li><p><strong>Namespace isolation</strong>: Labels and metrics live in separate object namespaces (<code>labels.*</code> and <code>metrics.*</code>), so a label named <code>status</code> and a metric named <code>status</code> cannot conflict with each other.</p></li>
<li><p><strong>Dimension identification</strong>: The <code>labels</code> passthrough object is configured with <code>time_series_dimension: true</code>, which means every field under <code>labels.*</code> is automatically treated as a TSDS dimension.
When Prometheus sends a time series with a label you have never seen before, it becomes a dimension without any explicit field mapping.</p></li>
<li><p><strong>Transparent queries</strong>: You don't need to write the <code>labels.</code> or <code>metrics.</code> prefix in ES|QL or PromQL.
A query can reference <code>job</code> instead of <code>labels.job</code>, or <code>http_requests_total</code> instead of <code>metrics.http_requests_total</code>.
The passthrough mapping handles the resolution.</p></li>
</ol>
<p><strong>Dynamic inference for metrics</strong> applies the naming-convention heuristics described above.
When a new metric name appears for the first time, its field mapping is created automatically under <code>metrics.*</code> with the correct <code>time_series_metric</code> annotation.</p>
<p><strong>Failure store</strong> is enabled.
Documents that fail indexing (for example, due to a mapping conflict where the same metric name appears with incompatible types) are routed to a separate failure store instead of being dropped silently.</p>
<h2 id="datastreamrouting">Data stream routing</h2>
<p>The three URL patterns map directly to data stream names:</p>
<p>| URL pattern | Data stream |
|---|---|
| <code>/_prometheus/api/v1/write</code> | <code>metrics-generic.prometheus-default</code> |
| <code>/_prometheus/metrics/{dataset}/api/v1/write</code> | <code>metrics-{dataset}.prometheus-default</code> |
| <code>/_prometheus/metrics/{dataset}/{namespace}/api/v1/write</code> | <code>metrics-{dataset}.prometheus-{namespace}</code> |</p>
<p>This lets you separate metrics from different Prometheus instances or environments into different data streams.
That separation is useful for a few reasons.</p>
<p><strong>Lifecycle isolation</strong>: you can apply different retention policies per data stream.
Production metrics might be kept for 90 days, while dev metrics might expire after 7 days.</p>
<p><strong>Access control</strong>: you can scope API keys to specific data streams.
A team's Prometheus instance writes to <code>metrics-teamA.prometheus-prod</code>, and their API key only has access to that stream.</p>
<p><strong>Query performance</strong>: PromQL queries and Grafana dashboards can be scoped to a specific index pattern, avoiding scans of unrelated data.</p>
<h2 id="errorhandlingandtheremotewritespec">Error handling and the Remote Write spec</h2>
<p>The Remote Write spec defines two response classes: retryable (5xx, 429) and non-retryable (4xx).
Prometheus uses this distinction to decide whether to retry or drop a failed request.</p>
<p>Elasticsearch returns 429 (Too Many Requests) if any sample in the bulk request was rejected due to indexing pressure.
This signals Prometheus to back off and retry with exponential backoff.</p>
<p>For partial failures (some samples indexed, others rejected), the response includes a summary.
It reports how many samples failed, grouped by target index and status code, along with a sample error message from each group.</p>
<p>Time series without a <code>__name__</code> label result in a 400 error for those samples.
Non-finite values (NaN, Infinity) are silently dropped: Prometheus receives a success response and will not retry.</p>
<p>NaN appears most commonly for summary quantiles when no observations have been recorded (for example, a p99 latency metric before any requests arrive) and for staleness markers.
The practical impact of dropping these is limited today: for most queries, a missing sample behaves similarly to a NaN one, since PromQL's lookback window fills the gap with the last known value either way.
The more significant gap is staleness markers, which are covered below.</p>
<h2 id="whatsnextremotewritev2andbeyond">What's next: Remote Write v2 and beyond</h2>
<p>Remote Write v2 is still experimental, which is why the current implementation starts with v1.
But v2 addresses several of v1's shortcomings.</p>
<p><strong>Metadata alongside samples</strong>: v2 sends metric type, unit, and description with each time series in the same request.
This eliminates the need for naming-convention heuristics entirely.</p>
<p><strong>Native histograms</strong>: v2 supports Prometheus native histograms, which map naturally to Elasticsearch's <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/exponential-histogram"><code>exponential_histogram</code></a> field type.
Classic histograms (one counter per bucket boundary) are verbose and lose precision at query time.
Native histograms are more compact and more accurate.</p>
<p><strong>Dictionary encoding</strong>: v2 replaces repeated label strings with integer references, reducing payload size significantly for high-cardinality label sets.</p>
<p><strong>Created timestamps</strong>: counters in v2 include a "created" timestamp that marks when the counter was initialized.
This allows backends to detect counter resets more accurately than the current heuristic (value decreased since last sample).</p>
<p>Beyond v2, there are two other items in consideration for future enhancements.</p>
<p><strong>Staleness marker support</strong>: currently, staleness markers (the special NaN that Prometheus writes when a scrape target disappears) are dropped.
Supporting them would allow correct PromQL lookback behavior and avoid the 5-minute "trailing data" artifact where a disappeared series still appears in query results.</p>
<p><strong>Shared metric field</strong>: the current layout creates a separate field for each metric name (<code>metrics.http_requests_total</code>, <code>metrics.go_goroutines</code>, etc.).
This works, but it means the number of field mappings grows with the number of distinct metric names, which is why the field limit is set to 10,000 for Prometheus data streams.
A different approach we're considering is to store the metric name only in the <code>__name__</code> label and write the metric value to a single shared field.
This eliminates the field explosion problem entirely and more closely matches how Prometheus stores data internally.
This direction is part of the broader effort to make Elasticsearch's metrics storage more efficient and more compatible with Prometheus conventions.</p>
<h2 id="availability">Availability</h2>
<p>The Prometheus Remote Write endpoint is available now on <a href="https://cloud.elastic.co/serverless-registration">Elasticsearch Serverless</a> with no additional configuration.</p>
<p>For self-managed clusters, check out <a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/local-development-installation-quickstart">start-local</a> to get up and running quickly.</p>
<p>If you run into issues or have feedback, open an issue on the <a href="https://github.com/elastic/elasticsearch">Elasticsearch repository</a>.</p>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/prometheus-remote-write-elasticsearch-architecture</link>
    <guid isPermaLink="false">prometheus-remote-write-elasticsearch-architecture</guid>
    <category><![CDATA[Metrics]]></category>
    <category><![CDATA[Data Management]]></category>
    <dc:creator><![CDATA[Felix Barnsteiner]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5bbe42711414c88f/6a7f19d2eab5be381320aaec/header.jpg" length="0" type="image/jpeg"/>
    <pubDate>Tue, 14 Apr 2026 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Improving the Elastic APM UI performance with continuous rollups and service metrics]]></title>
    <description><![CDATA[We made significant improvements to the UI performance in Elastic APM to make it scale with even the most demanding workloads, by pre-aggregating metrics at the service level, and storing the metrics at different levels of granularity.]]></description>
    <content:encoded><![CDATA[<p>In today's fast-paced digital landscape, the ability to monitor and optimize application performance is crucial for organizations striving to deliver exceptional user experiences. At Elastic, we recognize the significance of providing our user base with a reliable <a href="https://www.elastic.co/observability">observability platform</a> that scales with you as you’re onboarding thousands of services that produce terabytes of data each day. We have been diligently working behind the scenes to enhance our solution to meet the demands of even the largest deployments.</p>
<p>In this blog post, we are excited to share the significant strides we have made in improving the UI performance of Elastic APM. Maintaining a snappy user interface can be a challenge when interactively summarizing the massive amounts of data needed to provide an overview of the performance for an entire enterprise-scale service inventory. We want to assure our customers that we have listened, taken action, and made notable architectural changes to elevate the scalability and maturity of our solution.</p>
<h2 id="architecturalenhancements">Architectural enhancements</h2>
<p>Our journey began back in the 7.x series where we noticed that doing ad-hoc aggregations on raw <a href="https://www.elastic.co/guide/en/apm/guide/current/data-model-transactions.html">transaction</a> data put Elasticsearch<sup>®</sup> under a lot of pressure in large-scale environments. Since then, we’ve begun to pre-aggregate the transactions into transaction metrics during ingestion. This has helped to keep the performance of the UI relatively stable. Regardless of how busy the monitored application is and how many transaction events it is creating, we’re just querying pre-aggregated metrics that are stored at a constant rate. We’ve enabled the metrics-powered UI by default in <a href="https://github.com/elastic/kibana/issues/92024">7.15</a>.</p>
<p>However, when showing an inventory of a large number of services over large time ranges, the number of metric data points that need to be aggregated can still be large enough to cause performance issues. We also create a time series for each distinct set of dimensions. The dimensions include metadata, such as the transaction name and the host name. Our <a href="https://www.elastic.co/guide/en/apm/guide/current/data-model-metrics.html#_transaction_metrics">documentation</a> includes a full list of all available dimensions. If there’s a very high number of unique transaction names, which could be a result of improper instrumentation (see <a href="https://www.elastic.co/guide/en/kibana/current/troubleshooting.html#troubleshooting-too-many-transactions">docs</a> for more details), this will create a lot of individual time series that will need to be aggregated when requesting a summary of the service’s overall performance. Global labels that are added to the APM Agent configuration are also added as dimensions to these metrics, and therefore they can also impact the number of time series. Refer to the FAQs section below for more details.</p>
<p>Within the 8.7 and 8.8 releases, we’ve addressed these challenges with the following architectural enhancements that aim to reduce the number of documents Elasticsearch needs to search and aggregate on-the-fly, resulting in faster response times:</p>
<ul>
<li><strong>Pre-aggregation of transaction metrics into service metrics.</strong> Instead of aggregating all distinct time series that are created for each individual transaction name on-the-fly for every user request, we’re already pre-aggregating a summary time series for each service during data ingestion. Depending on how many unique transaction names the services have, this reduces the number of documents Elasticsearch needs to look up and aggregate by a factor of typically 10–100. This is particularly useful for the <a href="https://www.elastic.co/guide/en/kibana/master/services.html">service inventory</a> and the <a href="https://www.elastic.co/guide/en/kibana/master/service-overview.html">service overview</a> pages.</li>
<li><strong>Pre-aggregation of all metrics into different levels of granularity.</strong> The APM UI chooses the most appropriate level of granularity, depending on the selected time range. In addition to the metrics that are stored at a 1-minute granularity, we’re also summarizing and storing metrics at a 10-minute and 60-minute granularity level. For example, when looking at a 7-day period, the 60-minute data stream is queried instead of the 1-minute one, resulting in 60x fewer documents for Elasticsearch to examine. This makes sure that all graphs are rendered quickly, even when looking at larger time ranges.</li>
<li><strong>Safeguards on the number of unique transactions per service for which we are aggregating metrics.</strong> Our agents are designed to keep the cardinality of the transaction name low. But in the wild, we’ve seen some services that have a huge amount of unique transaction names. This used to cause performance problems in the UI because APM Server would create many time series that the UI needed to aggregate at query time. In order to protect APM Server from running out of memory when aggregating a large number of time series for each unique transaction name, metrics were published without aggregating when limits for the number of time series were reached. This resulted in a lot of individual metric documents that needed to be aggregated at query time. To address the problem, we've introduced a system where we aggregate metrics in a dedicated overflow bucket for each service when limits are reached. Refer to our <a href="https://www.elastic.co/guide/en/kibana/8.8/troubleshooting.html#troubleshooting-too-many-transactions">documentation</a> for more details.</li>
</ul>
<p>The exact factor of the document count reduction depends on various conditions. But to get a feeling for a typical scenario, if your services, on average, have 10 instances, no instance-specific global labels, 100 unique transaction names each, and you’re looking at time ranges that can leverage the 60m granularity, you’d see a reduction of documents that Elasticsearch needs to aggregate by a factor of 180,000 (10 instances x 100 transaction names x 60m x 3 because we’re also collapsing the event.outcome dimension). While the response times of Elasticsearch aggregations isn’t exactly scaling linearly with the number of documents, there is a strong correlation.</p>
<h2 id="faqs">FAQs</h2>
<h3 id="whenupgradingtothelatestversionwillmyolddataalsoloadfaster">When upgrading to the latest version, will my old data also load faster?</h3>
<p>Updating to 8.8 doesn’t immediately make the UI faster. Because the improvements are powered by pre-aggregations that APM Server is doing during ingestion, only new data will benefit from it. For that reason, you should also make sure to update APM Server as well. The UI can still display data that was ingested using an older version of the stack.</p>
<h3 id="iftheuiisbasedonmetricscanistillsliceanddiceusingcustomlabels">If the UI is based on metrics, can I still slice and dice using custom labels?</h3>
<p>High cardinality analysis is a big strength of Elastic Observability, and this focus on pre-aggregated metrics does not compromise that in any way.</p>
<p>The UI implements a sophisticated fallback mechanism that uses service metrics, transaction metrics, or raw transaction events, depending on which filters are applied. We’re not creating metrics for each user.id, for example. But you can still filter the data by user.id and the UI will then use raw transaction events. Chances are that you’re looking at a narrow slice of data when filtering by a dimension that is not available on the pre-aggregated metrics, therefore aggregations on the raw data are typically very fast.</p>
<p>Note that all global labels that are added to the APM agent configuration are part of the dimension of the pre-aggregated metrics, with the exception of RUM (see more details in <a href="https://github.com/elastic/apm-server/issues/11037">this issue</a>).</p>
<h3 id="caniusethepreaggregatedmetricsincustomdashboards">Can I use the pre-aggregated metrics in custom dashboards?</h3>
<p>Yes! If you use <a href="https://www.elastic.co/guide/en/kibana/current/lens.html">Lens</a> and select the "APM" data view, you can filter on either metricset.name:service_transaction or metricset.name:transaction, depending on the level of detail you need. Transaction latency is captured in transaction.duration.histogram, and successful outcomes and failed outcomes are stored in event.success_count. If you don't need a distribution of values, you can also select the transaction.duration.summary field for your metric aggregations, which should be faster. If you want to calculate the failure rate, here's a <a href="https://www.elastic.co/guide/en/kibana/current/lens.html#lens-formulas">Lens formula</a>: 1 - (sum(event.success_count) / count(event.success_count)). Note that the only granularity supported here is 1m.</p>
<h3 id="dotheadditionalmetricshaveanimpactonthestorage">Do the additional metrics have an impact on the storage?</h3>
<p>While we’re storing more metrics than before, and we’re storing all metrics in different levels of granularity, we were able to offset that by enabling <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-source-field.html#synthetic-source">synthetic source</a> for all metric data streams. We’ve even increased the default retention for the metrics in the coarse-grained granularity levels, so that the 60m rollup data streams are now stored for 390 days. Please consult our <a href="https://www.elastic.co/guide/en/apm/guide/current/apm-data-streams.html">documentation</a> for more information about the different metric data streams.</p>
<h3 id="aretherelimitsontheamountoftimeseriesthatapmservercanaggregate">Are there limits on the amount of time series that APM Server can aggregate?</h3>
<p>APM Server performs pre-aggregations in memory, which is fast, but consumes a considerable amount of memory. There are limits in place to protect APM Server from running out of memory, and from 8.7, most of them scale with available memory by default, meaning that allocating more memory to APM Server will allow it to handle more unique pre-aggregation groups like services and transactions. These limits are described in <a href="https://www.elastic.co/guide/en/apm/guide/current/data-model-metrics.html#_aggregated_metrics_limits_and_overflows">APM Server Data Model docs</a>.</p>
<p>On the APM Server roadmap, we have plans to move to a LSM-based approach where pre-aggregations are performed with the help of disks in order to reduce memory usage. This will enable APM Server to scale better with the input size and cardinality.</p>
<p>A common pitfall when working with pre-aggregations is to add instance-specific global labels to APM agents. This may exhaust the aggregation limits and cause metrics to be aggregated under the overflow bucket instead of the corresponding service. Therefore, make sure to follow the best practice of only adding a limited set of global labels to a particular service.</p>
<h2 id="validation">Validation</h2>
<p>To validate the effectiveness of the new architecture, and to ensure that the accuracy of the data is not negatively affected, we prepared a test environment where we generated 35K+ transactions per minute in a timespan of 14 days resulting in approximately 850 million documents.</p>
<p>We’ve tested the queries that power our service inventory, the service overview, and the transaction details using different time ranges (1d, 7d, 14d). Across the board, we’ve seen orders of magnitude improvements. Particularly, queries across larger time ranges that benefit from using the coarse-grained metrics in addition to the pre-aggregated service metrics saw incredible reductions of the response time.</p>
<p>We’ve also validated that there’s no loss in accuracy when using the more coarse-grained metrics for larger time ranges.</p>
<p>Every environment will behave a bit differently, but we’re confident that the impressive improvements in response time will translate well to setups of even bigger scale.</p>
<h2 id="plannedimprovements">Planned improvements</h2>
<p>As mentioned in the FAQs section, the number of time series for transaction metrics can grow quickly, as it is the product of multiple dimensions. For example, given a service that runs on 100 hosts and has 100 transaction names that each have 4 transaction results, APM Server needs to track 40,000 (100 x 100 x 4) different time series for that service. This would even exceed the maximum per-service limit of 32,000 for APM Servers with 64GB of main memory.</p>
<p>As a result, the UI will show an entry for “Remaining Transactions” in the Service overview page. This tracks the transaction metrics for a service once it hits the limit. As a result, you may not see all transaction names of your service. It may also be that all distinct transaction names are listed, but that the transaction metrics for some of the instances of that service are combined in the “Remaining Transactions” category.</p>
<p>We’re currently considering restructuring the dimensions for the metrics to avoid that the combination of the dimensions for transaction name and service instance-specific dimensions (such as the host name) lead to an explosion of time series. Stay tuned for more details.</p>
<h2 id="conclusion">Conclusion</h2>
<p>The architectural improvements we’ve delivered in the past releases provide a step-function in terms of the scalability and responsiveness of our UI. Instead of having to aggregate massive amounts of data on-the-fly as users are navigating through the user interface, we pre-aggregate the results for the most common queries as data is coming in. This ensures we have the answers ready before users have even asked their most frequently asked questions, while still being able to answer ad-hoc questions.</p>
<p>We are excited to continue supporting our community members as they push boundaries on their growth journey, providing them with a powerful and mature platform that can effortlessly handle the demands of the largest workloads. Elastic is committed to its mission to enable everyone to find the answers that matter. From all data. In real time. At scale.</p>
<p><em>The release and timing of any features or functionality described in this post remain at Elastic's sole discretion. Any features or functionality not currently available may not be delivered on time or at all.</em></p>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/apm-ui-performance-continuous-rollups-service-metrics</link>
    <guid isPermaLink="false">apm-ui-performance-continuous-rollups-service-metrics</guid>
    <category><![CDATA[APM]]></category>
    <category><![CDATA[Metrics]]></category>
    <category><![CDATA[Data Management]]></category>
    <dc:creator><![CDATA[Felix Barnsteiner,Yngrid Coello,Dario Gieselaar,Carson Ip]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt569decf8bd6851af/6a85cbf2342d6985a621b0fd/elastic-blog-header-ui.png" length="0" type="image/png"/>
    <pubDate>Thu, 29 Jun 2023 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Simplifying log data management: Harness the power of flexible routing with Elastic]]></title>
    <description><![CDATA[The reroute processor, available as of Elasticsearch 8.8, allows customizable rules for routing documents, such as logs, into data streams for better control of processing, retention, and permissions with examples that you can try on your own.]]></description>
    <content:encoded><![CDATA[<p>In Elasticsearch 8.8, we’re introducing the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/reroute-processor.html">reroute processor</a> in technical preview that makes it possible to send documents, such as logs, to different <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/data-streams.html">data streams</a>, according to flexible routing rules. When using Elastic Observability, this gives you more granular control over your data with regard to retention, permissions, and processing with all the potential benefits of the <a href="https://www.elastic.co/blog/an-introduction-to-the-elastic-data-stream-naming-scheme">data stream naming scheme</a>. While optimized for data streams, the reroute processor also works with classic indices. This blog post contains examples on how to use the reroute processor that you can try on your own by executing the snippets in the <a href="https://www.elastic.co/guide/en/kibana/current/console-kibana.html">Kibana dev tools</a>.</p>
<p>Elastic Observability offers a wide range of <a href="https://www.elastic.co/integrations/data-integrations?solution=observability">integrations</a> that help you to monitor your applications and infrastructure. These integrations are added as policies to <a href="https://www.elastic.co/guide/en/fleet/current/elastic-agent-installation.html">Elastic agents</a>, which help ingest telemetry into Elastic Observability. Several examples of these integrations include the ability to ingest logs from systems that send a stream of logs from different applications, such as <a href="https://www.elastic.co/guide/en/kinesis/current/aws-firehose-setup-guide.html">Amazon Kinesis Data Firehose</a>, <a href="https://docs.elastic.co/en/integrations/kubernetes">Kubernetes container logs</a>, and <a href="https://docs.elastic.co/integrations/tcp">syslog</a>. One challenge is that these multiplexed log streams are sending data to the same Elasticsearch data stream, such as logs-syslog-default. This makes it difficult to create parsing rules in ingest pipelines and dashboards for specific technologies, such as the ones from the <a href="https://docs.elastic.co/en/integrations/nginx">Nginx</a> and <a href="https://docs.elastic.co/en/integrations/apache">Apache</a> integrations. That’s because in Elasticsearch, in combination with the <a href="https://www.elastic.co/blog/an-introduction-to-the-elastic-data-stream-naming-scheme">data stream naming scheme</a>, the processing and the schema are both encapsulated in a data stream.</p>
<p>The reroute processor helps you tease apart data from a generic data stream and send it to a more specific one. You may use that mechanism to send logs to a data stream that is set up by the Nginx integration, for example, so that the logs are parsed with that integration and you can use the integration’s prebuilt dashboards or create custom ones with the fields, such as the url, the status code, and the response time that the Nginx pipeline has parsed out of the Nginx log message. You can also split out/separate regular Nginx logs and errors with the reroute processor, providing further separation ability and categorization of logs.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt512e74af45a03dab/6a85cdff99083f664140fa2d/blog-elastic-routing-pipeline.png" alt="routing pipeline" /></p>
<h2 id="exampleusecase">Example use case</h2>
<p>To use the reroute processor, first:</p>
<ol>
<li><p>Ensure you are on Elasticsearch 8.8</p></li>
<li><p>Ensure you have permissions to manage indices and data streams</p></li>
<li><p>If you don’t already have an account on <a href="https://cloud.elastic.co/registration?fromURI=/home">Elastic Cloud</a>, sign up for one</p></li>
</ol>
<p>Next, you’ll need to <a href="https://www.elastic.co/guide/en/elasticsearch/reference/master/set-up-a-data-stream.html">set up a data stream</a> and create a custom Elasticsearch <a href="https://www.elastic.co/guide/en/elasticsearch/reference/master/ingest.html">ingest pipeline</a> that is called as the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest.html#set-default-pipeline">default pipeline</a>. Below we go through this step by step for the “mydata” data set that we’ll simulate ingesting container logs into. We start with a basic example and extend it from there.</p>
<p>The following steps should be utilized in the Elastic console, which is found at <strong>Management -&gt; Dev tools -&gt; Console</strong>. First, we need an an ingest pipeline and a template for the data stream:</p>
<pre><code>PUT _ingest/pipeline/logs-mydata
{
  "description": "Routing for mydata",
  "processors": [
    {
      "reroute": {
      }
    }
  ]
}
</code></pre>
<p>This creates an ingest pipeline with an empty reroute processor. To make use of it, we need an index template:</p>
<pre><code>PUT _index_template/logs-mydata
{
  "index_patterns": [
    "logs-mydata-*"
  ],
  "data_stream": {},
  "priority": 200,
  "template": {
    "settings": {
      "index.default_pipeline": "logs-mydata"
    },
    "mappings": {
      "properties": {
        "container.name": {
          "type": "keyword"
        }
      }
    }
  }
}
</code></pre>
<p>The above template is applied to all data that is shipped to logs-mydata-*. We have mapped container.name as a keyword, as this is the field we will be using for routing later on. Now, we send a document to the data stream and it will be ingested into logs-mydata-default:</p>
<pre><code>POST logs-mydata-default/_doc
{
  "@timestamp": "2023-05-25T12:26:23+00:00",
  "container": {
    "name": "foo"
  }
}
</code></pre>
<p>We can check that it was ingested with the command below, which will show 1 result.</p>
<pre><code>GET logs-mydata-default/_search
</code></pre>
<p>Without modifying the routing processor, this already allows us to route documents. As soon as the reroute processor is specified, it will look for data_stream.dataset and data_stream.namespace fields by default and will send documents to the corresponding data stream, according to the <a href="https://www.elastic.co/blog/an-introduction-to-the-elastic-data-stream-naming-scheme">data stream naming scheme</a> logs-\&lt;dataset&gt;-\&lt;namespace&gt;. Let’s try this out:</p>
<pre><code>POST logs-mydata-default/_doc
{
  "@timestamp": "2023-03-30T12:27:23+00:00",
  "container": {
"name": "foo"
  },
  "data_stream": {
    "dataset": "myotherdata"
  }
}
</code></pre>
<p>As can be seen with the GET logs-mydata-default/_search command, this document ended up in the logs-myotherdata-default data stream. But instead of using default rules, we want to create our own rules for the field container.name. If the field is container.name = foo, we want to send it to logs-foo-default. For this we modify our routing pipeline:</p>
<pre><code>PUT _ingest/pipeline/logs-mydata
{
  "description": "Routing for mydata",
  "processors": [
    {
      "reroute": {
        "tag": "foo",
        "if" : "ctx.container?.name == 'foo'",
        "dataset": "foo"
      }
    }
  ]
}
</code></pre>
<p>Let's test this with a document:</p>
<pre><code>POST logs-mydata-default/_doc
{
  "@timestamp": "2023-05-25T12:26:23+00:00",
  "container": {
    "name": "foo"
  }
}
</code></pre>
<p>While it would be possible to specify a routing rule for each container name, you can also route by the value of a field in the document:</p>
<pre><code>PUT _ingest/pipeline/logs-mydata
{
  "description": "Routing for mydata",
  "processors": [
    {
      "reroute": {
        "tag": "mydata",
        "dataset": [
          "{{container.name}}",
          "mydata"
        ]
      }
    }
  ]
}
</code></pre>
<p>In this example, we are using a field reference as a routing rule. If the container.name field exists in the document, it will be routed — otherwise it falls back to mydata. This can be tested with:</p>
<pre><code>POST logs-mydata-default/_doc
{
  "@timestamp": "2023-05-25T12:26:23+00:00",
  "container": {
    "name": "foo1"
  }
}

POST logs-mydata-default/_doc
{
  "@timestamp": "2023-05-25T12:26:23+00:00",
  "container": {
    "name": "foo2"
  }
}
</code></pre>
<p>This creates the data streams logs-foo1-default and logs-foo2-default.</p>
<p><em>NOTE: There is currently a limitation in the processor that requires the fields specified in a <code>{{field.reference}}</code> to be in a nested object notation. A dotted field name does not currently work. Also, you’ll get errors when the document contains dotted field names for any</em> <em>data_stream.*</em> <em>field. This limitation will be</em> <a href="https://github.com/elastic/elasticsearch/pull/96243"><em>fixed</em></a> <em>in 8.8.2 and 8.9.0.</em></p>
<h2 id="apikeys">API keys</h2>
<p>When using the reroute processor, it is important that the API keys specified have permissions for the source and target indices. For example, if a pattern is used for routing from logs-mydata-default, the API key must have write permissions for <code>logs-*-*</code> as data could end up in any of these indices (see example further down).</p>
<p>We’re currently <a href="https://github.com/elastic/integrations/issues/5989">working</a> <a href="https://github.com/elastic/integrations/issues/6255">on</a> extending the API key permissions for our <a href="https://www.elastic.co/integrations/data-integrations">integrations</a> so that they allow for routing by default if you’re running a Fleet-managed Elastic Agent.</p>
<p>If you’re using a standalone Elastic Agent, or any other shipper, you can use this as a template to create your API key:</p>
<pre><code>POST /_security/api_key
{
  "name": "ingest_logs",
  "role_descriptors": {
    "ingest_logs": {
      "cluster": [
        "monitor"
      ],
      "indices": [
        {
          "names": [
            "logs-*-*"
          ],
          "privileges": [
            "auto_configure",
            "create_doc"
          ]
        }
      ]
    }
  }
}
</code></pre>
<h2 id="futureplans">Future plans</h2>
<p>In Elasticsearch 8.8, the reroute processor was released in technical preview. The plan is to adopt this in our data sink integrations like syslog, k8s, and others. Elastic will provide default routing rules that just work out of the box, but it will also be possible for users to add their own rules. If you are using our integrations, follow <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest.html#pipelines-for-fleet-elastic-agent">this guide</a> on how to add a custom ingest pipeline.</p>
<h2 id="tryitout">Try it out!</h2>
<p>This blog post has shown some sample use cases for document based routing. Try it out on your data by adjusting the commands for index templates and ingest pipelines to your own data, and get started with <a href="https://cloud.elastic.co/registration?fromURI=/home">Elastic Cloud</a> through a 7-day free trial. Let us know via <a href="https://ela.st/reroute-feedback">this feedback form</a> how you’re planning to use the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/reroute-processor.html">reroute processor</a> and whether you have suggestions for improvement.</p>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/simplifying-log-data-management-flexible-routing</link>
    <guid isPermaLink="false">simplifying-log-data-management-flexible-routing</guid>
    <category><![CDATA[Data Management]]></category>
    <category><![CDATA[Logs Analytics]]></category>
    <dc:creator><![CDATA[Felix Barnsteiner,Nicolas Ruflin]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc40c1bcfa3417a50/6a85ce0293ffb91c1ab91481/observability-digital-transformation-1.jpg" length="0" type="image/jpeg"/>
    <pubDate>Tue, 13 Jun 2023 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>