Faster, smaller checkpoints: the File Log receiver now uses protobuf

If you run the File Log receiver with a storage extension, the Collector writes a checkpoint every time it polls your log files. That checkpoint is how the receiver remembers where it stopped reading, so a restart does not lose or duplicate logs. For years that checkpoint format was JSON.

Starting with OpenTelemetry Collector Contrib v0.164.0, checkpoints are always written as protobuf. On our benchmarks, saving a checkpoint is almost 5 times faster, loading one is more than 5 times faster, and the checkpoint itself is about 35% smaller.

Why checkpoints were worth optimizing

The File Log receiver keeps a small record for every file it tracks: a fingerprint built from the first bytes of the file, the read offset, the record number, the file attributes, and some state used to flush partial lines. When storage is configured, that whole list is serialized and written to the storage extension at the end of every poll cycle, and once more when the receiver stops. It is read back only once, when the receiver starts.

With the default poll_interval of 200 ms, that means five writes per second, for as long as the Collector runs. On a Kubernetes node with hundreds of container log files, every one of those writes serializes hundreds of records.

In October 2025, a user reported that a Collector collecting Kubernetes logs with file_storage was using noticeably more resources than Fluent Bit doing similar work. Profiling showed a surprising amount of CPU going into JSON encoding of the checkpoint.

JSON vs protobuf

JSON is great when people need to read the data. Nobody reads a checkpoint except the receiver, so JSON was paying a price for nothing.

The biggest cost is the file fingerprint, the first 1,000 bytes of each file. JSON cannot hold raw bytes, so they were converted to text, which made them about a third bigger and had to be redone on every save and every load. On top of that, every record repeated the same field names, and numbers and timestamps were written out as text.

Protobuf is a compact binary format. It stores the fingerprint as it is, uses small numeric tags instead of field names, and keeps numbers as numbers. For a single tracked file, the checkpoint goes from 1,818 bytes to 1,186 bytes, and it takes much less work to write and read.

The work landed in #45097 and became the only way checkpoints are written in #52037. Before making it permanent we asked users running it in production for feedback and did not hear about any problems.

The numbers

We used the benchmarks that live next to the checkpoint code in pkg/stanza/fileconsumer. Each benchmark builds realistic metadata for 1, 10, 100 and 1,000 files, each with a 1,000 byte fingerprint and five file attributes, and saves it to an in memory storage. We ran the same binary with JSON and with protobuf encoding, interleaved, 10 runs each, and compared the results with benchstat.

The machine was a shared cloud VM (4 vCPU Intel Xeon at 2.80 GHz, linux/amd64, Go 1.26.0). Absolute numbers will differ on your hardware, but the ratios are what matter here.

Time

Operation, 1,000 filesJSONProtobufChange
Encode (Save)15.38 ms3.21 ms-79.15%
Decode (Load)28.18 ms5.01 ms-82.22%
Round trip44.04 ms8.54 ms-80.61%

The improvement does not depend on how many files you track. Protobuf is between 4 and 6.5 times faster than JSON from a single file all the way to 1,000 files:

Line chart of how many times faster protobuf is than JSON for 1, 10, 100 and 1,000 tracked files. Decode goes from 6.5x to 5.6x, round trip from 4.8x to 5.2x, and encode from 4.1x to 4.8x.

Encoding is the number that matters most, because it runs on every poll. To put it in perspective, at five saves per second with 1,000 tracked files, JSON spent roughly 77 ms of CPU every second just on checkpoints. With protobuf that goes down to about 16 ms. That is CPU your Collector gets back for parsing and exporting logs.

Size and memory

1,000 filesJSONProtobufChange
Stored checkpoint1.740 MiB1.139 MiB-34.58%
Allocated per Save5.807 MiB2.919 MiB-49.74%

The checkpoint is about 35% smaller at every file count we tested, which means less data written to disk by file_storage five times per second. Each save also allocates half as much memory, so there is less work for the garbage collector.

There is one small trade off. Protobuf makes about 10% to 13% more allocations per save, because each record is converted into a protobuf message before it is marshaled. Those allocations are smaller, and the total bytes allocated still go down by half, so in practice it is a net win.

What you need to do

Nothing. Existing JSON checkpoints are still read when the Collector starts and are rewritten as protobuf on the next save, so upgrading is transparent and you keep your read offsets.

The archive used by the polls_to_archive setting uses the same encoding, so it benefits from the same improvements.

If you see anything unexpected with checkpoints after upgrading, please leave a comment on #43266.