↩
Apache Spark WTF??? · 29 April 2026

Apache Spark WTF??? 🧛 Buffers The Slayer 🩇

Every Spark project has a moment where innocence dies.

13 min read
0% read·13 min in this view

“‘The way you sold me human-friendly support
As you sunk your teeth into me, oh
Resource-sucker, JVM-fucker
Bleedin’ my pipeline dry like a goddamn vampire”
(adapted from
“Vampire” by Olivia Rodrigo)


🧛 The Cost of Being Human-Readable

Performance problems don’t always arrive with an explosion; sometimes they just bleed you dry.

When a Spark job is sluggish, we round up the usual suspects: skewed joins, tiny files, or someone doing a collect() in broad daylight. But often the culprit is more fundamental: the format itself is expensive.

In Spark, a format isn’t just a preference—it dictates CPU cycles, memory churn, and I/O overhead.

That is where the vampire bite happens.

XML is the old-world aristocrat — expressive and hierarchical, but committed to making every record look like it has legal counsel. It pays for that richness with enough parsing overhead to make large-scale processing feel like punishment by paperwork.

JSON is the friendlier successor — simple, ubiquitous, and less ceremonial. But JSON is still text. It repeats field names incessantly and requires a parser to reconstruct typed structure from characters. At scale, those costs stop being philosophical and start becoming billable.

The problem? Human-readable formats do performance damage in places where humans aren’t involved.

Spark doesn’t need a payload to be charming; it needs it to be efficient.

Enter Protocol Buffers (or protobuf, if we’re friends).

Protobuf isn’t a silver bullet, but it is an elite weapon against bloated payloads. Instead of making every record repeat its structure like a cursed monologue, it relies on a pre-defined schema. The result is a compact binary serialization that stops being theatrical and gets to work.

In pipelines ingesting millions of structured events, the format itself becomes the bottleneck.

This article is about that fight: not “protobuf is always better,” but a look at where it actually stops the bleeding — and where the vampire is hiding somewhere else.


đŸ©ž How Protobuf Stops the Bleeding

If XML and JSON are the theatrical villains, Protobuf is a tactical fix — provided serialization is actually what’s hurting you.

The first win is payload size. Text formats haul around brackets, quotes, and tags like cursed luggage. Protobuf cuts this aggressively by using a pre-defined schema; because both sides know the contract, the payload doesn’t have to restate its life story every single time.

The second win is parsing cost. JSON forces Spark to interpret delimiters and infer types from characters, creating a “swarm of bats” that burns CPU. Protobuf’s binary decoding is more direct. It follows a map and gets on with its undead little life without the theatrical guesswork.

The third win is discipline. Text payloads thrive on chaos — fields appear and vanish, and Spark is forced to smile politely while someone else’s schema drift becomes your runtime disaster. Protobuf is strict. You define messages explicitly. It’s “garlic” for data contracts, ensuring producers and consumers stay in sync.

Protobuf shines in the “In-Between” places: Kafka streams, service events, and binary interchanges where “human-readable” is just an expensive hobby. It won’t fix a terrible join or heal tiny files, but it solves one specific problem beautifully: wasting compute and bandwidth on verbose serialization when the structure is already known.

That is a meaningful win. đŸ’Ș


📜 Binary Sorcery: What Protobuf Is

Protobuf’s core idea is simple: stop making every record explain itself from scratch.

JSON and XML are like employees who insist on a full reintroduction before doing any work. Every payload carries its own meaning inline, repeating field names and structures in text.

Protobuf is less chatty. It relies on a pre-defined schema — a .proto file—where you describe fields, types, and numeric identifiers once.

syntax = "proto3";  
  
package bench;  
  
message Metrics {  
  double latency_ms = 1;  
  int32 items = 2;  
}  
  
message Event {  
  string event_id = 1;  
  string user_id = 2;  
  string session_id = 3;  
  string event_type = 4;  
  string ts = 5;  
  string device = 6;  
  string country = 7;  
  double amount = 8;  
  Metrics metrics = 9;  
  repeated string tags = 10;  
}

In a message like this one, the number in event_id = 1 is the “Field Tag.” On the wire, protobuf doesn’t keep writing “event_id”; it just sends the tag and the value. Instead of a verbose JSON object, the payload effectively says: “Field 1: here’s a string. Field 8: here’s a double.”

This is the philosophical shift: the payload is no longer self-describing for humans. It is optimized for a decoder that already knows the rules. In Spark, this usually shows up when handling binary payloads from Kafka. You decode them with from_protobuf(), work with them as standard columns, and re-encode them with to_protobuf().

If JSON says: “Here is the data and a full textual explanation of what it is,” Protobuf says: “Here is the data. You already know the contract. Let’s not waste time.” In a high-volume pipeline, that attitude ages beautifully.


🧳 Avro Enters the Cemetery: Same Arena, Different Fighting Style

Protobuf isn’t the only warrior in the graveyard. Enter Apache Avro, a schema-based binary format that doesn’t exist to make Protobuf look good — it exists to compete. Like Protobuf, it is compact, efficient, and perfectly at home in Spark.

The choice between them isn’t about power; it’s about vibe:

  • Protobuf’s Vibe: It feels most natural in message contracts. It’s the king of service-to-service communication and Kafka payloads. Its attitude is: “We agreed on the contract. Here is the message. Keep moving.” It is strict, tactical, and built for speed.
  • Avro’s Vibe: It feels more natural in data-platform workflows. It’s designed for long-lived datasets and environments where schema evolution is constant. Its attitude is: “I brought the data, the schema, and the compatibility rules.”

If Protobuf is the slayer with a tactical crossbow, Avro is the field researcher who labeled the samples and already filled out the incident report. Protobuf is sharper for compact exchange; Avro is more comfortable in data ecosystems.

The real question isn’t which one is better, but which matches the specific kind of pain you’re trying to remove.


đŸ§Ș The Lab of Horrors: How We Measured the Blood Loss

To catch a vampire, you don’t just walk into a dark room; you bring a wooden stake and a precise stopwatch.

I didn’t run a “quick test” on three rows — I built a 10-million-row data monster and forced Spark to push it through a gauntlet of formats. It wasn’t a perfect benchmark, but like a suspicious red stain on the carpet, it tells you exactly where to start investigating.

1. The Victim: The Data

The test used 10 million rows of simulated event logs. This wasn’t “Hello World” data; it was a production-style payload with high-cardinality UUIDs, nested metrics, and dynamic arrays. It was structured enough to be understood, but complex enough to make a parser sweat — the kind of event that starts harmless and owns half your pipeline six months later.

2. The Execution Chamber: The Environment

Tests ran locally with 4–6GB of driver memory. This was a controlled chamber where formats had fewer places to hide.

As always, I used VisualVMto watch CPU and heap behavior because runtime tells you who won, but the JVM’s “heartbeat” tells you who was screaming while doing it.

3. The Gauntlet: The Test Logic

Each format followed a two-step ritual:

  • The Write Phase: Spark transformed the baseline data into the target format, measuring the “packing” cost.
  • The Read + Query Phase: Spark loaded the data, filtered records (amount > 50), and transformed them, saving to a noop sink. This ignored output bottlenecks to focus purely on the cost of unpacking and finding what you need.

4. The Rules of Engagement

This wasn’t a perfectly fair gladiator match; XML, JSON, Avro, and Protobuf live at different layers of the ecosystem.

  • JSON used standard text parsing.
  • Protobuf used a compiled descriptor stored in a Parquet envelope.
  • Avro used native schema-aware handling.
  • XML was asked to do “XML things,” which is essentially a punishment for the JVM.

The goal wasn’t to crown a winner, but to ask: Where does Spark pay the price?

5. What We Measured

We captured Write, Load + NoOp, and Total timings. Alongside the clocks, we monitored CPU and heap usage — because performance numbers without resource behavior are like vampire movies without fog: technically possible, but spiritually incomplete.

6. The Source of All Evil

The full ritual — generator, scripts, and schemas — lives in the buffers-the-slayer repo. If you want to reproduce the results or just stare into the abyss and ask why your CPU fan sounds possessed, the code is there.

Bring garlic. 🧄


📉 The Bloody Results: What Actually Happened

The stopwatches have stopped, and the numbers are in. If you were expecting a comforting story where “newer” means “faster,” I have bad news: distributed systems care about bytes and memory pressure, not your feelings.

XML: The Ancient Patriarch

At the bottom of the coffin is XML. It took 39.03s to write and a catastrophic 487.36s to load. That isn’t a result; it’s a haunting. XML dragged Spark into the crypt, forcing it to excavate meaning from a cathedral of angle brackets. Every tag was a needle in the side of the JVM.

JSON: The Socially Acceptable Monster

JSON wrote in 16.95s and processed in 23.47s. It wasn’t terrible. It’s ubiquitous and flexible, but it still pays the “Text Tax.” During the read, Spark must parse characters and rebuild structures repeatedly. JSON is the charming suspect who definitely knows more than they’re saying.

PROTOBUF: The Tactical Specialist

Protobuf was disciplined, writing in 20.13s and processing in 20.52s. It avoided text-format sins like curly-brace archaeology, but it didn’t obliterate the competition. Because it lived as a binary payload inside a file container, Spark had to encode, store, read, and decode. It was a soldier sprinting inside a shipping container.

AVRO: The Quiet Assassin

Avro walked in and ruined everyone’s evening: 12.98s to write and 6.76s to process (19.74s total). No drama, just brutal efficiency. It won because this was its home turf. It’s binary, schema-aware, and file-native, allowing Spark to read structured records directly without unpacking an opaque binary envelope.

đŸ«€ The Anatomy of the Kill: Resource Behavior

The XML CPU profile looked like a heart attack; the JVM spent its life parsing and allocating temporary objects, leading to aggressive Garbage Collection (GC).

JSON was calmer but still showed the churn of text-heavy parsing.

Protobuf behaved like a soldier with extra gear. It skipped the text fluff, but Spark still had to pay a “decode tax” to open the binary “coffin” before it could see the data.

Avro, however, was beautifully boring. It didn’t make the JVM host a sĂ©ance; it just flowed. In performance engineering, “uneventful” is a synonym for “excellent.”

🧟 Why This Benchmark Was Totally Cursed

This experiment was structurally unfair — a “cursed” comparison for three reasons:

  1. The Envelope Problem: XML and JSON are native files; Protobuf is a serialization format. We judged the slayer by how quickly they could escape a suitcase (the Parquet envelope) before the fight started.
  2. The Opaque Payload: To filter amount > 50, Spark had to fully deserialize the Protobuf blob. For Avro, that field was already part of the structured record. Protobuf had to pay a binary toll before the query could begin.
  3. The Wrong Battlefield: This was a file-processing contest. Avro was in its living room; Protobuf was dragged away from its true home — Kafka payloads and service contracts — into a race where the track was shaped like Avro.

The Verdict: Avro didn’t win because it’s magically superior. It won because it had fewer layers to fight through in this specific file-based ritual.


đŸ˜Č Spark 4: XML Native Support, Variant, and the Plot Twist

Spark 4 has introduced a modern complication to our “serialization graveyard.” First, it brought XML back from the retirement home. For years, you needed third-party libraries to handle it; now, XML support is native. It’s just
 there. No more JAR hunting, just the DataFrame API doing its best to make angle brackets tolerable.

But the real game-changer is VariantType — or as I like to call it, “JSON with a brain.”

The Rise of Variant and Shredding

Traditionally, Spark treats JSON as a “dumb” string. It has to parse the entire text block before it can find a single nested value. Variant changes the deal by storing semi-structured data in a binary format that Spark can query more naturally.

Then there’s the sharper weapon: Shredding. This is where Spark pulls frequently accessed paths out of the semi-structured payload and stores them in hidden, query-friendly columns. Instead of opening the entire “coffin” to find one field, Spark keeps that data right at the surface.

The Performance Caveat

The numbers show that Variant isn’t “free magic.” It comes with a significant Write Tax:

  • Plain Variant: 57.72s (Write) | 20.62s (Read). The upfront cost of converting data into Variant made it slower than basic JSON string overall.
  • Variant with Shredding: 37.70s (Write) | 12.93s (Read). This is the “Slayer” moment. By paying a bit more during the write, the read phase became nearly twice as fast as raw JSON.

Shredding is where the bite is. It makes the read path much more efficient by ensuring Spark doesn’t have to rummage through the whole semi-structured mess every time.

đŸ€” Is Protobuf Still Worth Using?

Yes — but only when the pain is at the serialization boundary. For Kafka payloads, binary event streams, and service-to-service contracts where bytes and discipline are king, Protobuf remains the elite choice.

However, if you’re doing flexible internal analytics where JSON was previously “good enough,” Variant and Shredding make the old “JSON is sloppy” narrative feel outdated. Spark 4 didn’t kill Protobuf or Avro; it just made JSON less embarrassing.

Or, said properly:

Spark 4 gave JSON a gym membership. Shredding gave it a knife. Protobuf still brings the stake.


🌅 The Morning After: Choosing Your Slayer

The sun is rising over the cluster, and the “serialization vampires” are retreating. After poking the binary beast and watching Variant perform surgery on messy JSON, one thing is clear: there is no single “Ultimate Slayer.” Every tool is a specialized weapon, and picking the wrong one leads to a cautionary tale in a post-mortem report.

⚖ The Final Verdict: Which Weapon for Which Monster?

  • The Avro Ingestion: If you need to land data as fast as possible, Avro is your champion. It’s the “Get it in the lake now, worry later” format — ideal for surviving ingestion spikes.
  • The Protobuf Contract: For Kafka or microservices, Protobuf remains the elite choice. Its discipline and tiny wire-size make it the king of the “In-Between,” keeping network costs low and contracts signed in blood.
  • The Variant Intelligence: For analytics on “messy” data, Variant is the new King. You pay a “Write Tax” once for JSON-like flexibility with Parquet-like query speeds. It is the ultimate Lakehouse cheat code.

Spark 4 didn’t just give JSON a gym membership; it gave it a bypass. Protobuf remains the Slayer for the network, but Variant is the Slayer for the Lakehouse. The “Victorian Mansions” of XML and JSON haven’t been torn down, but we finally have the power to walk through their walls without getting stuck in the floorboards.


🌙 Final Thoughts from the Hellmouth

What did we learn? XML is still elegant, formal, and willing to make your cluster suffer for aesthetics. JSON remains the people’s champion — practical and ubiquitous, even if your CPU resents the relationship. Avro is the serious data-platform cousin, while Protobuf remains the sharpest answer to the waste of too many bytes and too little discipline.

The goal isn’t to turn these formats into ideology.

  • Use XML when you must (or when legal counsel insists).
  • Use JSON when humans actually need to inspect the payload.
  • Use Avro for schema-aware exchange and high-speed file landing.
  • Use Protobuf when bytes, parsing costs, and strict contracts are the primary bottleneck.

Not every slow Spark job is a serialization problem; skewed joins and tiny files are still waiting in the dark. But when the problem is the payload? Avro, Protobuf, and now Variant are the ones you call when the bytes start acting expensive.

Choose your weapon wisely, or the vampire will be the one writing your next cloud bill.


“The battle’s done and we kind of won
So we sound our victory cheer
Where do we go from here?”
(from
“Where do we go from here?” — Buffy the Vampire Slayer)


89Published
44,161Visits
21,693Read
VisitsReadLAST 5 WEEKS
Week ending 16 Sep · WholeStageCodegen · 90 visits · 5 readsWeek ending 9 Sep · 62 visits · 13 readsWeek ending 2 Sep · 433 visits · 75 readsWeek ending 26 Aug · 295 visits · 19 readsWeek ending 19 Aug · 398 visits · 49 readsWeek ending 16 Sep · 5 readsWeek ending 9 Sep · 13 readsWeek ending 2 Sep · 75 readsWeek ending 26 Aug · 19 readsWeek ending 19 Aug · 49 reads