<?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[Orestis Floros - 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[Orestis Floros - 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/orestis-floros</link>
    </image>
    <link>https://www.elastic.co/observability-labs/author/orestis-floros</link>
    <atom:link href="https://www.elastic.co/observability-labs/rss/author/orestis-floros.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <lastBuildDate>Fri, 18 Sep 2026 17:59:23 GMT</lastBuildDate>
  <item>
    <title><![CDATA[No log file too small: How Elastic Agent tracks files below the 1 KiB threshold]]></title>
    <description><![CDATA[Elastic Agent 9.5 builds a small log file's identity out of the bytes it already has, then re-links it as it grows and crosses 1024 bytes, so it is never re-ingested.]]></description>
    <content:encoded><![CDATA[<p>Elastic Agent 9.5 reads log files smaller than the 1024-byte fingerprint threshold.
Growing fingerprint identifies each one by hashing the bytes it currently has, and the read offset carries over when the file crosses the threshold, so nothing is re-ingested.
Until 9.5, a file that small had no stable identity, so it was skipped until it grew.</p>
<h2 id="whysmalllogfileswereskipped">Why small log files were skipped</h2>
<p>Filestream needs a stable identifier for every file it reads so that it can track how far it has read from each file.
In earlier versions, we used filesystem metadata such as inode numbers.
This was not enough.
For example, filesystems such as ext4 can reuse an inode number after a file is deleted, leading to a new file being mistaken for the old one.
This is why we <a href="https://www.elastic.co/blog/introducing-filestream-fingerprint-mode">introduced</a> the fingerprint identity and made it the default in 9.0.</p>
<p>Fingerprinting means files are identified by their content, not their metadata.
The first 1024 bytes (by default) are hashed and used as the unique identity of the file.
A registry keeps track of all IDs we encounter beyond the agent's process lifetime.
Restart your machine, change the underlying device, host the files over the network, it doesn't matter; Elastic Agent will remember.</p>
<p>The major downside is that we can't hash a file's first 1024 bytes if it doesn't have this many bytes to begin with.
Imagine a critical Kubernetes pod that panics on startup and its small log output is the reason the panic never reaches your Kibana dashboard.</p>
<p>The main way we've dealt with this until now is through configuration: instead of 1024 bytes, we could hash 512.
Or 256.
Or even less.
However, each step towards reducing the threshold leaves less file content available to distinguish one file from another.
Distinct files are therefore more likely to share the same fingerprint material, making the resulting file identity less stable.
This is where the new growing fingerprint can shine instead.</p>
<h2 id="howgrowingfingerprintsplitsfileidentityintotwophases">How growing fingerprint splits file identity into two phases</h2>
<p>With the introduction of the growing fingerprint mode, filestream splits a file's identity into two phases based on its size.
While a file is smaller than the 1024-byte fingerprint threshold, it's in "growing" mode.
Filestream identifies it with a hash of everything the file contains so far.
When the file grows past the threshold, it gets promoted to a static fingerprint, the one it would have had pre-9.5.</p>
<p>What about file-identity collisions?
Two files with identical contents do share a growing fingerprint, but only for as long as they stay identical: the moment their contents diverge, so do their fingerprints.</p>
<p>Let's take a closer look inside the internals.
Let's start with a simple 13-byte text file:</p>
<pre><code>mkdir -p /tmp/demo
echo 'Hello world!' &gt; /tmp/demo/file.txt
</code></pre>
<p>and start Filebeat, the component inside Elastic Agent that collects log files, with a minimal configuration:</p>
<pre><code>cat &gt;/tmp/demo/filebeat.yml &lt;&lt;YAML
path.home: /tmp/demo

filebeat.inputs:
  - type: filestream
    id: growing-demo
    paths: [ "/tmp/demo/file.txt" ]

output.file:
  path: /tmp/demo/out
logging.level: debug
YAML

filebeat -c /tmp/demo/filebeat.yml
</code></pre>
<h3 id="phase1thegrowingfingerprintofa13bytefile">Phase 1: the growing fingerprint of a 13-byte file</h3>
<p>After a few moments, filestream will pick up our new file.
Digging through the debug logs (vastly reduced here for simplicity), we see:</p>
<pre><code>{"log.level":"debug","log.logger":"input.filestream.prospector","message":"A new file /tmp/demo/file.txt has been found"}
</code></pre>
<p>We can also directly check the registry write-ahead log (WAL) filestream writes to <code>data/registry/filebeat/log.json</code>:</p>
<pre><code>{
  "k": "filestream::growing-demo::fingerprint::f04f9d5ad8c21a115cd207488c6aaadeb4d7db581ea675f48c7f5d23e038c805",
  "v": {
    "cursor": {
      "eof": false,
      "offset": 13
    },
    "meta": {
      "fingerprint_len": 13,
      "identifier_name": "fingerprint",
      "source": "/tmp/demo/file.txt"
    }
  }
}
</code></pre>
<p>The <code>k</code> field is the registry key filestream uses to identify this file state.
Its final segment is the fingerprint hash, which we can reproduce from the file contents:</p>
<pre><code>$ xxd -p /tmp/demo/file.txt | tr -d '\n' | sha256sum
f04f9d5ad8c21a115cd207488c6aaadeb4d7db581ea675f48c7f5d23e038c805  -
</code></pre>
<h3 id="stillinphase1thefingerprintchangesasthefilegrows">Still in phase 1: the fingerprint changes as the file grows</h3>
<p>Let's write more lines to the file:</p>
<pre><code>for i in {1..70}; do echo "Hello $i" &gt;&gt; /tmp/demo/file.txt; done
</code></pre>
<p>Filestream detects the write while the file remains below the threshold:</p>
<pre><code>{"log.logger":"input.filestream.prospector","message":"File /tmp/demo/file.txt has been updated","operation":"write"}
</code></pre>
<p>In the registry, the larger file now has a new fingerprint and a 634-byte <code>fingerprint_len</code>:</p>
<pre><code>{
  "k": "filestream::growing-demo::fingerprint::cb8e361971e7ba91e343d1b5bd8e6560be0bdadbbed2b2f89a6c50099e972d63",
  "v": {
    "cursor": {
      "eof": false,
      "offset": 634
    },
    "meta": {
      "fingerprint_len": 634,
      "identifier_name": "fingerprint",
      "source": "/tmp/demo/file.txt"
    }
  }
}
</code></pre>
<p>The updated key ends in the SHA-256 hash of the hexadecimal fingerprint material:</p>
<pre><code>$ xxd -p /tmp/demo/file.txt | tr -d '\n' | sha256sum
cb8e361971e7ba91e343d1b5bd8e6560be0bdadbbed2b2f89a6c50099e972d63  -
</code></pre>
<h3 id="phase2promotiontoastatic1024bytefingerprint">Phase 2: promotion to a static 1024-byte fingerprint</h3>
<p>Let's now grow the file beyond the 1024-byte threshold:</p>
<pre><code>for i in {1..500}; do echo "More messages $i" &gt;&gt; /tmp/demo/file.txt; done
</code></pre>
<p>Once the file crosses the threshold, filestream reports another write:</p>
<pre><code>{"log.logger":"input.filestream.prospector","message":"File /tmp/demo/file.txt has been updated","operation":"write"}
</code></pre>
<p>The registry key changes one final time as the growing fingerprint is promoted to its static form:</p>
<pre><code>{
  "k": "filestream::growing-demo::fingerprint::c8e252d76d621a02161f92e7a59d1617e3079db666cefd6cfcab106329082d1e",
  "v": {
    "cursor": {
      "eof": false,
      "offset": 9526
    },
    "meta": {
      "identifier_name": "fingerprint",
      "source": "/tmp/demo/file.txt"
    }
  }
}
</code></pre>
<p>The promoted fingerprint hashes the first 1024 bytes.
It is exactly what a pre-9.5 Filebeat would have computed for this file:</p>
<pre><code>$ head -c 1024 /tmp/demo/file.txt | sha256sum
c8e252d76d621a02161f92e7a59d1617e3079db666cefd6cfcab106329082d1e
</code></pre>
<h2 id="howfilestreammatchesagrowingfingerprinttoanexistingfile">How filestream matches a growing fingerprint to an existing file</h2>
<p>During each scan, filestream resolves a file's identity in this order:</p>
<ol>
<li>Look for an exact registry key matching the file's current fingerprint. This part has not changed.</li>
<li>If no key is found, search the growing entries for a shorter fingerprint, trying the same path first. A prefix match means filestream saw the same file earlier, when it contained fewer bytes.</li>
<li>Move the existing registry state to the new key, preserving the read offset.</li>
</ol>
<p>You might have noticed from the registry entries above that we don't store the raw file contents.
That was our initial approach, but it quickly exploded the size of the registry.
Instead, we use the SHA-256 hash as the registry key and store <code>fingerprint_len</code>, which records the number of raw bytes the fingerprint represents.
This reduces the storage required per entry by nearly 100×.</p>
<p>When filestream looks for a prefix match, it first groups the growing fingerprints by length.
It then streams the scanned file's hexadecimal fingerprint material through SHA-256, snapshotting the digest at each group length.
Filestream compares each snapshot only with stored hashes for the same length.
A match means that the stored fingerprint is a prefix of the scanned file.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdf5e8f36fc898105/6aabc22f92a58722afee1031/how-it-works.svg" alt="Diagram of a 700-byte scanned file whose hexadecimal fingerprint material is hashed in one SHA-256 pass. Digest snapshots are compared with registry entries of the same length; the 634-byte entry matches, so its state moves to the new key" /></p>
<h2 id="growingfingerprintbenchmarksmemoryandcpuat10000logfiles">Growing fingerprint benchmarks: memory and CPU at 10,000 log files</h2>
<p>So what does all this extra work cost?
We benchmark Elastic Agent 9.5 ingesting thousands of above-threshold and sub-threshold files.
To make the benchmark fair, we compare the growing fingerprint against the workaround of reducing the fingerprint size to 128 bytes.
Otherwise, we would be comparing doing something against doing nothing and nothing would always win.</p>
<p>The benchmarks run on an r8id.2xlarge EC2 instance (8 vCPUs, Ubuntu 24.04) and report the median of five runs.
Memory numbers are the Elastic Agent's anonymous memory as reported by the kernel via cgroups.</p>
<p>At 10,000 files, the two modes compare as follows.</p>
<p>| Metric | Condition | 128-byte static | Growing | Difference |
|---|---|---|---|---|
| Peak memory | Files above 1 KiB | 1,057 MiB | 1,099 MiB | +4% |
| Peak memory | Files below 1 KiB | 941 MiB | 1,012 MiB | +8% |
| Peak memory | Files crossing the threshold | 968 MiB | 1,016 MiB | +5% |
| Steady-state memory | Files above 1 KiB | 291 MiB | 290 MiB | none |
| Steady-state memory | Files below 1 KiB | 270 MiB | 322 MiB | +52 MiB |
| CPU, one core | Files above 1 KiB, idle | 4.1% | 4.1% | none |
| CPU, one core | Files below 1 KiB, idle | 3.9% | 4.9% | +1.0pp |
| CPU, one core | Files crossing the threshold | 16.8% | 25.5% | +8.7pp |
| Registry size at rest | Files above and below 1 KiB | 8.6 MB | 8.6 MB | none |</p>
<h3 id="memoryuseat1001000and10000logfiles">Memory use at 100, 1,000 and 10,000 log files</h3>
<p>At 100 and 1,000 files, 128-byte static and growing fingerprints differ by only a few MiB.
At 10,000 files the peaks separate by a few percent, but the only lasting difference is for sub-threshold files: above the threshold both modes stabilize within a MiB of each other.</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfb3f4bc0bc224b00/6aabc25719b2267b105a8882/benchmark-peak-memory.svg" alt="Grouped bar chart of peak memory at 100, 1,000 and 10,000 log files. At 10,000 files, growing uses 1,099 vs 1,057 MiB above the 1 KiB threshold and 1,012 vs 941 MiB below it" /></p>
<p>The most demanding case is when the fingerprints are still changing.
We created 10,000 sub-threshold files and appended 2,000 lines/s across them until every file crossed the threshold.
That repeatedly exercises the prefix-lookup path and moves registry state to new keys.
The extra memory this costs is released once the files are past the threshold and their harvesters close: both modes settle under 300 MiB, within a few MiB of each other:</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltede5d60e559fad77/6aabc26a14e38491ae4eaa40/benchmark-promotion-timeline.svg" alt="Line chart of memory over thirteen minutes while 10,000 files grow past the 1 KiB threshold under sustained writes and then idle. After the harvesters close, the 128-byte static and growing fingerprint modes settle at about 282 and 291 MiB" /></p>
<h3 id="cpuusewithandwithoutwrites">CPU use with and without writes</h3>
<p>The CPU impact is larger: every scan re-hashes a sub-threshold file's full contents instead of a fixed 128-byte prefix.
That costs one extra percentage point of a core with no writes coming in, and almost nine under the changing-fingerprint workload above:</p>
<p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt218b4365aaf1002f/6aabc27e8f2cd72150e5380e/benchmark-cpu.svg" alt="Bar chart of CPU as percent of one core at 10,000 files. Idle scans cost 4.1% for 128-byte static and growing fingerprints on above-threshold files and 3.9% vs 4.9% on sub-threshold files; while files grow past the threshold under sustained writes, the 128-byte static fingerprint uses 16.8% and growing 25.5%" /></p>
<h3 id="whatchangesin96">What changes in 9.6</h3>
<p>We are already working on improving these numbers.
On current main (future 9.6 release), the same benchmarks peak at less than half the memory, and the growing fingerprint's steady-state overhead, in both memory and CPU, is roughly halved.</p>
<h2 id="upgradingandrollingback">Upgrading and rolling back</h2>
<p>Growing mode ships enabled by default in 9.5 through <code>file_identity.fingerprint.growing</code>.
Set it to <code>false</code> to restore the pre-9.5 behavior.
There is no need for a migration on upgrade; existing registry entries keep working and files that were previously too small get picked up on the next scan.</p>
<p>Rolling back is also possible, but you go back to pre-9.5 behaviour: small files are ignored and when they cross the threshold, they get re-ingested.</p>
<h2 id="whatgrowingfingerprintmeansforyourlogfiles">What growing fingerprint means for your log files</h2>
<p>Elastic Agent 9.5 ingests log files smaller than 1024 bytes by default, with no configuration and no waiting for the file to grow.
Shared identities between identical small files resolve themselves as soon as their contents diverge, which a static fingerprint cannot do.
The cost stays small: at realistic node scales the growing fingerprint is indistinguishable from the old workarounds, and at a 10,000-file workload it costs a few percent of memory and single-digit percent of one CPU core.
Remember when upgrading that small files that were invisible before count as new, however long they have been sitting in your paths.
If the trade doesn't work for you, <code>file_identity.fingerprint.growing: false</code> brings back the old behavior.</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://www.elastic.co/docs/reference/beats/filebeat/file-identity">How to choose file identity for filestream</a> covers all identity options and their trade-offs.</li>
<li><a href="https://www.elastic.co/docs/reference/beats/filebeat/file-identity#file-identity-fingerprint-growing">Growing fingerprint reference</a> documents the setting and the rollback caveat.</li>
<li><a href="https://github.com/elastic/beats/pull/50566">PR #50566</a> is the implementation and <a href="https://github.com/elastic/beats/pull/51784">PR #51784</a> the registry optimization; <a href="https://github.com/elastic/beats/pull/51863">PR #51863</a> and <a href="https://github.com/elastic/beats/pull/51914">PR #51914</a> trimmed per-scan allocations and fingerprint retention in 9.5.2.</li>
<li>Optimizations coming up in 9.6: <a href="https://github.com/elastic/beats/pull/51278">PR #51278</a> parks idle harvesters instead of keeping one goroutine per open file, <a href="https://github.com/elastic/beats/pull/51694">PR #51694</a> scans each file in a single pass, and <a href="https://github.com/elastic/beats/pull/52476">PR #52476</a> normalizes published events in place. <a href="https://github.com/elastic/beats/pull/52103">PR #52103</a>, which replaces the multiline timeout goroutine with a read deadline, is not exercised by these benchmarks.</li>
</ul>]]></content:encoded>
    <link>https://www.elastic.co/observability-labs/blog/growing-fingerprint-filestream-file-identity</link>
    <guid isPermaLink="false">growing-fingerprint-filestream-file-identity</guid>
    <category><![CDATA[Logs Analytics]]></category>
    <category><![CDATA[Infrastructure Monitoring]]></category>
    <category><![CDATA[What's New]]></category>
    <dc:creator><![CDATA[Orestis Floros]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd520b0634f15c227/6aabc2b1a4ecdf0c30ca91a0/header.jpg" length="0" type="image/jpeg"/>
    <pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>