<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Elastic Observability Labs - Elastic Architecture Enhancements</title>
        <link>https://www.elastic.co/observability-labs</link>
        <description>Trusted security news &amp; research from the team at Elastic.</description>
        <lastBuildDate>Wed, 22 Apr 2026 02:08:57 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Elastic Observability Labs - Elastic Architecture Enhancements</title>
            <url>https://www.elastic.co/observability-labs/assets/observability-labs-thumbnail.png</url>
            <link>https://www.elastic.co/observability-labs</link>
        </image>
        <copyright>© 2026. Elasticsearch B.V. All Rights Reserved</copyright>
        <item>
            <title><![CDATA[How to cut Elasticsearch log storage costs with LogsDB]]></title>
            <link>https://www.elastic.co/observability-labs/blog/elasticsearch-logsdb-index-mode-storage-savings</link>
            <guid isPermaLink="false">elasticsearch-logsdb-index-mode-storage-savings</guid>
            <pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Learn how to enable LogsDB index mode in Elasticsearch and measure real storage savings. We compare a standard index against a LogsDB index using Apache logs and show how much storage you can reclaim.]]></description>
            <content:encoded><![CDATA[<p>LogsDB is a specialized Elasticsearch index mode that gives you full functionality at a fraction of the storage cost. Your Kibana dashboards, searches, alerts, and visualizations all continue to work exactly as before. No data is discarded. No queries need to be updated. No workflows break. It is one setting, and everything else gets cheaper.</p>
<p>In benchmarks, LogsDB brought a dataset from <strong>162.7 GB down to 39.4 GB</strong> — a <strong>76% reduction in storage</strong>. You can explore the full nightly benchmark results at <a href="https://elasticsearch-benchmarks.elastic.co/#tracks/logsdb/nightly/default/90d">elasticsearch-benchmarks.elastic.co</a>.</p>
<p>In this tutorial you'll reproduce the experiment yourself using Kibana Dev Tools and an Apache logs dataset. You'll create two identical indices, ingest the same documents into both, and measure the storage difference with the <code>_stats</code> API. By the end, you'll see a 44% reduction on your test data — and understand exactly why production numbers push even higher.</p>
<blockquote>
<p><strong>Already on Elasticsearch 9.2+?</strong> Any data stream with a <code>logs-</code> prefix already uses LogsDB by default. Jump to <a href="#what-about-your-existing-logs">What about your existing logs?</a> to verify your setup.</p>
</blockquote>
<blockquote>
<p><strong>Want the full picture?</strong> For the engineering history behind these savings — how Lucene doc values, synthetic <code>_source</code>, index sorting, and ZSTD were developed and stacked over twelve years — see <a href="https://www.elastic.co/observability-labs/blog/elasticsearch-logsdb-storage-evolution"><em>Elasticsearch over the years: how LogsDB cuts index size by up to 75%</em></a>.</p>
</blockquote>
<h2>Prerequisites</h2>
<ul>
<li>Elasticsearch 8.17+ cluster, Elastic Cloud deployment, or Serverless</li>
<li>Kibana with Dev Tools access</li>
<li>Some logs</li>
<li>Basic familiarity with running API calls in Kibana Dev Tools</li>
</ul>
<h2>How LogsDB saves storage</h2>
<p>LogsDB stacks three mechanisms to achieve its storage reduction:</p>
<ul>
<li><strong>Index sorting</strong> — documents are sorted by <code>host.name</code> then <code>@timestamp</code>, grouping similar log lines so compression codecs find far more repeated patterns. Sorting alone accounts for roughly 30% of the savings.</li>
<li><strong>ZSTD compression with delta/GCD/run-length encoding</strong> — <code>best_compression</code> switches from LZ4 to Zstandard and applies numeric codecs to each doc values column. The standard index in this tutorial uses LZ4, so part of what you're measuring is the full package LogsDB delivers automatically.</li>
<li><strong>Synthetic <code>_source</code></strong> — Elasticsearch skips storing the raw JSON blob entirely and reconstructs <code>_source</code> on demand from doc values, adding another 20–40% of savings on top.</li>
</ul>
<blockquote>
<p><strong>Synthetic <code>_source</code> trade-offs:</strong> Field ordering in returned documents may differ from the original, and some edge cases around multi-value array fields behave differently. For most log analytics workloads these differences are invisible, but check the <a href="#next-steps">synthetic <code>_source</code> documentation</a> before enabling it in latency-sensitive applications.</p>
</blockquote>
<p>For a deep dive into the architecture behind each mechanism, see <a href="https://www.elastic.co/observability-labs/blog/elasticsearch-logsdb-storage-evolution"><em>Elasticsearch over the years: how LogsDB cuts index size by up to 75%</em></a>.</p>
<p>Let's now walk through the steps you can take to enable LogsDB and measure the storage savings.</p>
<h2>Step 1: Collect logs with Elastic Agent</h2>
<p>The recommended way to ingest Apache logs into Elasticsearch is through Elastic Agent with the Apache integration. It handles collection, parsing, ECS field mapping, and routing automatically.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/elasticsearch-logsdb-index-mode-storage-savings/integration.png" alt="Elastic Agent Apache integration setup in Kibana" /></p>
<p>Browse all available integrations in the <a href="https://www.elastic.co/integrations">Elastic integrations catalog</a>.</p>
<p>Once the Agent is collecting logs and routing them to <code>logs-apache.access-*</code>, move to the next step.</p>
<h2>Step 2: Create the two indices</h2>
<p>All commands in this tutorial are run in <strong>Kibana Dev Tools</strong>.</p>
<p>Create one standard index and one LogsDB index with identical field mappings. The only difference is <code>&quot;index.mode&quot;: &quot;logsdb&quot;</code>.</p>
<p><strong>Standard index:</strong></p>
<pre><code class="language-json">PUT /apache-standard
{
  &quot;mappings&quot;: {
    &quot;properties&quot;: {
      &quot;@timestamp&quot;:                  { &quot;type&quot;: &quot;date&quot; },
      &quot;host.name&quot;:                   { &quot;type&quot;: &quot;keyword&quot; },
      &quot;http.request.method&quot;:         { &quot;type&quot;: &quot;keyword&quot; },
      &quot;url.path&quot;:                    { &quot;type&quot;: &quot;keyword&quot; },
      &quot;http.version&quot;:                { &quot;type&quot;: &quot;keyword&quot; },
      &quot;http.response.status_code&quot;:   { &quot;type&quot;: &quot;integer&quot; },
      &quot;http.response.bytes&quot;:         { &quot;type&quot;: &quot;integer&quot; },
      &quot;http.request.referrer&quot;:       { &quot;type&quot;: &quot;keyword&quot; },
      &quot;user_agent.original&quot;:         { &quot;type&quot;: &quot;keyword&quot; }
    }
  }
}
</code></pre>
<p><strong>LogsDB index:</strong></p>
<pre><code class="language-json">PUT /apache-logsdb
{
  &quot;settings&quot;: {
    &quot;index.mode&quot;: &quot;logsdb&quot;
  },
  &quot;mappings&quot;: {
    &quot;properties&quot;: {
      &quot;@timestamp&quot;:                  { &quot;type&quot;: &quot;date&quot; },
      &quot;host.name&quot;:                   { &quot;type&quot;: &quot;keyword&quot; },
      &quot;url.path&quot;:                    { &quot;type&quot;: &quot;keyword&quot; },
      &quot;http.request.method&quot;:         { &quot;type&quot;: &quot;keyword&quot; },
      &quot;http.version&quot;:                { &quot;type&quot;: &quot;keyword&quot; },
      &quot;http.response.status_code&quot;:   { &quot;type&quot;: &quot;integer&quot; },
      &quot;http.response.bytes&quot;:         { &quot;type&quot;: &quot;integer&quot; },
      &quot;http.request.referrer&quot;:       { &quot;type&quot;: &quot;keyword&quot; },
      &quot;user_agent.original&quot;:         { &quot;type&quot;: &quot;keyword&quot; }
    }
  }
}
</code></pre>
<p>That single <code>&quot;index.mode&quot;: &quot;logsdb&quot;</code> line activates all three storage mechanisms. Elasticsearch enables these additional settings behind the scenes — you don't set any of them manually:</p>
<pre><code class="language-json">{
  &quot;index.sort.field&quot;:              [&quot;host.name&quot;, &quot;@timestamp&quot;],
  &quot;index.sort.order&quot;:              [&quot;asc&quot;, &quot;desc&quot;],
  &quot;index.codec&quot;:                   &quot;best_compression&quot;,
  &quot;index.mapping.ignore_malformed&quot;: true,
  &quot;index.mapping.ignore_above&quot;:    8191
}
</code></pre>
<h2>Step 3: Reindex the logs</h2>
<p>Use the <code>_reindex</code> API to copy the same documents into both test indices:</p>
<pre><code class="language-json">POST /_reindex
{
  &quot;source&quot;: { &quot;index&quot;: &quot;logs-apache.access-*&quot; },
  &quot;dest&quot;:   { &quot;index&quot;: &quot;apache-standard&quot; }
}

POST /_reindex
{
  &quot;source&quot;: { &quot;index&quot;: &quot;logs-apache.access-*&quot; },
  &quot;dest&quot;:   { &quot;index&quot;: &quot;apache-logsdb&quot; }
}
</code></pre>
<p>Both indices now hold identical documents, so the storage comparison in the next step reflects only the index mode difference.</p>
<h2>Step 4: Force merge for a fair comparison</h2>
<p>Before measuring, force merge both indices to a single segment:</p>
<pre><code>POST /apache-standard/_forcemerge?max_num_segments=1

POST /apache-logsdb/_forcemerge?max_num_segments=1
</code></pre>
<p>These calls block until the merge finishes. Wait for both responses before continuing.</p>
<p><strong>Why this matters:</strong> Elasticsearch writes data into multiple Lucene segments before merging them in the background. Measuring mid-merge gives artificially inflated numbers because each segment is compressed independently. Forcing a single segment shows the real steady-state storage footprint you'd see in a mature production index.</p>
<blockquote>
<p><strong>Only run <code>_forcemerge</code> on indices that are no longer being written to.</strong> Force merging an index that is still receiving writes is resource-intensive and can impact ingestion performance. In production, you can use <a href="https://www.elastic.co/docs/manage-data/lifecycle/index-lifecycle-management">Index Lifecycle Management (ILM)</a> to automate force merges as part of the warm or cold phase, once an index is rolled over and no longer actively ingested into.</p>
</blockquote>
<h2>Step 5: Measure the difference</h2>
<pre><code>GET /apache-standard/_stats?filter_path=indices.*.primaries.store

GET /apache-logsdb/_stats?filter_path=indices.*.primaries.store
</code></pre>
<p>The <code>filter_path</code> parameter keeps the response focused. Look for <code>primaries.store.size_in_bytes</code> in each response.</p>
<p>In our test with Apache log records, the results were:</p>
<table>
<thead>
<tr>
<th>Index</th>
<th>Documents</th>
<th>Size</th>
</tr>
</thead>
<tbody>
<tr>
<td>apache-standard</td>
<td>111,818</td>
<td>15.37 MB</td>
</tr>
<tr>
<td>apache-logsdb</td>
<td>111,818</td>
<td>8.6 MB</td>
</tr>
<tr>
<td><strong>Reduction</strong></td>
<td></td>
<td><strong>44%</strong></td>
</tr>
</tbody>
</table>
<p>To put this in perspective: at 1 TB of log data, LogsDB brings that down to around 560 GB. That's 450 GB saved without any changes to your queries. At production scale with billions of documents and synthetic <code>_source</code> enabled, savings push to 76% — taking 162.7 GB down to 39.4 GB in our benchmark.</p>
<h2>Visualize in Kibana</h2>
<p>To see the storage difference visually, open Kibana and go to <strong>Management → Stack Management → Index Management</strong>. You'll see both indices listed with their current sizes side by side.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/elasticsearch-logsdb-index-mode-storage-savings/index-stats.png" alt="Kibana Index Management showing storage comparison between standard and LogsDB indices" /></p>
<blockquote>
<p><strong>Why Kibana shows larger numbers than <code>_stats</code>:</strong> Kibana Index Management displays the total index size including all replica shards. The <code>_stats</code> query above uses <code>primaries</code> to report primary shards only. The ratio between the two indices remains the same either way.</p>
</blockquote>
<h2>What about your existing logs?</h2>
<h3>Elasticsearch 9.2+ (already enabled by default)</h3>
<p>Since 9.2, any data stream matching the <code>logs-*</code> naming pattern automatically uses LogsDB. You're likely already saving storage without any configuration change.</p>
<p>Verify your existing data streams:</p>
<pre><code>GET /.ds-logs-*/_settings?filter_path=*.settings.index.mode
</code></pre>
<p>If you see <code>&quot;index.mode&quot;: &quot;logsdb&quot;</code> in the responses, you're already getting the savings.</p>
<h3>Elasticsearch 8.x or 9.0–9.1 (enable per data stream via index template)</h3>
<p>For earlier versions, enable LogsDB on a data stream by updating its index template. This affects all new indices created from that template — existing indices are not changed, so the transition is safe and gradual.</p>
<p><strong>Option A — Update an existing template:</strong></p>
<pre><code class="language-json">PUT _index_template/logs-myapp-template
{
  &quot;index_patterns&quot;: [&quot;logs-myapp-*&quot;],
  &quot;data_stream&quot;: {},
  &quot;template&quot;: {
    &quot;settings&quot;: {
      &quot;index.mode&quot;: &quot;logsdb&quot;
    }
  },
  &quot;priority&quot;: 200
}
</code></pre>
<p><strong>Option B — Check and patch an existing integration template:</strong></p>
<p>First, find the template managing your data stream:</p>
<pre><code>GET _index_template/logs-apache*
</code></pre>
<p>Then add the <code>index.mode</code> setting to the <code>template.settings</code> block using a <code>PUT _index_template/&lt;name&gt;</code> call with the full template body including your addition.</p>
<p>After updating the template, the next index rollover will use LogsDB. Trigger a rollover immediately if you don't want to wait:</p>
<pre><code>POST /logs-myapp-default/_rollover
</code></pre>
<p><strong>Upgrading from 8.x to 9.0+:</strong> Existing data streams are not changed automatically. Only new rollovers will use LogsDB. There is no data loss and no reindexing required — the savings accumulate as new indices roll over.</p>
<h2>What about query performance?</h2>
<p>LogsDB does not significantly impact query performance for typical log analytics workloads. The index sorting by <code>host.name</code> and <code>@timestamp</code> can actually <em>improve</em> range query and aggregation performance on those fields, since matching documents are stored adjacently. Queries that don't filter on those fields perform comparably to a standard index.</p>
<p>For indexing throughput data across releases, see the <a href="https://www.elastic.co/observability-labs/blog/elasticsearch-logsdb-storage-evolution#performance-not-just-storage">performance section</a> of the companion article.</p>
<h2>Conclusion</h2>
<p>LogsDB activates with a single <code>&quot;index.mode&quot;: &quot;logsdb&quot;</code> setting and delivers measurable storage savings immediately: 44% in our hands-on test, and 76% (162.7 GB → 39.4 GB) in production benchmarks with synthetic <code>_source</code>. On Elasticsearch 9.2+, <code>logs-*</code> data streams already use LogsDB by default. For 8.x or earlier 9.x clusters, a one-line index template change enables it on your next rollover with no data loss and no reindexing required.</p>
<h2>Next steps</h2>
<ul>
<li><a href="https://www.elastic.co/docs/reference/elasticsearch/index-settings/logsdb">LogsDB index mode documentation</a></li>
<li><a href="https://www.elastic.co/docs/reference/elasticsearch/mapping/synthetic-source">Synthetic <code>_source</code> documentation and limitations</a></li>
<li><a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/logs-data-stream">Configuring a logs data stream</a></li>
<li><a href="https://www.elastic.co/blog/logsdb-index-mode-generally-available">LogsDB GA announcement</a></li>
<li><a href="https://www.elastic.co/blog/elasticsearch-logsdb-tsds-benchmarks">LogsDB and TSDS performance benchmarks</a></li>
</ul>
]]></content:encoded>
            <category>observability-labs</category>
            <enclosure url="https://www.elastic.co/observability-labs/assets/images/elasticsearch-logsdb-index-mode-storage-savings/header.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Elasticsearch over the years — how LogsDB cuts index size by up to 75% at no throughput cost]]></title>
            <link>https://www.elastic.co/observability-labs/blog/elasticsearch-logsdb-storage-evolution</link>
            <guid isPermaLink="false">elasticsearch-logsdb-storage-evolution</guid>
            <pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[By default, Elasticsearch is optimized for retrieval, not storage. LogsDB changes that. Here's the layered architecture behind a 77% index size reduction.]]></description>
            <content:encoded><![CDATA[<p>Elasticsearch was built as a search engine. That heritage has a cost for log storage: every event fans out to multiple on-disk structures, each optimized for retrieval rather than compression. LogsDB changes both. On our nightly benchmark, Enterprise mode produces a 37.5 GB index from the same data that takes 161.9 GB without LogsDB — a 77% reduction from a single setting.</p>
&lt;div align=&quot;center&quot;&gt;
![Standard vs LogsDB storage breakdown](/assets/images/elasticsearch-logsdb-storage-evolution/storage-breakdown-v3-bold@2x.png)
&lt;/div&gt;
<h2>The write overhead</h2>
<p>Lucene, the library underneath, keeps multiple structures for every indexed document:</p>
<ul>
<li>The <strong>inverted index</strong> maps terms to documents. This is what makes text search fast.</li>
<li><strong><code>_source</code></strong> stores the original JSON blob, returned when you fetch a document.</li>
<li><strong>Doc values</strong> store field values in columns for sorting and aggregation.</li>
<li><strong>Points / BKD trees</strong> index numeric and date fields for range queries.</li>
</ul>
<p>The inverted index earns its keep: it's what lets you search a billion log lines by keyword in milliseconds, and there's no cheaper way to build that capability. <code>_source</code> exists to give you back exactly what you indexed: search results and <code>GET</code> requests return this blob directly. The problem is that it stores the full event even though the same field values are already available through doc values and the other structures.</p>
<p>Take a log event with fields like <code>host.name</code>, <code>@timestamp</code>, <code>http.response.status_code</code>, and <code>duration_ms</code>. The entire event is serialized as JSON in <code>_source</code>. The same field values are also written into doc values columns, indexed into the inverted index, and stored in BKD trees for range queries. Same data, multiple structures, each with its own on-disk footprint.</p>
<p>For a search engine where you need fast retrieval across all dimensions, that overhead is a reasonable tradeoff. For logs, where you rarely need the raw JSON and almost never do relevance-ranked search, much of it is pure waste.</p>
&lt;div align=&quot;center&quot;&gt;
![One incoming log event fans out to four on-disk structures](/assets/images/elasticsearch-logsdb-storage-evolution/dual-storage-bold@2x.png)
_One write, four on-disk structures: `_source` (the raw JSON blob), the inverted index, doc values columns, and BKD / points trees for numeric range queries. The same field values end up in multiple places._
&lt;/div&gt;
<h2>Why columnar storage matters for compression</h2>
<p>Doc values are the key to everything LogsDB does. Unlike <code>_source</code>, which stores entire documents as blobs, doc values store each field as a separate column across all documents in a Lucene segment.</p>
<p>Picture a segment with a million log events. The <code>_source</code> representation is a million JSON blobs, one per event, each containing all fields jumbled together. The doc values representation is a set of columns: one column of a million timestamps, one column of a million host names, one column of a million status codes, and so on.</p>
&lt;div align=&quot;center&quot;&gt;
![Row-oriented vs column-oriented storage](/assets/images/elasticsearch-logsdb-storage-evolution/doc-values-columns-bold@2x.png)
_Row-oriented `_source` keeps all fields for each document in one blob — doc0 through doc5 each carry `host.name`, `@timestamp`, `status`, `duration_ms`, and more jumbled together. Column-oriented doc values restructure the same data so all `host.name` values sit in one column, all timestamps in another, all status codes in another. Compression codecs can then run on each contiguous column independently._
&lt;/div&gt;
<p>That columnar layout is what makes per-column compression possible. When all values of <code>http.response.status_code</code> sit in a contiguous column, Lucene can apply codecs that exploit patterns in the sequence.</p>
<p>Delta encoding stores differences between adjacent values instead of full values. GCD encoding finds a common factor and divides everything down. Run-length encoding collapses repeats. Lucene picks the codec per segment and re-evaluates when segments merge.</p>
&lt;div align=&quot;center&quot;&gt;
![Numeric codec pipeline: RAW → DELTA → GCD → BIT-PACK](/assets/images/elasticsearch-logsdb-storage-evolution/numeric-codec-pipeline-bold@2x.png)
_Four sorted `@timestamps` from the same host, compressed in four stages. RAW: four 32-bit integers, 128 bits total. DELTA: store differences instead of full values — base stays, deltas +100, +200, +300 take 59 bits. GCD: divide out the common factor of 100, leaving 1, 2, 3 at 39 bits. BIT-PACK: pack those three small integers into contiguous bit storage, 9 bits freed._
&lt;/div&gt;
<p>But here's the catch: these codecs only work well when adjacent documents have correlated values. Consider the <code>@timestamp</code> column.</p>
<p>If logs arrive from dozens of hosts interleaved randomly, the timestamps in the column jump around. The delta between adjacent values might be +3 seconds, then -47 seconds, then +120 seconds. Delta encoding can't do much with that.</p>
<p>Now consider what happens if you sort by <code>host.name</code> and <code>@timestamp</code> before writing to the segment. All logs from host-A land in a contiguous run, followed by all logs from host-B, and so on. Within each host's run, the timestamps are monotonically increasing and the deltas are predictable.</p>
<p>Four timestamps from the same host might look like 1706745600, +100s, +200s, +300s. Delta encoding shrinks those to a base value plus three small integers.</p>
<p>GCD encoding finds that 100, 200, 300 are all divisible by 100 and stores 1, 2, 3 instead. Bit-packing then fits those three values into a handful of bits. The same pattern applies to fields like <code>host.name</code>, <code>service.name</code>, or <code>http.response.status_code</code>: within a sorted run, long stretches of identical values collapse to near nothing under run-length encoding.</p>
&lt;div align=&quot;center&quot;&gt;
![Index sorting: arrival order → sorted by host.name → after RLE](/assets/images/elasticsearch-logsdb-storage-evolution/index-sorting-bold@2x.png)
_Five hosts — api-01, api-02, db-01, web-01, web-02 — scattered randomly in arrival order (left). Sorting by `host.name` groups them into five contiguous blocks of eight (center). Run-length encoding collapses each block to a single (value, count) pair — 5 pairs stored instead of 40, the remaining slots freed (right)._
&lt;/div&gt;
<p>Elasticsearch never sorted by default. Documents landed in arrival order, compressed with DEFLATE. We left a lot on the table.</p>
<h2>How we got here: 2012–2026</h2>
<p>Not all of the individual techniques in LogsDB were designed for logs. They were built over twelve years to solve different problems, and LogsDB is what happens when you stack them.</p>
<p><strong>The foundation (2012–2017).</strong> Lucene 4.0 introduced doc values in 2012. By Elasticsearch 5.0 in 2016, they were on by default for all keyword and numeric fields. Lucene 7.0 added sparse doc values, so fields that only appear in some documents don't waste space on every document in the segment. That fixed a significant force-merge bloat problem (up to 10× on sparse fields) and set up the storage model everything else depends on.</p>
&lt;div align=&quot;center&quot;&gt;
![Dense vs sparse doc values encoding](/assets/images/elasticsearch-logsdb-storage-evolution/sparse-doc-values-bold@2x.png)
_Dense encoding reserves an 8-byte slot per document regardless of presence. Sparse encoding stores only documents that have a value at 12 bytes each (value + doc ID). For `error_code` with 2 of 16 docs populated (12% fill), sparse is 81% smaller: 24 B vs 128 B. For `request_path` at 88% fill, sparse is larger: 168 B vs 128 B. Lucene picks per field; sparse wins below ~67% fill._
&lt;/div&gt;
<p><strong>Incremental wins (2020–2021).</strong> Two smaller changes targeted observability workloads. Dictionary-based stored fields compression deduplicated repetitive string metadata for about a 10% win.</p>
<p>The <code>match_only_text</code> field type dropped term frequencies and positions from the inverted index. Term frequencies are what BM25 uses to score documents by relevance — how often a term appears in a document relative to the rest of the corpus. For log search that signal is meaningless: you don't care whether &quot;timeout&quot; appeared twice or seven times in a log line, you just want to find it. Positions are similar: they're stored so Elasticsearch can do exact phrase matching, but the position data is expensive and phrase queries on logs are rare enough that the tradeoff is worth it. When you do run a phrase query on a <code>match_only_text</code> field, it still works — it just falls back to a slower path that rescores candidates rather than using stored positions directly.</p>
&lt;div align=&quot;center&quot;&gt;
![text vs match_only_text inverted index storage](/assets/images/elasticsearch-logsdb-storage-evolution/match-only-text-bold@2x.png)
_`text` stores each term with its frequency and every position it appears at. `match_only_text` keeps only the doc IDs — enough to find the document, nothing more. The `timeout` term appears twice in this message (positions 1 and 4), which is exactly the kind of data that gets dropped._
&lt;/div&gt;
<p>Dropping frequencies and positions cuts the inverted index for a text field by roughly 40%. The overall index impact in 2021 was only ~10%, which sounds like a poor return on a 40% field-level reduction. The reason is where storage was going at the time: <code>_source</code> was stored in full for every document as a raw JSON blob, doc values were uncompressed and unsorted, and nothing was using ZSTD. The <code>message</code> field's inverted index was a small slice of a much larger, poorly-compressed whole. As the next five years of work addressed those other structures, the same 40% field-level savings became a meaningful fraction of a much smaller total.</p>
<p>Neither change was decisive on its own, but they established that log-specific storage optimization was worth pursuing.</p>
<p><strong>The TSDB turning point (April 2023).</strong> This is where the story really starts. We shipped synthetic <code>_source</code> and index sorting for time series metrics in Elasticsearch 8.7.</p>
<p>Synthetic source changes the write-and-read contract. At write time, we skip storing the raw JSON blob entirely. At read time, when a query needs to return the original document, we reconstruct it by reading each field's value out of doc values and stored fields and assembling them back into JSON. The result is functionally equivalent to the original <code>_source</code> (with minor differences like field ordering), but we never stored the blob.</p>
<p>Index sorting groups documents by dimension fields and timestamp before writing to disk. Together, synthetic source and index sorting cut metrics storage by up to 70%.</p>
<p>That result told us something important: the same architecture could work for logs.</p>
&lt;div align=&quot;center&quot;&gt;
![Standard _source vs synthetic _source](/assets/images/elasticsearch-logsdb-storage-evolution/synthetic-source-bold@2x.png)
_Without LogsDB, Elasticsearch writes every log event twice: once as a raw `_source` blob on disk, once into doc values columns. LogsDB skips the blob entirely. At read time, a `GET &lt;index&gt;/_doc/1` request gathers field values from doc values and assembles the document on the fly._
&lt;/div&gt;
<p><strong>The TSDB codec (2024).</strong> In 8.13 and 8.14, we built a custom doc values codec with run-length encoding optimized for sorted consecutive values, PFOR-delta encoding, and cyclic ordinal encoding for multi-valued dimensions. The numbers were striking: <code>kubernetes.pod.name</code> doc values dropped from 110 MB to 7.25 MB in one benchmark. We extended coverage to all numeric and keyword types including <code>ip</code>, <code>scaled_float</code>, and <code>unsigned_long</code>.</p>
<p><strong>LogsDB Tech Preview (August 2024).</strong> In <a href="https://github.com/elastic/elasticsearch/pull/108896">8.15</a>, we combined everything into <code>index.mode: logsdb</code>: host-first sorting, synthetic <code>_source</code>, ZSTD compression, and the TSDB numeric codecs. One decision mattered more than expected: sort order. Sorting by <code>host.name</code> first, then <code>@timestamp</code>, delivers up to ~40% storage reduction. Sorting by timestamp first gives ≤10%. The host-first ordering co-locates documents that share field values, which is exactly what the numeric codecs need.</p>
<p><strong>ZSTD and GA (November–December 2024).</strong> In <a href="https://github.com/elastic/elasticsearch/pull/112665">8.16</a>, we switched <code>best_compression</code> from DEFLATE to ZSTD permanently (level 3, blocks up to 2,048 documents or 240 kB, native bindings via Panama FFI on JDK 21+). ZSTD gave us ~12% smaller stored fields and ~14% higher indexing throughput at the same time, which almost never happens. LogsDB went GA in 8.17.</p>
<p>At GA, we claimed up to 65% storage reduction.</p>
<p><strong>Routing and recovery (April 2025).</strong> In 8.18, <a href="https://github.com/elastic/elasticsearch/pull/116687"><code>route_on_sort_fields</code></a> started routing documents to shards by sort field values instead of <code>_id</code>. Without this optimization, Elasticsearch hashes the <code>_id</code> to pick a shard, so logs from the same host scatter across all shards. With routing on sort fields, logs with similar <code>host.name</code> values land on the same shard. This co-locates similar documents at the shard level, not just within segments, adding ~20% storage reduction at a 1–4% ingest penalty. Routing on sort fields requires auto-generated <code>_id</code>.</p>
&lt;div align=&quot;center&quot;&gt;
![Shard routing: standard, routed, routed + sorted](/assets/images/elasticsearch-logsdb-storage-evolution/shard-routing-bold@2x.png)
_Data stream `.ds-logs-nginx-default-00001` with six hosts across three shards. STANDARD (hashed by `_id`): all host colors scattered randomly. ROUTED (`route_on_sort_fields`): same-host logs land on the same shard, but remain in arrival order within it. ROUTED + SORTED (host-first sort): each shard contains contiguous blocks of a single host — the combination that lets numeric codecs and RLE reach their full potential._
&lt;/div&gt;
<p>We also <a href="https://github.com/elastic/elasticsearch/pull/119110">switched peer recovery to synthetic source reconstruction</a>, eliminating the duplicate <code>_recovery_source</code> blob. In <a href="https://github.com/elastic/elasticsearch/pull/121049">9.0</a>, <code>logs-*-*</code> indices default to LogsDB.</p>
&lt;div align=&quot;center&quot;&gt;
![Index size written: _recovery_source eliminated](/assets/images/elasticsearch-logsdb-storage-evolution/recovery-source-bold@2x.png)
_Nightly synthetic source benchmark, December 2024. Index size written drops 39% — from ~279 GB to ~171 GB — the day peer recovery switches from copying the raw `_recovery_source` blob to reconstructing documents from doc values._
&lt;/div&gt;
<p><strong>Merge and recovery overhaul: 9.1 (July 2025).</strong> We fully eliminated the recovery source. Peer recovery uses batched synthetic reconstruction, cutting write I/O by ~50% and boosting median indexing throughput ~19% over the 8.17 baseline. We replaced up to four separate doc values merge passes with a single pass, cutting background merge CPU by up to 40%. And we swapped <code>_seq_no</code>'s BKD tree for Lucene doc value skippers, halving <code>_seq_no</code> storage.</p>
<p><strong>pattern_text and Failure Store: 9.2–9.3 (October 2025–February 2026).</strong> In <a href="https://github.com/elastic/elasticsearch/pull/124323">9.2</a>, we shipped <code>pattern_text</code> as a Tech Preview: a new field type that decomposes log messages into static templates and dynamic variable parts. A log line like <code>Session opened for user alice from 10.0.1.42 via TLS</code> gets split into the template <code>Session opened for user {} from {} via TLS</code> (stored once, as a template ID) and the variables <code>alice</code>, <code>10.0.1.42</code> (stored per document). For logs with high template repetition, this cuts message field storage by up to 50%. A companion <code>template_id</code> sub-field lets you sort by template, and the LogsDB setting <code>index.logsdb.default_sort_on_message_template</code> enables this automatically. <code>pattern_text</code> <a href="https://github.com/elastic/elasticsearch/pull/135370">went GA in 9.3</a>.</p>
&lt;div align=&quot;center&quot;&gt;
![TEXT vs PATTERN_TEXT field type](/assets/images/elasticsearch-logsdb-storage-evolution/pattern-text-bold@2x.png)
_TEXT stores each log message as a full string per document — eight copies of near-identical blobs. PATTERN_TEXT decomposes them: the shared template `Session opened for user {} from {} via TLS` is stored once with ID T0, and only the variable columns (`user`, `ip`) are stored per document — alice/10.0.1.42, bob/10.0.1.87, carol/10.0.2.11, and so on._
&lt;/div&gt;
<p><code>pattern_text</code> does come with an indexing CPU cost: decomposing each message into template and variables takes more work at write time than storing a raw string. Whether that tradeoff makes sense depends on your dataset and your priorities.</p>
<p>If your log messages follow highly repetitive patterns (structured application logs, Kubernetes events, access logs), the storage wins are large and the CPU overhead is bounded. If your messages are free-form or low-repetition, the compression gains shrink while the CPU cost stays roughly the same.</p>
<p>For data you keep for months or years, the cumulative storage reduction usually makes it worthwhile. For high-cardinality, rapidly changing messages where storage isn't the constraint, it may not be.</p>
<p>9.3 also brought compression for binary doc values, making <code>wildcard</code> field types significantly more storage-efficient. Internally, wildcard fields store an inverted index of trigrams in a binary doc values column; that column is now compressed with Zstandard instead of being stored raw. In one benchmark, a URL field dropped from 2.92 GB to 1.12 GB, more than 60% compression. If you use <code>wildcard</code> fields heavily, the gain is automatic with no mapping changes needed.</p>
<p>Also in 9.3, skip lists for <code>@timestamp</code> and <code>host.name</code> became available as an opt-in for LogsDB. Skip lists let Elasticsearch jump ahead in a doc values column without reading every entry, which speeds up time-range queries on large segments. Other index modes have skip lists disabled by default; in LogsDB you can enable them selectively for the fields you range-query most.</p>
<p>Also in 9.3, the <a href="https://www.elastic.co/docs/manage-data/data-store/data-streams/failure-store">Failure Store</a> <a href="https://github.com/elastic/elasticsearch/pull/131261">became enabled by default</a> for <code>logs-*-*</code> data streams. Failed documents (mapping conflicts, ingest pipeline errors) now land in dedicated <code>::failures</code> indices instead of being rejected, which means LogsDB's strict synthetic source requirements are less likely to cause silent data loss during migration.</p>
<h2>Performance, not just storage</h2>
<p>LogsDB started as a storage optimization, and the early releases came with a throughput cost — sorting, synthetic source reconstruction, and ZSTD all add work at write time. Over two years of releases, we clawed that back. Indexing throughput is now on par with what users had before enabling LogsDB. You get the storage reduction without giving up the ingest rate you were used to.</p>
&lt;div align=&quot;center&quot;&gt;
![LogsDB throughput and storage on disk over time](/assets/images/elasticsearch-logsdb-storage-evolution/performance-over-time-bold@2x.png)
_Throughput (teal) has climbed from ~25k to ~35k docs/s since the Tech Preview. Storage on disk (blue) has dropped from ~65 GB to ~36 GB on the same benchmark dataset. Both curves move in the right direction, driven by the same layered releases: ZSTD in 8.16, routing optimization in 8.18, the merge and recovery overhaul in 9.1. Live numbers at [elasticsearch-benchmarks.elastic.co](https://elasticsearch-benchmarks.elastic.co/#tracks/logsdb/nightly/default/90d)._
&lt;/div&gt;
<p>The two trends compound each other. Less storage means fewer segments to merge, which frees CPU for indexing. Synthetic source reconstruction is cheaper to compute than it is to store and replicate the raw blob. Each release that shrank the index also reduced background I/O, which fed back into throughput.</p>
<p>The practical result: if you were running standard Elasticsearch for log ingestion two years ago, the throughput you had then is roughly what LogsDB delivers now — with a 50–75% smaller index alongside it.</p>
<h2>How to enable it</h2>
<p>As of 9.0, <code>logs-*-*</code> data streams default to LogsDB automatically. If your data streams match that pattern, you're already using it.</p>
<blockquote>
<p><strong>Want a hands-on walkthrough?</strong> <a href="https://www.elastic.co/observability-labs/blog/elasticsearch-logsdb-index-mode-storage-savings"><em>Cut Elasticsearch log storage costs by 76% with LogsDB</em></a> walks through creating two indices, reindexing, and measuring the difference with the <code>_stats</code> API — including version-specific enable instructions for 8.x clusters.</p>
</blockquote>
<p>For other index patterns, set it in your template:</p>
<pre><code class="language-json">PUT _index_template/logs-template
{
  &quot;index_patterns&quot;: [&quot;logs-*&quot;],
  &quot;template&quot;: {
    &quot;settings&quot;: {
      &quot;index.mode&quot;: &quot;logsdb&quot;
    }
  }
}
</code></pre>
<p>Synthetic <code>_source</code> turns on automatically with <code>index.mode: logsdb</code>.</p>
<p>For the routing optimization (8.18+), add one more setting:</p>
<pre><code class="language-json">PUT _index_template/logs-template
{
  &quot;index_patterns&quot;: [&quot;logs-*&quot;],
  &quot;template&quot;: {
    &quot;settings&quot;: {
      &quot;index.mode&quot;: &quot;logsdb&quot;,
      &quot;index.logsdb.route_on_sort_fields&quot;: true
    }
  }
}
</code></pre>
<p>This routes shards by sort field values instead of <code>_id</code>, adding ~20% storage reduction at a 1–4% ingestion penalty. It requires at least two sort fields beyond <code>@timestamp</code> and auto-generated <code>_id</code>.</p>
<p>Switching an existing index to LogsDB requires a reindex. So does rolling back. There's no in-place conversion, so try it on new data streams first.</p>
<p>Storage improves further as segments merge — freshly written data compresses well, but merged segments compress even better.</p>
<h2>What's next</h2>
<p>Elasticsearch still carries some structural overhead from its search engine roots. <code>_id</code> and <code>_seq_no</code> are two examples: both consume meaningful disk space (on small documents they can account for more than half the index size), but neither is essential for log analytics workloads.</p>
<p>We've already taken the first step for TSDB: <a href="https://github.com/elastic/elasticsearch/pull/144026">PR #144026</a> eliminated stored <code>_id</code> bytes from TSDB indices by reconstructing the field on the fly from doc values, the same approach synthetic <code>_source</code> uses. We're exploring the same direction for LogsDB.</p>
<p><strong>9.4 and beyond.</strong> The architecture still has room to improve, and we're on it.</p>
<p>For the full reference, see the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/logs-data-stream.html">logs data stream documentation</a>.</p>
]]></content:encoded>
            <category>observability-labs</category>
            <enclosure url="https://www.elastic.co/observability-labs/assets/images/elasticsearch-logsdb-storage-evolution/elasticsearch-logsdb-storage-evolution.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[How to use Elasticsearch and Time Series Data Streams for observability metrics]]></title>
            <link>https://www.elastic.co/observability-labs/blog/time-series-data-streams-observability-metrics</link>
            <guid isPermaLink="false">time-series-data-streams-observability-metrics</guid>
            <pubDate>Thu, 04 May 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[With Time Series Data Streams (TSDS), Elasticsearch introduces optimized storage for metrics time series. Check out how we use it for Elastic Observability.]]></description>
            <content:encoded><![CDATA[<p>Elasticsearch is used for a wide variety of data types — one of these is metrics. With the introduction of Metricbeat many years ago and later our APM Agents, the metric use case has become more popular. Over the years, Elasticsearch has made many improvements on how to handle things like metrics aggregations and sparse documents. At the same time, <a href="https://www.elastic.co/guide/en/kibana/current/tsvb.html">TSVB visualizations</a> were introduced to make visualizing metrics easier. One concept that was missing that exists for most other metric solutions is the concept of time series with dimensions.</p>
<p>Mid 2021, the Elasticsearch team <a href="https://github.com/elastic/elasticsearch/issues/74660">embarked</a> on making Elasticsearch a much better fit for metrics. The team created <a href="https://www.elastic.co/guide/en/elasticsearch/reference/master/tsds.html">Time Series Data Streams (TSDS)</a>, which were released in 8.7 as generally available (GA).</p>
<p>This blog post dives into how TSDS works and how we use it in Elastic Observability, as well as how you can use it for your own metrics.</p>
<h2>A quick introduction to TSDS</h2>
<p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/master/tsds.html">Time Series Data Streams (TSDS)</a> are built on top of data streams in Elasticsearch that are optimized for time series. To create a data stream for metrics, an additional setting on the data stream is needed. As we are using data streams, first an Index Template has to be created:</p>
<pre><code class="language-json">PUT _index_template/metrics-laptop
{
  &quot;index_patterns&quot;: [
    &quot;metrics-laptop-*&quot;
  ],
  &quot;data_stream&quot;: {},
  &quot;priority&quot;: 200,
  &quot;template&quot;: {
    &quot;settings&quot;: {
      &quot;index.mode&quot;: &quot;time_series&quot;
    },
    &quot;mappings&quot;: {
      &quot;properties&quot;: {
        &quot;host.name&quot;: {
          &quot;type&quot;: &quot;keyword&quot;,
          &quot;time_series_dimension&quot;: true
        },
        &quot;packages.sent&quot;: {
          &quot;type&quot;: &quot;integer&quot;,
          &quot;time_series_metric&quot;: &quot;counter&quot;
        },
        &quot;memory.usage&quot;: {
          &quot;type&quot;: &quot;double&quot;,
          &quot;time_series_metric&quot;: &quot;gauge&quot;
        }
      }
    }
  }
}
</code></pre>
<p>Let's have a closer look at this template. On the top part, we mark the index pattern with metrics-laptop-*. Any pattern can be selected, but it is recommended to use the <a href="https://www.elastic.co/blog/an-introduction-to-the-elastic-data-stream-naming-scheme">data stream naming scheme</a> for all your metrics. The next section sets the &quot;index.mode&quot;: &quot;time_series&quot; in combination with making sure it is a data_stream: &quot;data_stream&quot;: {}.</p>
<h3>Dimensions</h3>
<p>Each time series data stream needs at least one dimension. In the example above, host.name is set as a dimension field with &quot;time_series_dimension&quot;: true. You can have up to 16 dimensions by default. Not every dimension must show up in each document. The dimensions define the time series. The general rule is to pick fields as dimensions that uniquely identify your time series. Often this is a unique description of the host/container, but for some metrics like disk metrics, the disk id is needed in addition. If you are curious about default recommended dimensions, have a look at this <a href="https://github.com/elastic/ecs/pull/2172">ECS contribution</a> with dimension properties.</p>
<h2>Reduced storage and increased query speed</h2>
<p>At this point, you already have a functioning time series data stream. Setting the index mode to time series automatically turns on <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-source-field.html#synthetic-source">synthetic source</a>. By default, Elasticsearch typically duplicates data three times:</p>
<ul>
<li><a href="https://en.wikipedia.org/wiki/Column-oriented_DBMS#Row-oriented_systems">row-oriented storage</a> (_source field)</li>
<li><a href="https://en.wikipedia.org/wiki/Column-oriented_DBMS#Column-oriented_systems">column-oriented storage</a> (doc_values: true for aggregations)</li>
<li>indices (index: true for filtering and search)</li>
</ul>
<p>With synthetic source, the _source field is not persisted; instead, it is reconstructed from the doc values. Especially in the metrics use case, there are little benefits to keeping the source.</p>
<p>Not storing it means a significant reduction in storage. Time series data streams sort the data based on the dimensions and the time stamp. This means data that is usually queried together is stored together, which speeds up query times. It also means that the data points for a single time series are stored alongside each other on disk. This enables further compression of the data as the rate at which a counter increases is often relatively constant.</p>
<h2>Metric types</h2>
<p>But to benefit from all the advantages of TSDS, the field properties of the metrics fields must be extended with the <code>time_series_metric: {type}</code>. Several <a href="https://www.elastic.co/guide/en/elasticsearch/reference/master/tsds.html#time-series-metric">types are supported</a> — as an example, gauge and counter were used above. Giving Elasticsearch knowledge about the metric type allows Elasticsearch to offer more optimized queries for the different types and reduce storage usage further.</p>
<p>When you create your own templates for data streams under the <a href="https://www.elastic.co/blog/an-introduction-to-the-elastic-data-stream-naming-scheme">data stream naming scheme</a>, it is important that you set &quot;priority&quot;: 200 or higher, as otherwise the built-in default template will apply.</p>
<h2>Ingest a document</h2>
<p>Ingesting a document into a TSDS isn't in any way different from ingesting documents into Elasticsearch. You can use the following commands in Dev Tools to add a document, and then search for it and also check out the mappings. Note: You have to adjust the @timestamp field to be close to your current date and time.</p>
<pre><code class="language-bash"># Add a document with `host.name` as the dimension
POST metrics-laptop-default/_doc
{
  # This timestamp neesd to be adjusted to be current
  &quot;@timestamp&quot;: &quot;2023-03-30T12:26:23+00:00&quot;,
  &quot;host.name&quot;: &quot;ruflin.com&quot;,
  &quot;packages.sent&quot;: 1000,
  &quot;memory.usage&quot;: 0.8
}

# Search for the added doc, _source will show up but is reconstructed
GET metrics-laptop-default/_search

# Check out the mappings
GET metrics-laptop-default
</code></pre>
<p>If you do search, it still shows _source but this is reconstructed from the doc values. The additional field added above is @timestamp. This is important as it is a required field for any data stream.</p>
<h2>Why is this all important for Observability?</h2>
<p>One of the advantages of the Elastic Observability solution is that in a single storage engine, all signals are brought together in a single place. Users can query logs, metrics, and traces together without having to jump from one system to another. Because of this, having a great storage and query engine not only for logs but also metrics is key for us.</p>
<h2>Usage of TSDS in integrations</h2>
<p>With <a href="https://www.elastic.co/integrations/data-integrations">integrations</a>, we give our users an out of the box experience to integrate with their infrastructure and services. If you are using our integrations, eventually you will automatically get all the benefits of TSDS for your metrics assuming you are on version 8.7 or newer.</p>
<p>Currently we are working through the list of our integration packages, add the dimensions, metric type fields and then turn on TSDS for the metrics data streams. What this means is as soon as the package has all properties enabled, the only thing you have to do is upgrade the integration and everything else will happen automatically in the background.</p>
<p>To visualize your time series in Kibana, use <a href="https://www.elastic.co/guide/en/kibana/current/lens.html">Lens</a>, which has native support built in for TSDS.</p>
<h2>Learn more</h2>
<p>If you switch over to TSDS, you will automatically benefit from all the future improvements Elasticsearch is making for metrics time series, be it more efficient storage, query performance, or new aggregation capabilities. If you want to learn more about how TSDS works under the hood and all available config options, check out the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/master/tsds.html">TSDS documentation</a>. What Elasticsearch supports in 8.7 is only the first iteration of the metrics time series in Elasticsearch.</p>
<p><a href="https://www.elastic.co/blog/whats-new-elasticsearch-8-7-0">TSDS can be used since 8.7</a> and will be in more and more of our integrations automatically when integrations are upgraded. All you will notice is lower storage usage and faster queries. Enjoy!</p>
]]></content:encoded>
            <category>observability-labs</category>
            <enclosure url="https://www.elastic.co/observability-labs/assets/images/time-series-data-streams-observability-metrics/ebpf-monitoring.jpeg" length="0" type="image/jpeg"/>
        </item>
        <item>
            <title><![CDATA[Improving the Elastic APM UI performance with continuous rollups and service metrics]]></title>
            <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>
            <pubDate>Thu, 29 Jun 2023 00:00:00 GMT</pubDate>
            <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>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&lt;sup&gt;®&lt;/sup&gt; 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>FAQs</h2>
<h3>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>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>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 &quot;APM&quot; 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>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>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>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>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>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>
            <category>observability-labs</category>
            <enclosure url="https://www.elastic.co/observability-labs/assets/images/apm-ui-performance-continuous-rollups-service-metrics/elastic-blog-header-ui.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[How Prometheus Remote Write Ingestion Works in Elasticsearch]]></title>
            <link>https://www.elastic.co/observability-labs/blog/prometheus-remote-write-elasticsearch-architecture</link>
            <guid isPermaLink="false">prometheus-remote-write-elasticsearch-architecture</guid>
            <pubDate>Tue, 14 Apr 2026 00:00:00 GMT</pubDate>
            <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/observability-labs/blog/prometheus-remote-write-elasticsearch">Ship Prometheus Metrics to Elasticsearch with Remote Write</a>, covers the setup and configuration side.</p>
<h2>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>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>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 class="language-json">{
  &quot;@timestamp&quot;: &quot;2026-04-01T12:00:00.000Z&quot;,
  &quot;data_stream&quot;: {
    &quot;type&quot;: &quot;metrics&quot;,
    &quot;dataset&quot;: &quot;generic.prometheus&quot;,
    &quot;namespace&quot;: &quot;default&quot;
  },
  &quot;labels&quot;: {
    &quot;__name__&quot;: &quot;http_requests_total&quot;,
    &quot;job&quot;: &quot;prometheus&quot;,
    &quot;instance&quot;: &quot;localhost:9090&quot;,
    &quot;method&quot;: &quot;GET&quot;,
    &quot;status&quot;: &quot;200&quot;
  },
  &quot;metrics&quot;: {
    &quot;http_requests_total&quot;: 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>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>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>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 class="language-json">PUT /_component_template/metrics-prometheus@custom
{
  &quot;template&quot;: {
    &quot;mappings&quot;: {
      &quot;dynamic_templates&quot;: [
        {
          &quot;counter&quot;: {
            &quot;path_match&quot;: &quot;metrics.*_counter&quot;,
            &quot;mapping&quot;: {
              &quot;type&quot;: &quot;double&quot;,
              &quot;time_series_metric&quot;: &quot;counter&quot;
            }
          }
        }
      ]
    }
  }
}
</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>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>Data stream routing</h2>
<p>The three URL patterns map directly to data stream names:</p>
<table>
<thead>
<tr>
<th>URL pattern</th>
<th>Data stream</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>/_prometheus/api/v1/write</code></td>
<td><code>metrics-generic.prometheus-default</code></td>
</tr>
<tr>
<td><code>/_prometheus/metrics/{dataset}/api/v1/write</code></td>
<td><code>metrics-{dataset}.prometheus-default</code></td>
</tr>
<tr>
<td><code>/_prometheus/metrics/{dataset}/{namespace}/api/v1/write</code></td>
<td><code>metrics-{dataset}.prometheus-{namespace}</code></td>
</tr>
</tbody>
</table>
<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>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>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 &quot;created&quot; 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 &quot;trailing data&quot; 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>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>
            <category>observability-labs</category>
            <enclosure url="https://www.elastic.co/observability-labs/assets/images/prometheus-remote-write-elasticsearch-architecture/header.jpg" length="0" type="image/jpg"/>
        </item>
    </channel>
</rss>