Engineering Note

Why Turn Immutable YAML Logs into Full Objects?

A practical look at why stream/event-based YAML processing can be a better fit than building a full DOM or object tree when high-volume YAML logs only need filtering.

June 23, 2026 · Pletor Engineering yamlloggingperformancestreamingoperations

When we need to process YAML, one approach usually comes to mind first: read the file, pass it to a parser, build an object or map, and then pull out the fields we need.

For small configuration files, that approach is convenient. The code is readable, field access is simple, and if we need to edit values or rewrite the structure, having the whole tree in memory makes sense.

But not every YAML workload is configuration editing. The problem I ran into was closer to log processing.

An application was receiving tens, sometimes hundreds, of YAML-formatted logs per second. It only needed to select a subset of those logs. Once produced, each log event was immutable. We did not modify the content, pretty-print it again, or keep the full structure as a domain object for long.

The work looked more like this:

Read one log event.
Check a few fields.
Forward it if it matches.
Drop it if it does not.

The first implementation still built a full object tree for every YAML log.

Illustration of many translucent log documents flowing as a stream while a few selected events glow warmly
When immutable logs only need filtering, every event may not need to become a full object tree.

The Obvious First Choice Was Objects

A single log event looked roughly like this:

timestamp: "2026-06-23T10:15:30Z"
service: payment-api
level: WARN
event:
  type: dependency_timeout
  target: settlement-db
  elapsed_ms: 1280
trace:
  id: 7f9d0b7e8f0a4c31
  sampled: true
labels:
  region: ap-northeast-2
  tenant: enterprise-a

The initial implementation parsed that YAML into a map or object, then read fields such as level, service, event.type, and trace.sampled to decide what to do.

That is a familiar pattern. It is easy to test, easy to reason about, and easy to handle missing fields.

The cost became visible as throughput increased. The application was not modifying logs, but it still constructed the complete YAML structure for every event. Nested maps, lists, scalar values, and wrapper objects were created even for fields that had nothing to do with filtering.

One event looked small. Continuous traffic changed the picture. Object allocation increased, short-lived objects piled up, and GC had more work to do. CPU time was spent not only on parsing YAML but also on building and discarding trees.

That forced a simpler set of questions:

Will this data be modified?
Do we need the full structure?
Can we check the required fields and then discard the event?

For logs, the answer was fairly clear. A produced log was not something to edit. It was something to evaluate.

DOM Is Convenient, but Not Free

Here, I use DOM broadly. YAML libraries may expose a node tree, a map/list structure, or mapped domain objects rather than an XML-style Document Object Model. The common point is that the whole document becomes an in-memory structure.

The benefits are real:

  • We can traverse the whole structure multiple times.
  • Parent and child relationships are easy to navigate.
  • Updating values and writing the result back is natural.
  • Schema validation and complex transformations are easier.
  • The application code often looks straightforward.

But log filtering may not need most of those benefits.

If the filter only needs level, event.type, and service, building the whole tree means doing more work than the task requires.

Required work: decide whether the event matches
Actual work: build the full YAML object graph, then decide whether the event matches

At low throughput, the gap may be invisible. When input keeps arriving, events grow larger, and filtering remains simple, that extra work starts to matter.

Switching to a Stream View

Stream or event-based processing consumes parser events instead of building a full object tree first.

Conceptually, the parser emits something like this:

document start
mapping start
scalar: timestamp
scalar: 2026-06-23T10:15:30Z
scalar: service
scalar: payment-api
scalar: level
scalar: WARN
...
mapping end
document end

The processing application tracks the current path while events flow by. When it sees a field it cares about, it checks the value.

For example:

Is level WARN or ERROR?
Is event.type dependency_timeout?
Is service in the target set?
Is trace.sampled true?

Once the application has enough information to make a decision, it does not need to materialize the whole document. Depending on the implementation, it can quickly drain the rest of the event or move to the next log.

The point is not that YAML no longer needs parsing. YAML syntax still needs a real parser. The difference is what happens after parsing begins: we avoid constructing a complete application-level object graph when the task only needs a few values.

The Important Clue Was Immutability

The central clue was not YAML itself. It was the lifecycle of the data.

A log event becomes fixed once it is produced. Downstream systems usually do not edit the body of the log. They select, route, aggregate, extract fields, or evaluate alert conditions.

Configuration file: read, understand, modify, write again.
Log event: read, evaluate, forward or drop.

Both can be YAML, but they do not need the same processing model.

Configuration files often benefit from DOM-style processing. Users may edit values, fill defaults, rewrite sections, or save the result.

Log events are different. There is usually little reason to hold the full structure for long. We only need to know whether the event matches. Stream/event processing matches that lifecycle more closely.

Where the Performance Difference Comes From

The benefit of streaming usually comes from four places.

First, memory use drops. The application does not need to create a full tree for every log event.

Second, GC pressure drops. Log processing creates many short-lived values. The more of those values we allocate, the busier GC becomes.

Third, the application can focus on the fields that matter. Not every scalar needs to become an application-level object.

Fourth, backpressure is easier to reason about. Input, parsing, filtering, and output can be treated as stages, which makes it easier to see where the pipeline is slowing down.

For a few small files, none of this may matter. DOM-based processing may be simpler and easier to maintain. Stream processing requires more careful state tracking.

So the rule is not “streaming is always faster.”

Do we need to modify the document?
Do we need the full structure?
Do we need random access?
Is the input volume high enough?
Are the filter conditions narrow?

Those questions should drive the choice.

YAML Still Needs Care

YAML is broader than JSON. It has indentation, sequences, mappings, scalar styles, multi-document streams, anchors, aliases, and merge keys.

With stream-based processing, the application tracks paths from parser events. Treating YAML as a simple line-oriented format is risky. If the logs are generated by a system rather than written by humans, it helps to constrain the shape of the output.

For example, a consumer becomes much simpler when the producer guarantees:

  • no multi-document streams
  • no anchors or aliases
  • no merge keys
  • stable paths for the fields used by filters
  • simple scalar styles where possible

Without those constraints, streaming is still possible, but the implementation needs to understand the parser’s event model and edge cases.

In Java, SnakeYAML Can Expose the Event Stream

In my case, the application was written in Java and used SnakeYAML. It is easy to think of SnakeYAML only as a library that loads YAML into Map structures or Java objects. That misses another useful layer.

SnakeYAML also has a path for handling parsing events directly. In classic SnakeYAML, Yaml#parse(...) exposes YAML input as a sequence of Event objects. Internally, the parser model includes events such as stream start, document start, mapping start, scalar, and sequence start/end.

That model fits log filtering well.

See a scalar key.
Update the current path.
If it is a field of interest, inspect the next scalar value.
Keep only the values needed for the filter decision.

This is less convenient than DOM-style parsing. The application needs to track paths, sequence positions, and nested mapping state. But when the log will not be modified and only a few fields matter, avoiding a full Map<String, Object> per event can be valuable.

Using SnakeYAML does not force the application into DOM or object-tree processing. The important decision is which layer of the library to use. For log pipelines where throughput and allocation matter, the event layer is worth considering.

The Same Idea Applies to JSON

This idea is not limited to YAML. JSON has the same distinction.

The common JSON path is to build a full object tree or bind the document to a domain object. With Jackson, that might mean using ObjectMapper for data binding or building a JsonNode tree. That is convenient and powerful.

But if JSON logs are immutable and the application only needs a few fields for filtering, Jackson Streaming API is the equivalent lower-level model. JsonParser emits tokens in order, and the application checks only the field names and value tokens it needs.

The format changes, but the rule is the same.

If the whole document needs manipulation, tree or binding is convenient.
If a passing event only needs a few values checked, stream/token processing may fit better.

Whether the input is YAML or JSON, the first question should not be “how do I turn this into objects?” It should be how the data is used, whether it will be modified, and how much of it will arrive.

When DOM Is the Right Choice

This is not an argument against DOM-style parsing. It is still a good default in many cases.

Building the full tree makes sense when:

  • The YAML content needs to be modified.
  • A decision depends on many parts of the document.
  • The same document will be traversed multiple times.
  • Schema validation must report detailed user-facing errors.
  • Comments, ordering, or formatting need to be preserved or rewritten.
  • Implementation simplicity matters more than throughput.

The important distinction is not the file format. It is the nature of the work.

Is this YAML something we edit?
Or is it an event passing through the pipeline?

That distinction should shape the processing model.

The Operations Angle

Performance problems in log processing often appear to come from storage, network, queues, or downstream sinks. But costs can accumulate inside the first piece of code that receives and classifies an event.

When data keeps arriving, most events are dropped, and only a subset moves forward, the question is not always “how nicely can we turn each event into an object?” Sometimes it is “how quickly can we decide and move on?”

Konduo is built around a similar operational view. Resources, metrics, and alerts are connected because problems rarely live in one layer. In a log pipeline, parser behavior, allocation, GC, queues, and output sinks move together. The processing model should be chosen with that whole flow in mind.

Conclusion

Processing YAML does not always mean building a full object tree.

For configuration files that need editing and rewriting, DOM-style processing is often the natural choice. For immutable logs that only need filtering and routing, stream/event-based processing can be a better fit.

The lesson I kept was simple:

If the data will not be modified, question whether it needs to become a full object first.

YAML logs may be data we read, evaluate, and pass through. The processing model should match that lifecycle.

Further Reading

For more context connected to this topic, these posts are also worth reading.