← Back to list

Apache Spark WTF??? 📖 The Neverending Real-Time Story 🐉

Four data engineers sat around the campfire, wrapped in old cluster logs, whispering about the thing that had eaten their micro-batches.

Ángel Álvarez Pascua in Towards Data Engineering · 2026-06-16 03:57 · 0 claps · 45.3 min read
#apache-spark #streaming #data-engineering #databricks #real-time-analytics
Open on Medium ↗
Wiki topics: MM · Multimodal & Generative Media GRW · Growth & Analytics 🔧 · Data Engineering 🎬 · Film & Television

Apache Spark WTF??? 📖 The Neverending Real-Time Story 🐉

Four data engineers sat around the campfire, wrapped in old cluster logs, whispering about the thing that had eaten their micro-batches.

“The trigger fired,” said one. “The DAG appeared,” said another. “But the latency…” said the third. The fourth stared into the dark. “The Nothing had already arrived.”

This article is about Spark Real-Time Mode: the low-latency execution path that keeps the Structured Streaming API but changes the machinery underneath.

Spark did not kill micro-batches. It just learned that some events should not have to wait for the dinner bell.

When your Spark job finally hits real-time mode and immediately develops luckdragon confidence.

When your Spark job finally hits real-time mode and immediately develops luckdragon confidence.

“Turn it round. Watch the records stream In each trace. The metrics of your dreams” (adapted from **“The neverending story” by Limahl**)

📚 The Antiquarian Bookshop

The Forbidden Book Opens

There are books you should not open. Books bound in forgotten leather, smelling of dust, destiny, and suspicious production debt.

Books that whisper: *Just call* writeStream.

And there you are — a data engineer hiding from sprint planning — opening the forbidden book of Spark Structured Streaming.

The first page looks innocent: a DataFrame, a sink, a checkpoint, a trigger, and one polite little start.

But that line does not merely start a query. It opens a portal.

The Beast Named p99

Behind the portal live offsets, commit logs, state stores, watermarks, shuffles, sink semantics, checkpoint recovery, and one glowing-eyed creature named p99.

In latency land, averages are the bait.

p95 means 95% of events were faster than that number, but 5% were slower. p99 means 99% were faster, but the slowest 1% were still out there, dragging their cursed little feet through your pipeline.

That slow 1% is where “real-time” systems usually get judged.

p99 does not care that your notebook printed ten rows in 300 milliseconds.

It waits until the Kafka topic spikes, the state store grows teeth, the object store develops opinions, and someone from Product says the cursed words: But this is real-time, right?

The Real-Time Translation Problem

That is where the room splits.

  • For Product, real-time means the dashboard refreshes before the coffee gets cold.
  • For fraud and payments, real-time means the decision arrives before the card authorization has already escaped the building.
  • For classic Spark Structured Streaming, real-time usually meant something more disciplined: Take the infinite river, slice it into tiny trays of reality, process each slice reliably, checkpoint progress, and move on.

That was micro-batch streaming. And it was brilliant.

The Micro-Batch Curse

Structured Streaming gave Spark a powerful streaming model without abandoning DataFrames, SQL, Catalyst, fault tolerance, or the comforting idea that streaming code could still look suspiciously like batch code.

But micro-batches have a rhythm.

Wake up. Plan. Schedule. Process. Shuffle. Commit. Checkpoint. Repeat.

Even with a one-second trigger, the engine still pays coordination tax. A one-second trigger is not a one-millisecond decision. It’s just a very anxious metronome.

The Nothing Arrives

Then came The Nothing. Officially: Structured Streaming Real-Time Mode (RTM).

RTM doesn’t turn Spark into a nanosecond Flink dragon wearing a hoodie. It doesn’t make slow sinks fast, bad state design healthy, or overloaded clusters heroic. What it does is attack the empty space between Spark’s streaming heartbeats.

For compatible pipelines, RTM changes the execution shape: long-running processing, stages scheduled together, and data passed between stages through streaming shuffle as soon as it is produced.

What RTM Is Actually For

The result is not *magic real-time.* It’s Spark widening its streaming spectrum.

Micro-batch mode remains the sane default for ETL, medallion pipelines, analytics, CDC, dashboards, and workloads where seconds are fine and cost matters.

RTM is for the more dangerous creatures: fraud decisions, operational alerts, personalization loops, feature updates, and systems where milliseconds are not vanity metrics but business requirements.

What RTM Does Not Save You From

The forbidden book is still dangerous.

RTM doesn’t remove production physics. You still need enough task slots, sensible partitions, supported sources and sinks, careful checkpoint intervals, idempotent outputs, and brutal benchmarking against your actual workload.

It changes the latency model. It doesn’t repeal gravity.

🧪 Lab 1: Open the Book

[**lab_01__open_the_book.ipynb](https://github.com/SauronShepherd/the-neverending-real-time-story/blob/main/lab_01__open_the_book.ipynb)** uses a local Spark rate stream with injected millisecond timestamps to expose the micro-batch rhythm.

First, it shows how a heavy trigger turns a smooth stream into chunky staircase output.

Then it lowers the trigger to reveal the planning, scheduling, coordination, and checkpoint overhead that prevents classic micro-batching from behaving like true sub-second real time.

👸🏻 The Childlike Empress Is Ill

The Kingdom Is Not Dying

Sparkland is not dying. It still chews through terabytes for breakfast, powers endless ETL kingdoms, and remains one of the great beasts of distributed data.

But one part of the kingdom has developed a suspicious cough: the belief that near real-time is always good enough.

For dashboards, reports, CDC replication, medallion pipelines, and lakehouse analytics, it often is. Seconds or minutes are perfectly respectable citizens.

But operational systems are different. They do not merely observe the world after the fact. They must act before the moment expires.

The Empress Coughs in Milliseconds

Some data has a short shelf life. A fraud score loses value after the card transaction has already cleared. A personalization signal decays when the user closes the tab. A sensor anomaly becomes archaeology if the turbine has already eaten itself.

Batch remains the ledger of historical truth. Streaming is the nervous system. And modern nervous systems are increasingly connected to machines, not humans: payment risk checks, recommendation updates, ad decisions, cybersecurity responses, feature freshness, and alerting loops where the answer matters only inside a narrow decision window.

The question is no longer: Can we update the dashboard soon? It is: Can we decide before the decision is gone?

Real-Time Is Not a Number

Real-time is not a magic latency badge. It’s a deadline.

Hard real-time means missing the deadline is system failure: avionics, pacemakers, industrial safety loops, airbags. Spark is not built for that, and you should not deploy a seatbelt pretensioner with a DataFrame and vibes.

Soft real-time means lateness degrades value. Fraud scoring, recommendations, alerts, online features, and operational decisions usually live here. The system may survive a late answer, but the answer may no longer matter.

Near real-time means brief, predictable delay. This is the happy home of analytics pipelines, log aggregation, CDC, monitoring dashboards, and “please refresh before the stand-up ends” workflows.

The useful definition is simple:

Real-time means before the decision expires.

Everything else is vendor perfume.

The Latency Gap

Classic Structured Streaming micro-batching is robust, scalable, and still the sane default for many workloads.

The sickness is not Spark. The sickness is pretending micro-batch coordination overhead can shrink forever.

A micro-batch still has a rhythm: discover offsets, plan work, schedule tasks, cross shuffle boundaries, update state, write progress, checkpoint, and commit results.

Spark has improved parts of that machinery, including ways to reduce checkpoint-related latency. But when your target moves from seconds to milliseconds, coordination tax stops being background noise. It becomes the budget.

At that point, lowering the trigger interval is like asking a dragon to tap-dance faster. Impressive. Still a dragon.

Why RTM Exists

Real-Time Mode exists because some workloads need Spark to stop waiting for the next tiny batch ceremony.

  • It does not replace micro-batching.
  • It does not make slow sinks fast.
  • It does not cancel distributed systems physics.
  • It widens the spectrum.

Micro-batch mode remains the reliable workhorse for analytical streaming.

RTM is for operational streaming: the strange, hot, deadline-haunted land where milliseconds are not bragging rights but expiration dates.

The Empress is not dying. But she would like Spark to answer sooner.

🧪 Lab 2: Diagnose the Empress

[**lab_02__diagnose_the_empress.ipynb](https://github.com/SauronShepherd/the-neverending-real-time-story/blob/main/lab_02__diagnose_the_empress.ipynb)** runs a metadata-enriched local rate stream through 5-second and 1-second triggers, then compares p50, p95, and p99 latency against different operational SLA windows.

The goal is to expose the tail-latency illusion: human-scale workloads may pass comfortably, while machine-scale deadlines collide with the structural overhead of classic micro-batching.

🐴 Cairon Brings AURYN

The River Becomes Salami

Before Structured Streaming, declarative DataFrames, and event-time watermarks became things people pretended to understand in architecture meetings, Sparkland had its first streaming amulet: DStreams.

DStreams were honest. They did not promise mystical event-by-event processing.

The name meant Discretized Streams: take an infinite river of data, cut it into fixed-time slices, and process each slice as an RDD. That was the trick.

Spark already knew how to distribute, schedule, retry, and fault-tolerate RDD jobs. DStreams let Spark inherit streaming without rewriting its batch-born soul.

The river became a sausage factory. Every batch interval packed new records into another finite slice of salami, then handed it to the Spark engine.

For logs, metrics, ingestion, and high-throughput analytics, this was a heroic abstraction. But it also meant the stream inherited the heartbeat of a batch system.

Shrink the interval too much, and you do not magically get true real time. You get thinner salami, more scheduling pressure, and a coordinator wondering why everyone is screaming.

What the First Amulet Gave Us

DStreams deserve respect. They gave early Spark users a practical bridge from batch into streaming without forcing them to abandon the RDD world.

Transformations felt familiar. Fault tolerance reused Spark’s execution model. Parallelism came naturally. Stateful operations existed. Windows existed. External sinks could be handled batch by batch.

And then there was the famous escape hatch: foreachRDD.

Beautiful in theory. Dangerous in production.

It let engineers drop into the underlying RDD for each micro-batch and do almost anything.

Sometimes that meant clean custom sink logic. Sometimes it meant turning a streaming application into a haunted drawer full of tiny batch jobs wearing a trench coat.

What DStreams Could Not Give Us

The amulet had limits. DStreams were built around processing-time batch intervals, not the richer event-time and watermark model that later arrived with Structured Streaming.

Handling out-of-order data, late arrivals, and clean state cleanup required more manual engineering than most teams wanted to admit.

They also lived below the modern SQL/DataFrame world. That meant no Catalyst optimizer calmly peering into your streaming logic, no declarative query planning, and no unbounded-table mental model.

Stateful APIs like updateStateByKey and mapWithState were powerful, but lower-level and easier to abuse than the newer structured state model.

And the biggest limitation was architectural: DStreams made streaming fit inside a distributed batch engine.

That was exactly their genius. And exactly their ceiling. They were built for reliable micro-batch streaming. Not millisecond operational decisioning.

By Spark 3.4, the old StreamingContext API was officially marked legacy.

The amulet had done its job. Sparkland needed a new magic.

🧪 Lab 3: The RDD Sausage Factory

[**lab_03__the_rdd_sausage_factory.ipynb](https://github.com/SauronShepherd/the-neverending-real-time-story/blob/main/lab_03__the_rdd_sausage_factory.ipynb)** recreates legacy DStream processing with an old-school StreamingContext and a fixed batch interval.

It shows how Spark turns incoming records into periodic RDD slices, while exposing the practical rough edges of the legacy API: deprecation warnings, notebook-unfriendly output behavior, shutdown awkwardness, and the central architectural truth.

DStreams are finite batch jobs arranged on a conveyor belt. Not continuous event-by-event execution.

🐎 Atreyu Enters the Great Quest

The Known World Ends

Atreyu leaves the known world. The grasslands behind him. The ordinary rules behind him. The comforting illusion that the quest will be mostly walking, some horses, and one responsible adult nearby.

Spark had to do the same. DStreams were the known world: RDDs over time, tiny batches, neat intervals, the infinite river sliced into something a batch engine could understand.

But users wanted more than tiny RDD sausages. They wanted SQL. DataFrames. Windows. Joins. Event time. Watermarks. State. Streaming pipelines that looked enough like batch pipelines that the human brain did not file a formal complaint.

So Spark entered the Great Quest. It stepped out of low-level RDD streaming and into declarative streaming.

The spell was simple: A stream is an unbounded table.

The Infinite Table

DStreams asked you to imagine streaming as a sequence of RDDs. Structured Streaming asks you to imagine streaming as a table that never ends.

Rows keep arriving. The input table keeps growing. The query keeps updating the result table. The output is not a one-time answer. It’s a living answer.

A bounded table gives you an answer. An unbounded table gives you a responsibility.

Catalyst Enters the Fairytale

Every fairytale needs a wizard. Spark has Catalyst, which is worse, because Catalyst is a query optimizer.

Structured Streaming is built on the Spark SQL engine, so streaming queries can use familiar DataFrame and Dataset operations: filters, projections, aggregations, windows, joins, and sink writes.

This was a major leap away from the RDD-first DStream model.

RDDs are powerful, but Spark cannot always understand the meaning inside arbitrary procedural code. DataFrames are more declarative. You describe the result, and Spark gets room to plan.

That’s why Structured Streaming felt like a new era.

Streaming could now speak SQL-shaped language. A stream could be filtered like a table. Grouped like a table. Joined like a table. Monitored like a query.

And feared like a production dependency.

Event Time, Watermarks, and the Swamp

In batch, time is annoying. In streaming, time is a swamp.

There is processing time: when Spark sees the event. There is event time: when the event says it happened. There is ingestion time: when some upstream system received it. And there is dashboard time: when the stakeholder starts yelling.

Structured Streaming made event time just another column. That sounds small, but it changes everything.

A window is no longer a mystical streaming beast. It becomes a grouped aggregation over an event-time column. Then watermarks enter, and the wizard becomes threatening.

A watermark tells Spark how long old event-time state should remain useful before it can be cleaned up and very late records can stop changing the answer.

It’s not magic. It’s a treaty between correctness and memory.

Allow infinite lateness, and you need infinite patience, infinite state, and possibly infinite cloud invoices.

Set the watermark too aggressively, and valid late events fall into the swamp.

This is where Structured Streaming became serious: it gave Spark a cleaner language for windows, lateness, state, and incremental correctness.

Not easy. But finally expressible.

The Beautiful Trick

Here is the trick. Structured Streaming changed the programming model, but the default engine still usually runs with a micro-batch heartbeat.

At the top of the castle, you see the infinite table. In the basement, small batch goblins are still running around with clipboards.

Every trigger, Spark checks for new data, updates the conceptual input table, runs the incremental query, updates the result table, emits changes according to the output mode, records progress, and repeats.

That doesn’t make Structured Streaming fake. It makes it practical.

The unbounded table is the mental model. Micro-batch is the default execution strategy. Those are not the same thing.

Structured Streaming’s genius was separating how engineers think from how the engine usually runs. You write a living SQL/DataFrame query. Spark executes it incrementally, fault-tolerantly, and often as a sequence of small batch jobs.

For analytics, ETL, CDC, dashboards, and medallion pipelines, this is often exactly what you want. But for low-latency operational streaming, the heartbeat remains visible.

The trigger matters. Scheduling matters. Shuffle boundaries matter. Sink commits matter. Checkpoints matter. State stores matter.

You can make the heartbeat faster. You can tune. You can negotiate with the cluster like it is a moody luckdragon.

But the old rhythm is still there. Beautiful table. Tiny batch heartbeat.

The Great Quest continues. The Nothing is not here yet, but now we can see the shape of the thing it will eventually devour.

🧪 Lab 4: The Table That Blinks

[**lab_04__the_table_that_blinks.ipynb](https://github.com/SauronShepherd/the-neverending-real-time-story/blob/main/lab_04__the_table_that_blinks.ipynb)* tests the Structured Streaming “infinite table”* model with an event-time window aggregation under different processing-time triggers.

The lab shows how the declarative DataFrame query remains elegant while the default micro-batch engine still exposes trigger rhythm, planning cost, task scheduling, offset handling, checkpoint pressure, and tail-latency pain when the heartbeat is pushed too aggressively.

🏞️ The Swamps of Sadness

Micro-Batch Was Not Stupidity

The Swamps of Sadness don’t attack with a roar. They slow you down step by step, breath by breath, batch by batch.

Micro-batching is often mocked as “fake streaming,” as if Spark spent years running tiny batch jobs in a trench coat behind the barn.

Micro-batching was not a failure of imagination. It was Spark’s bargain with the distributed systems gods.

Spark was born as a batch engine. It already knew how to schedule tasks, retry failed work, coordinate shuffles, track progress, and process absurd amounts of data without requiring every engineer to become a feral runtime specialist.

So when streaming arrived, Spark asked a reasonable question: What if the infinite river became a rapid sequence of small, reliable batch jobs?

That compromise was brilliant. It gave Spark streaming high throughput, clean recovery boundaries, deterministic offset tracking, replayable failure handling, and reuse of the same SQL/DataFrame engine that powered batch analytics.

For Bronze ingestion, CDC landing, logs, metrics, lakehouse pipelines, and dashboards, this was not sadness. This was civilization.

The sadness begins only when we ask the responsible adult with a clipboard to win an Olympic sprint.

Sadness Has Latency

A micro-batch has a rhythm, and every beat costs time. An event arrives. It may wait for the next trigger. Spark discovers source offsets. The query is planned. Tasks are scheduled. Shuffles create stage boundaries. State may be read or written. The sink commits. Progress is checkpointed. Only then does the output become visible.

Every step exists for a good reason. Every step leaks milliseconds. Average latency is where dashboards go to lie politely.

The danger lives in the tail: the event that arrives just after a trigger starts, hits a wide shuffle, waits behind state-store work, or meets a cluster having a brief JVM existential crisis.

Operational real time isn’t won at the average. It’s won or lost at p95 and p99.

The Artax Moment

Every data engineer eventually has the same idea: What if we just make the trigger smaller?

One second. Half a second. As fast as possible. Surely the swamp will respect our ambition. It does not.

A smaller trigger compresses the rhythm, but it does not remove the ceremony. Even a batch with one lonely record still pays the distributed coordination entry fee: planning, scheduling, offset handling, state bookkeeping, sink commit, and checkpoint work.

If that ceremony takes longer than the trigger interval, the interval becomes decorative.

The stream falls behind. Source lag grows. Batches stack up. The cluster starts behaving like a meeting organizer trying to schedule ten emergency meetings about why meetings are slow.

That is the Artax moment. The horse is not sinking because it stopped trying. It’s sinking because micro-batching is the swamp.

Shorter triggers can improve latency, but they cannot erase the structural floor. To go lower, Spark needs a different execution shape: long-running work, concurrent stage scheduling, and data flowing between stages without waiting for every tiny batch ceremony to fully complete.

The Swamps were never evil. They were just not built for millisecond prophecy.

🧪 Lab 5: StreamingQueryProgress Autopsy

[**lab_05__snapshots_vs_changelogs.ipynb](https://github.com/SauronShepherd/the-neverending-real-time-story/blob/main/lab_05__snapshots_vs_changelogs.ipynb)** dissects Spark’s StreamingQueryProgress telemetry for a stateful window aggregation under 10-second and 1-second triggers.

It compares average latency against p95 and p99 tails to expose the micro-batch penalty tax: lowering the trigger can reduce the average while planning, scheduling, offset handling, state work, commits, and checkpoint overhead still create a structural floor that machine-scale deadlines cannot simply outrun.

🐢 Morla the Ancient One

Morla Is Not Wrong

Morla does not care. She has crawled through the ages for so long that urgency itself has become background noise. Atreyu arrives desperate, covered in dust, carrying the survival of Fantastica on his shoulders. Morla blinks with the emotional intensity of a deprecated configuration warning: We don’t care whether we care.

Every architecture review board has a Morla. Morla sits at the end of the table, slowly opens one ancient eye, and asks the most annoying question in streaming architecture: Do we actually need milliseconds?

And here is the terrifying part. Sometimes Morla is right.

Real-Time Mode widens Spark’s latency spectrum. It does not make every micro-batch pipeline obsolete, shameful, or morally suspicious.

Micro-batching remains the sane choice when the latency contract allows it. Bronze ingestion, CDC landing, medallion pipelines, log aggregation, lakehouse table writes, monitoring dashboards, and human-facing BI often benefit more from stability, replayability, cost control, broad operator support, and clean commits than from shaving milliseconds nobody asked for.

A Gold aggregate table doesn’t become wiser because you update it every 50 milliseconds. It usually just becomes a tiny-file confetti cannon with a finance department attached.

The Correct Question

The dangerous question is: Is Real-Time Mode better?

Better is not a requirement. Better is a vibe wearing a conference badge.

The correct question is: What is the explicit latency contract?

A latency contract says when the answer stops being useful. It defines the deadline, the percentile that matters, the correctness expectation, the cost tolerance, the sink behavior, and the state-retention rules.

  • A fraud decision may need machine-time response before the authorization window closes.
  • A personalization loop may need fresh features while the user is still browsing.
  • A security alert may need to fire before the attacker has finished moving sideways.
  • But a marketing attribution refresh? A weekly executive dashboard? A Bronze table feeding tomorrow’s analytics?

Morla raises one eyebrow. Those don’t need a millisecond dragon. They need a reliable pipeline.

Latency without correctness is just fast lying.

The Danger of Real-Time Cosplay

Real-time cosplay happens when a team builds an operational streaming war machine for a workload that doesn’t care about time.

It has all the accessories: tiny triggers, aggressive autoscaling, Kafka partition drama, p99 dashboards, and a Slack channel called #realtime-war-room.

Then someone checks the business process and discovers the data is reviewed every Monday morning by three people and a pivot table.

That’s not engineering. That’s latency theater.

Changing the Spark trigger doesn’t make the sink faster. Moving to a lower-latency mode doesm’t fix bad state design. Streaming into a lakehouse table every few milliseconds doesn’t remove commit overhead, file layout pressure, metadata costs, or compaction work.

A sports car inside a swamp is still in a swamp.

True engineering maturity is choosing the least dramatic architecture that keeps the promise.

Sometimes that’s Real-Time Mode. Sometimes it’s boring, dependable micro-batch streaming. And sometimes Morla, ancient and miserable, is the only adult in the room.

🧪 Lab 6: Ask Morla

[**lab_06__ask_morla.ipynb](https://github.com/SauronShepherd/the-neverending-real-time-story/blob/main/lab_06__ask_morla.ipynb)** builds a workload strategy matrix that scores pipelines by latency deadline, decision type, sink behavior, correctness needs, cost sensitivity, and state complexity.

The goal is to separate genuine machine-time workloads from real-time cosplay, and to show when boring micro-batch execution is not a compromise. It’s the responsible architecture.

🕷️ Ygramul the Many

The First Escape Attempt

Ygramul is not one creature. It’s a cloud of stinging bodies that looks whole only from far away. Up close, the monster becomes a thousand tiny wounds.

That’s a perfect metaphor for Spark Continuous Processing.

From a distance, it looked heroic: What if Spark stopped doing micro-batches?

Not smaller batches. Not faster triggers. Not the same old coordinator running in panic mode.

Continuous Processing asked a stranger question: What if the tasks just kept running?

Introduced as an experimental execution mode inside Structured Streaming, CP reused the familiar DataFrame-style programming model but changed the runtime underneath.

Instead of repeatedly launching short-lived micro-batch jobs, Spark started long-running tasks that continuously read from the source, pushed records through supported transformations, and wrote to the sink without waiting for a batch boundary to close.

The continuous trigger did not mean: Run a batch every second. It meant: Process continuously, and checkpoint progress at that interval.

That was the monster bite. For the first time, Spark showed that an event did not always need to sit politely in a buffer waiting for the next micro-batch ceremony.

But Ygramul’s gift was venomous.

The Venom

Continuous Processing proved the dream. Then it handed you the warning label. It remained experimental.

Its supported query shape was narrow: projections, filters, maps, and other map-like operations.

The moment you asked for aggregations, event-time windows, joins with serious coordination, or normal stateful logic, the monster bit back.

That limitation was not random. Low-latency record flow and distributed coordination are natural enemies.

Aggregations need keys to meet. Windows need state. Joins need synchronization. Watermarks need time boundaries.

Those are exactly the ceremonies CP was trying to avoid.

The fault-tolerance contract also changed. Micro-batch Structured Streaming can provide stronger exactly-once behavior when paired with compatible sources and sinks. Continuous Processing offered at-least-once guarantees, which means duplicates become someone’s problem. Usually yours.

The ecosystem story was also tiny. Kafka was the serious path. The rate source was useful for testing. Memory and console sinks were useful for debugging.

This was not a general-purpose lakehouse streaming runtime ready to feed every transactional table sink in the kingdom.

Then came the resource requirement. Because CP uses long-running tasks, it needs enough available cores to run all required tasks at the same time. If your source has ten Kafka partitions, Spark needs at least ten cores for the query to make progress.

These are not polite batch tasks waiting their turn. They occupy the road.

And if a task fails, there are no automatic task retries. The query stops and must be restarted from the checkpoint.

Ygramul was fast. Ygramul was also deeply unfriendly.

Why It Matters

Continuous Processing matters because it was Spark’s first serious attempt to escape the micro-batch swamp from inside Structured Streaming.

It proved three important ideas.

  1. Long-running streaming tasks were possible.
  2. Checkpointing frequency could be separated from per-record movement.
  3. The Structured Streaming API didn’t have to imply micro-batch execution forever.

But it also mapped the cliff edge.

  • The more general your streaming query becomes, the more coordination it needs.
  • The more coordination it needs, the harder it is to preserve ultra-low latency.

CP solved latency by narrowing the world. That’s why it never became the final answer. It wasn’t useless. It was a fossil with teeth.

Continuous Processing was the first monster bite. RTM is what happens when Spark studies the venom.

🧪 Lab 7: Ygramul the Many

[**lab_07__ygramul_continuous_processing.ipynb](https://github.com/SauronShepherd/the-neverending-real-time-story/blob/main/lab_07__ygramul_continuous_processing.ipynb)** autopsies Spark’s experimental Continuous Processing mode with the rate source.

First, it shows a supported stateless map/filter-style stream moving through long-running tasks. Then it deliberately adds an aggregation to trigger the engine-level limitation that CP does not support aggregate operations.

Finally, it compares the same stateful shape with micro-batch execution to expose the trade-off: CP buys low-latency flow by giving up much of the distributed coordination machinery that makes general Structured Streaming useful.

🔮 The Southern Oracle

First Gate: Latency

The Southern Oracle doesn’t care about your architecture diagram.

It doesn’t care that your boxes are aligned, your arrows have gradients, or the phrase real-time appears seven times in your design doc and once in the team’s Slack channel name.

The Oracle asks crueler questions: How fast? Measured from where? At which percentile? What happens if you are late? What action changes because this event arrived now?

This is where streaming conversations collapse, because real-time is used for wildly different beasts: a dashboard refreshing every 10 seconds, a fraud decision with a 50 ms budget, and a CDC pipeline landing changes within a minute.

Those are not the same creature.

Latency is not just time spent inside Spark. That is a local rumor. Real latency is the whole path: event creation, serialization, ingestion, queueing, Spark processing, shuffle, state update, sink write, and final business action.

And averages are not enough. Average latency is polite. It wears a clean shirt and lies in quarterly reviews.

Operational systems live in the tail: p95, p99, and maximum delay.

If p50 is 20 ms but p99 is 4 seconds, you don’t have a fast system. You have a system where the unlucky events become archaeology.

The Oracle also asks about replay latency: after a crash, how long does the system need to consume backlog and catch up?

A beautiful low-latency happy path means little if recovery takes 45 minutes.

Second Gate: Correctness

The second gate asks: What does correct mean when something fails?

Without correctness, low latency is just high-performance lying.

  • At-most-once means events may be lost. Useful for sampling, telemetry, or disposable signals. Terrifying for money.
  • At-least-once means events are not lost, but duplicates can happen. Survivable only if downstream systems can deduplicate or apply updates idempotently.
  • Exactly-once sounds like a magic spell, but it’s not a single engine checkbox. It’s an end-to-end contract: replayable source, deterministic processing, checkpointed progress, and a sink that can handle idempotent or transactional writes.

The key survival rule is idempotency. If the same event is processed twice, the final state should still be correct.

An upsert keyed by transaction ID can survive duplicates. A blind append often cannot.

Correctness also means deterministic logic. If replaying the same stream produces different results because your code calls random functions, mutable timestamps, or volatile external APIs inside the hot path, your recovery story has already joined the Swamps of Sadness.

Third Gate: Usefulness

The third gate asks the rudest question: So what?

A 5 ms stream that writes a perfect answer into a system nobody reads in time is just expensive cloud-native jazz.

  • A fraud score matters only if it can intercept the authorization path before the decision leaves the building.
  • A security signal matters only if it can isolate the endpoint before lateral movement spreads.
  • An online feature matters only if the serving layer can use it before the user’s next interaction.

This is where many real-time systems fail. They are fast, correct, and beautifully monitored, but their output lands in a slow sink, stale cache, hourly dashboard, or human workflow that cannot react.

That’s not real time. That’s journalism with better infrastructure.

Real-time is not moving bytes quickly. Real-time is shortening the distance between an event and its consequence.

Pass all three gates — latency, correctness, and usefulness — and your architecture has a reason to exist. Fail one, and the Oracle does not open.

🧪 Lab 8: The Southern Oracle

[**lab_08__the_southern_oracle.ipynb](https://github.com/SauronShepherd/the-neverending-real-time-story/blob/main/lab_08__the_southern_oracle.ipynb) turns vague real-time** requests into an executable workload contract matrix. It scores candidate runtimes against latency, correctness, usefulness, cost, and sink behavior.

The lab shows why machine-time workloads may require lower-latency execution, while human-facing dashboards and CDC compaction often pass with simpler micro-batch designs.

🐺 Gmork in the City of Ghosts

Not Every Engine Thinks in Batches

There is a moment in every Spark engineer’s life when the wolf appears.

Not a friendly wolf. Not a corporate mascot wolf wearing a Patagonia vest.

Gmork. Patient, hungry, and completely unimpressed.

He waits in the City of Ghosts and explains the thing Spark people must eventually hear:

Low-latency streaming is not always batch processing with a smaller trigger.

Spark’s story is expansion. It began as a general-purpose distributed analytics engine, then learned streaming through DStreams, Structured Streaming, Continuous Processing, and now Real-Time Mode.

Its historical question was: How do we make streaming fit into Spark? Flink asks a different question: What if the stream is the world?

Spark’s native home is unified analytics: SQL, DataFrames, batch ETL, lakehouse pipelines, machine learning, and streaming that shares the same ecosystem.

Flink’s native home is continuous dataflow: long-running operators, event time, keyed state, timers, watermarks, checkpoints, and backpressure as part of the runtime’s daily weather.

Gmork is not here to declare Spark obsolete. He’s here to kill a comforting lie: Sometimes low-latency streaming is not a smaller batch. Sometimes it’s a different animal.

Flink’s Streaming-First Shape

Flink treats bounded and unbounded data through the same streaming worldview.

  • A bounded dataset is a stream that eventually stops talking.
  • An unbounded stream is a stream that keeps talking forever, usually at the worst possible time.

A Flink job is a continuous graph of operators. Sources emit records. Operators transform them. Keyed operators keep state. Watermarks advance event time. Timers fire. Checkpoints protect recovery. Backpressure travels upstream when downstream work cannot keep up.

This feels natural in Flink because the engine was designed around that shape.

Keyed state keeps memory close to the part of the stream that owns the key, which is why patterns like fraud velocity checks, session tracking, and user-level personalization feel native.

Timers let operators schedule future actions in processing time or event time.

Backpressure is not treated as shame. It’s the system telling the truth: Something downstream is slower than the river.

That is the Flink worldview. The river flows. The operators live in the river. The engine learns to breathe underwater.

Spark and Flink Are Aligned Differently

This is where framework debates usually become useless.

Someone says Spark is better. Someone says Flink is better. A third person opens a benchmark from 2019 and everyone loses the will to live.

The useful question is not which engine is spiritually superior. The useful question is: Which engine aligns with the workload?

Spark shines when streaming is part of a larger analytics platform: lakehouse ingestion, medallion pipelines, SQL transformations, historical reprocessing, batch-and-stream code reuse, governance, and teams already built around Spark.

Flink shines when the application is streaming-first: complex event-time graphs, deeply stateful logic, fine-grained timers, low-latency operational decisions, and systems where continuous backpressure and keyed state are central to the product.

Spark historically made streaming practical by slicing infinity into manageable micro-batches. Flink made streaming practical by treating infinity as normal.

Real-Time Mode changes the Spark side of the map. It gives Structured Streaming a lower-latency execution path with long-running work, simultaneous stage scheduling, and streaming shuffle.

Some workloads that once needed a separate streaming engine may now stay inside Spark.

But RTM doesn’t erase Flink’s worldview. It gives Spark a new room in its own castle. It doesn’t move the castle into the river.

Gmork’s Warning

Don’t turn this into religion. Spark doesn’t eliminate Flink. Flink is not universally superior. RTM is not a magic migration shield.

The correct architecture depends on the contract: latency, correctness, state complexity, event-time behavior, sink semantics, team skills, operational burden, and what action actually happens downstream.

The execution spectrum is simple.

  • Spark batch and AvailableNow are for bounded work, backfills, and scheduled ingestion.
  • Spark micro-batch is for reliable analytical streaming, lakehouse writes, CDC, dashboards, and pipelines where seconds are fine.
  • Spark Real-Time Mode is for compatible operational streaming that needs lower latency while staying inside Structured Streaming.
  • Flink is for streaming-first applications where continuous dataflow, complex keyed state, timers, and event-time machinery are the core of the system.

Choose the least dramatic engine that keeps the promise. Gmork is not asking you to abandon Sparkland. He’s asking you to stop pretending every river is a lake with anxiety.

🧪 Lab 9: Two Worlds, Same River

[**lab_09__two_worlds_same_river.ipynb](https://github.com/SauronShepherd/the-neverending-real-time-story/blob/main/lab_09__two_worlds_same_river.ipynb)** runs the same stateless risk-filtering and enrichment logic through Spark micro-batch, Spark Continuous Processing, and a PyFlink-style path, comparing how each engine coordinates work: micro-batch exposes trigger-driven planning and commit overhead, Continuous Processing exposes operator-shape limits, and Flink shows the continuous-dataflow worldview where long-running operators, state, timers, and backpressure are native citizens.

✨ The Nothing Over Fantastica

What The Nothing Devours

The Nothing doesn’t arrive like a monster. No claws. No roar. No giant shadow over the Ivory Tower. It arrives as absence.

In Sparkland, The Nothing eats waiting. Not the useful kind. Checkpoints, commits, replay boundaries, and correctness still matter. Let us not become marketing goblins.

It eats the dead air between micro-batch heartbeats: the record that arrives one millisecond too late and gets punished with a full trigger interval of silence.

That’s the old pain Real-Time Mode attacks. Not by changing the Structured Streaming story you write, but by changing how the engine breathes underneath it. The bell stops being the center of the universe.

The Trigger That Lies to Your Micro-Batch Brain

RTM looks deceptively small from the outside. You change the trigger. Your micro-batch brain panics.

A five-minute real-time trigger sounds like disaster: Surely Spark will wait five minutes before processing data? No. That is the old intuition speaking.

In RTM, records are processed as they arrive during long-running execution. The interval is about checkpointing, progress reporting, and replay distance after failure.

  • Longer intervals mean less bookkeeping and fewer checkpoints, but more data may need replay after a crash.
  • Shorter intervals mean fresher progress and smaller replay windows, but checkpoint work can creep back into the latency path.

The trigger stops being the dinner bell. It becomes the survival diary.

The Castle and the Managed Kingdom

One trap hides in the naming. Spark now has three related low-latency stories, and they are not the same spell.

1. Open-source Continuous Processing. This is the old Spark 2.3-era path: .trigger(continuous="1 second"). It is real, open-source, and very low-latency, but still experimental and narrow: at-least-once guarantees, map-like operations only, limited sources and sinks, enough-core requirements, and no automatic task retries.

Great for Kafka in, parse, filter, route, Kafka out. Not great for windows, joins, watermarks, stateful business logic, or lakehouse writes.

2. Open-source Spark 4.1 Real-Time Mode. This is newer and official Apache Spark, not just a vendor label. But its first public support is intentionally limited: Spark 4.1 calls out Scala stateless Structured Streaming workloads. So vanilla Spark is no longer only “micro-batch or old Continuous Processing,” but the RTM surface is still young.

🤔 Why mention Scala if Spark runs on the JVM anyway? Because engine internals and user-facing support are not the same thing. Spark may run under the hood in the JVM world, but a streaming feature is only production-usable when its public API, trigger syntax, analyzer rules, sources, sinks, UDF behavior, and language bindings are officially supported. So when Spark 4.1 calls out Scala stateless RTM support, that does not mean Python and Java automatically get the same support surface for free.e The engine may be there. The contract may not be.

3. Databricks Real-Time Mode. This is the managed, productized version most people will see with .trigger(realTime="5 minutes"). It uses the same big idea — long-running execution, stages scheduled together, streaming shuffle, and records moving as they arrive — but wraps it in a Databricks-specific support matrix.

🤔 So is Databricks RTM just branding? No. It’s the managed, productized version of the RTM idea: familiar trigger syntax, documented runtime rules, broader language examples, supported compute modes, and a platform-specific matrix for sources, sinks, output modes, UDFs, joins, windows, and stateful APIs. Do not ask only: Does Spark have RTM? Ask: Does my runtime support this exact query shape?

In a nutshell:

  • Continuous Processing is the old narrow tunnel.
  • Spark 4.1 RTM is the new official tunnel, still narrow at first.
  • Databricks RTM is the managed highway: wider in places, but full of lane rules.

Brand names do not run streaming queries. Analyzers do.

What The Nothing Is Not

RTM is not exactly-once fairy dust. A trigger change does not make slow sinks fast, unsafe writes idempotent, or flaky databases transactional. Correctness still needs a replayable source, deterministic logic, checkpointed progress, and a sink that survives retries.

RTM is not universal operator support. Stateless pipelines are the easy path; aggregations, joins, UDFs, custom state, and table sinks need runtime-specific checks.

RTM is not free latency. Long-running stages need enough task slots to stay alive together. Under-provision the cluster, and the monster does not fly. It just sits there looking expensive.

RTM is not a lakehouse small-file cure. Transaction commits, compaction, indexing, metadata, and slow acknowledgments remain physics. RTM just gets you to the bottleneck sooner.

And RTM is not “Flink is dead.” It narrows the gap for compatible Spark workloads, but Flink remains streaming-first territory for continuous operators, keyed state, timers, watermarks, backpressure, and complex event-time graphs.

🧪 Lab 10: Summon The Nothing

[**lab_10__summon_the_nothing.ipynb](https://github.com/SauronShepherd/the-neverending-real-time-story/blob/main/lab_10__summon_nothing.ipynb)** compares a classic 1-second micro-batch heartbeat with a continuous-style execution shape over the same stateless risk-filtering pipeline, using local telemetry to show where time disappears into trigger coordination, offset handling, planning, commit logging, and sink work; the goal is not to pretend a notebook fully reproduces production RTM, but to make the architectural difference visible: batch-shaped repetition versus long-running dataflow that removes the old waiting room.

🏰 The Ivory Tower

Long-Running Batches

Classic Structured Streaming repeats the same ceremony: discover offsets, plan, schedule, run, write, commit, checkpoint.

RTM changes the geometry by keeping execution alive, so records move as they arrive instead of waiting for the next micro-batch.

That makes long-running batch sound cursed.

A five-minute batch? For real time? Yes, because in RTM, the interval isn’t the processing heartbeat. It’s the checkpoint and progress-reporting cadence.

Longer intervals reduce bookkeeping but increase replay distance. Shorter intervals improve recovery freshness but can push checkpoint pressure back into p99.

The trigger is no longer the dinner bell. It’s the diary.

Concurrent Stage Scheduling

Classic Spark thinks in stages. Upstream work runs, downstream work waits, and the baton moves only after the stage boundary clears.

Great for throughput. Bad for records that are ready now.

RTM schedules compatible stages together so source, shuffle, processing, and sink tasks can stay alive at the same time. That removes waiting. It also pins capacity.

Micro-batch can reuse task slots stage by stage. RTM needs enough slots for the active graph to stand up all at once.

No slots, no river. Just a queue with better branding.

Streaming Shuffle

Shuffle is Spark’s superpower and its tax collector.

Classic shuffle often means upstream tasks partition and materialize data, then downstream tasks read it later. Great for scalable analytics. Painful for low-latency records.

RTM needs that wall to become a gate. Streaming shuffle lets data move between active stages as soon as it is produced, instead of waiting for the whole upstream stage to finish.

That’s why the three ideas belong together:

  • Long-running execution keeps tasks alive.
  • Concurrent scheduling keeps downstream stages ready.
  • Streaming shuffle lets records flow between them.

Remove one, and the old waiting room starts growing back.

This also explains RTM’s support boundaries. Projections and filters are easy to pipeline. Aggregations, joins, deduplication, and event-time logic may require state, buffering, synchronization, and correctness barriers.

Those limits are not random paperwork. They are where continuous flow meets distributed truth.

The Cost of Keeping the Kingdom Awake

RTM lowers latency by keeping more of the execution graph alive. That is powerful. It is also expensive.

Micro-batching is efficient because it works in chunks and reuses resources across stages. RTM trades some of that efficiency for immediacy. Tasks may sit ready, waiting for data, because readiness is exactly what removes scheduling jitter.

So capacity planning becomes part of the latency contract. You need enough task slots for active stages, source parallelism that matches ingestion, shuffle partitioning that avoids both backlog and waste, and a sink that acknowledges writes inside the business deadline.

If Spark processes a record in three milliseconds but the target database commits in two hundred, your end-to-end latency is not three milliseconds. It’s two hundred and three milliseconds, plus whatever the rest of the path charges you.

RTM doesn’t make slow sinks fast. It gets you to the bottleneck sooner.

The Ivory Tower’s lesson is simple: Real-Time Mode is not a faster metronome. It’s a different execution shape. But the kingdom must stay awake to use it.

🤔 Does Flink pay this cost too? Yes. Flink also keeps operators alive, reserves slots, holds state, checkpoints progress, and deals with backpressure.

That’s not free. The difference is that Flink was born in that world. Continuous execution is its normal shape.

In Spark, micro-batch mode can reuse resources stage by stage. RTM moves Spark closer to the always-on model, so the cost becomes more visible: active stages must coexist instead of politely taking turns.

Same physics. Different default lifestyle.

🧪 Lab 11: Draw the Ivory Tower

[**lab_11__draw_ivory_tower.ipynb](https://github.com/SauronShepherd/the-neverending-real-time-story/blob/main/lab_11__draw_ivory_tower.ipynb)** decomposes streaming execution plans and telemetry to compare micro-batch coordination costs with RTM-style capacity requirements, using task-slot modeling to show where latency disappears into planning, offset handling, commit logs, checkpointing, shuffle boundaries, sink acknowledgment, and under-provisioned execution graphs.

📖 The Old Man of Wandering Mountain

The Chronicler Does Not Flatter You

The Old Man writes the story as it happens. In Sparkland, that chronicler is the engine contract.

Docs promise. Blog posts excite. Release notes bless. But the contract decides what Spark will actually run.

RTM looks tiny from the outside: one trigger, one familiar query, one innocent change. Underneath, it asks harsher questions:

Can the source read without a fixed end offset? Can the reader wait when the river is quiet? Can partition offsets merge into recoverable progress? Can the sink, output mode, and operators survive without rebuilding the old micro-batch wall?

RTM is not just a trigger. It’s a chain of promises.

Start at the Trigger

The journey begins with the trigger — but the spelling depends on the kingdom.

  • In open-source Spark 4.1+, the JVM API exposes **Trigger.RealTime(...)**.
  • In Databricks, use the friendlier writer syntax: **.trigger(realTime="5 minutes")**.

Either way, the idea is not: Wake up every few seconds and process whatever arrived. That’s the old micro-batch brain talking.

In RTM, records flow during long-running execution. The duration is not a buffering interval; it is the page break for progress, checkpoints, and recovery bookkeeping.

That is why the default five minutes sounds terrifying only until you remember Spark is not waiting five minutes to touch the data.

  • Longer intervals mean less bookkeeping but more replay distance.
  • Shorter intervals mean fresher progress but more checkpoint pressure.

The trigger is not the dinner bell. It’s the chronicler’s page break.

The Gatekeepers

Before Spark lets the query run, the analyzer checks whether the plan belongs in Real-Time Mode. They check the output mode, the sink, the source, the operators.

Some operators need buffering. Some need synchronization. Some need finality. Some need state semantics that do not naturally fit row-by-row flow.

The Source and Reader Contract

A compatible source must prepare for real-time execution, create input partitions from a starting offset, and later merge partition progress back into one checkpointable global offset.

That last part is the skeleton of correctness. RTM is not low latency because it ignores recovery. It’s low latency because it keeps data moving while still leaving a map behind. The reader contract makes that physical.

In micro-batch mode, reaching the end of the assigned range means the task is done. In RTM, reaching the current tip of the source only means the river is briefly quiet. So the reader waits.

Timeout-aware reads let a task block for new data, wake up when records arrive, and report its current partition offset back to Spark.

Kafka, the Familiar Creature

Kafka is the familiar creature in this story because its partitioned log model fits the contract naturally.

Each topic partition can map cleanly to streaming progress. Each reader can report local offsets. Spark can merge those offsets into a global recoverable position.

That’s why Kafka-style pipelines are the easy mental model for RTM: read, parse, filter, enrich, route, write, checkpoint.

Records may move quickly. Recovery still needs coordinates.

The Full Handshake

The RTM story is not “add trigger, receive magic.”

A real-time query selects the trigger, passes analyzer checks, starts from known offsets, keeps compatible readers alive, moves records through the supported plan, reports partition progress, and merges that progress into something Spark can recover from.

RTM is not a vibe. It’s a contract: trigger, analyzer, source, reader, offsets, checkpoint, sink, and operator support.

The Old Man does not care what your slide deck says. He writes what the engine can actually do.

🎶 Uyulala, the Invisible Oracle

Processing Time Is What Spark Sees

Uyulala is invisible, present, and impossible to ignore. Event time is like that.

Spark sees tasks, offsets, triggers, sinks, commits, and checkpoints. But event time whispers from inside the payload: I happened at 12:03.

Maybe that timestamp is true. Maybe the producer clock drifted, a mobile app went offline, an IoT sensor cached readings, or someone upstream forgot that time zones are where hope goes to die.

Processing time is different: it’s when Spark sees the record.

That makes it useful for telemetry — ingestion delay, scheduling drift, trigger duration, executor timing, and p99 behavior — but it’s not business truth.

If a payment happened at 10:00 and Spark sees it at 10:05, processing time says: fresh event. Event time says: late evidence.

Spark can process a record fast and still misunderstand when reality happened.

Event Time Is What the Event Claims

Event time lives inside the data: a click timestamp, card-swipe timestamp, sensor timestamp, or business clock carried by the event itself.

Structured Streaming made this powerful because event time became just another column. You can group by event-time windows, update older windows when late records arrive, and reason about out-of-order streams without pretending the network is a polite queue.

That is the good news. The bad news is state.

If Spark accepts late data, it must keep old windows alive long enough to update them. Keep them alive forever, and the state store becomes a cursed forest: memory grows, checkpoint pressure grows, latency grows, and finance grows suspicious.

So the engine needs a deadline. That deadline is the watermark.

Watermarks Are Memory With a Deadline

A watermark is not magic. It’s a treaty between correctness and memory.

You tell Spark how much lateness your business will tolerate. Spark tracks event-time progress and uses that delay to decide when old state can be cleaned up and when very late records may stop changing the answer.

The rough mental model is:

watermark = maximum observed event time − allowed lateness

If Spark has seen events up to 12:30 and your lateness threshold is 10 minutes, the watermark moves around 12:20. But the guarantee matters.

Spark should not drop data that is less delayed than your threshold. Data older than the threshold may be dropped.

“May” is doing real work here, because output mode, query shape, partition progress, and execution timing matter.

  • A short watermark keeps state small but risks rejecting valid late data.
  • A long watermark protects correctness but forces Spark to carry more history, increasing memory, storage, checkpoint pressure, and recovery cost.

Ten minutes is not an engine default blessed by angels. It’s a product decision disguised as an API call.

RTM Does Not Abolish Time Complexity

Real-Time Mode reduces the time a record spends waiting inside Spark’s execution machinery. It doesn’t fix time itself.

A row can fly through RTM in milliseconds and still carry an event timestamp from an hour ago. RTM can reduce processing latency, but it cannot repair broken clocks, validate mobile timestamps, or make out-of-order events line up like obedient schoolchildren. These are different problems.

RTM is about data movement latency. Event time is about business meaning. Watermarks are about bounded state.

Confuse them, and your system becomes a fast liar.

Before chasing milliseconds, carry event timestamps explicitly, measure processing time separately, define lateness as a business contract, and test what happens when records arrive late, out of order, duplicated, and emotionally unavailable.

Uyulala is invisible. Ignore her, and the whole kingdom starts telling time with a broken clock.

🧪 Lab 12: Listen to Uyulala

[**lab_12__uyulala_event_time.ipynb](https://github.com/SauronShepherd/the-neverending-real-time-story/blob/main/lab_12__uyulala_event_time.ipynb)** uses local JSON file batches to compare event time, processing time, output time, and Spark watermark progress in a 10-second event-time window aggregation, showing how out-of-order records can still update their true historical windows while records beyond the watermark boundary may be rejected to keep state bounded.

When Spark Structured Streaming opens a portal, p99 becomes the monster, micro-batches become salami, and Real-Time Mode rides in yelling: “I’m not magic, I just hate waiting.”

When Spark Structured Streaming opens a portal, p99 becomes the monster, micro-batches become salami, and Real-Time Mode rides in yelling: “I’m not magic, I just hate waiting.”

👦🏻 Bastian Enters Fantastica

When the Reader Enters the World

Bastian matters when he stops reading and enters the story.

RTM matters for the same reason: not when data is reported faster, but when it can still change an action — a payment, recommendation, alert, feature, match, or intervention.

Once the consequence has expired, low latency is just expensive narration.

Fraud Detection

Fraud is the classic answer before the moment dies workload.

A transaction arrives, gets enriched, scored, checked against recent behavior, and routed: approve, decline, review, or step-up authentication.

Five minutes later, that score is useful for investigation. Not for stopping the transaction that already escaped wearing sunglasses.

RTM can move Spark into the operational path, but fraud is rarely stateless. It needs keyed state, velocity signals, device and merchant context, deterministic decision IDs, and idempotent outputs.

Low latency without idempotency is duplicate chaos with better posture.

Live Personalization

Live personalization is fraud detection’s marketing cousin: fewer firewalls, same deadline anxiety.

If a user clicks three premium hiking boots, the recommendation layer should react before the session dies.

RTM helps when Spark updates an online cache, feature store, or recommendation router for the active journey.

If the result lands in a table for tomorrow’s campaign email, that’s not live personalization. That’s archaeology with emojis.

ML Feature Serving

Models need fresh inputs, not just clever algorithms.

A fraud model using stale rolling counts is an expensive horoscope. A recommender using yesterday’s session signals is politely guessing.

RTM helps with fast-decaying features: rolling counts, recent category affinity, short-lived error rates, live session profiles, and freshness-sensitive behavior.

The advantage is reuse: Spark teams can keep hot-path feature logic close to the offline Structured Streaming and SQL ecosystem, reducing training-serving skew.

But the whole chain must match the speed. If the online store exposes the feature three seconds later, your latency is three seconds wearing a tiny Spark hat.

IoT Anomaly Detection

IoT is where streaming starts touching machinery.

A delayed click hurts engagement. A delayed turbine alert can become very expensive noise.

RTM matters when sensor events can still trigger action: shutoffs, alerts, routing changes, pressure controls, or edge interventions.

The useful cases usually need state: rolling averages, variance, spikes, device baselines, and cross-sensor context.

But field data is messy. Gateways buffer. Devices reconnect. Timestamps drift. Old records arrive in clumps.

RTM reduces Spark-side delay. It doesn’t solve event-time truth. You still need watermarks, lateness rules, and a clean split between live alerts and late diagnostics.

Gaming and Session Telemetry

Gaming systems are streaming systems with better graphics and angrier users.

Some events matter later: retention, economy balancing, ledgers, achievements.

Others matter only during the session: matchmaking, anti-cheat, chat moderation, routing, incentives, and rewards.

A reward tomorrow is a database record. A reward at the moment of victory is the experience.

RTM is for streams where the story changes only if Spark enters before the page turns.

🐉 AURYN Grants Wishes

Wish One: Lower Latency

AURYN grants wishes. Wonderful, until you remember every wish sends an invoice.

Spark Real-Time Mode grants the obvious wish first: lower record-level latency.

Classic micro-batch Structured Streaming remains resilient, scalable, and excellent for many workloads, but its trigger-driven rhythm adds ceremony. RTM changes the execution shape so compatible records move through long-running execution instead of waiting for the next batch bell.

That matters when value decays quickly: fraud decisions, live recommendations, urgent alerts, fresh features, and operational actions that must happen before the moment expires.

RTM moves Spark closer to the hot path. Not just reporting what happened. Helping decide what happens next.

Wish Two: Familiar APIs

The second wish is less flashy but valuable: You don’t throw away the Structured Streaming spellbook.

For compatible workloads, teams can keep familiar DataFrame logic, SQL-shaped transformations, checkpoint habits, deployment patterns, and monitoring practices.

That matters because streaming bugs are rarely just syntax bugs. They are semantic goblins: wrong output mode, unsafe sink, bad watermark, duplicate handling, immortal state, or late data quietly rewriting the past.

The API stays familiar. The runtime contract gets stricter.

Wish Three: Fewer Engines

RTM also reduces engine sprawl. When micro-batch latency was not enough, teams often had to add Flink, Kafka Streams, custom services, or some cursed YAML kingdom with three owners and no daylight.

Those tools can be excellent. But every new engine adds operational gravity: deployment model, state semantics, recovery procedures, metrics, connectors, security rules, and on-call expertise.

RTM doesn’t eliminate specialized streaming engines. It reduces the number of workloads that must leave Spark only to chase lower latency.

That’s especially useful when offline history, streaming features, governance, lineage, and ML pipelines already live in Spark.

Wish Four: Operational Continuity

Low latency is not impressive if nobody can recover the job at 02:00.

If your organization already runs Spark at scale, you already have muscle memory: deployment pipelines, cluster sizing, executor debugging, checkpoint monitoring, lineage controls, access policies, and people who know which logs are lying.

RTM lets compatible low-latency workloads inherit that world.

They still need capacity planning, supported sources and sinks, deterministic logic, enough task slots, and idempotent writes. But they don’t arrive as a mysterious second kingdom with incident runbooks written by one engineer who left in 2023.

Operational continuity is boring. That’s why it saves weekends.

Wish Five: Better Operational AI Pipelines

Modern AI systems need fresh context.

An agent reading yesterday’s customer state is a confident intern with stale notes. A fraud model using old velocity features is an expensive horoscope. A recommender using delayed session behavior is politely guessing.

RTM can help Spark feed feature stores, online caches, vector context stores, and model-serving paths with fresher signals.

The advantage is not only speed. It’s consistency.

Spark teams can keep feature definitions closer across offline training and online serving, reducing training-serving skew and making lineage easier to audit.

But the whole path must match the promise. If Spark updates a feature in milliseconds and the online store exposes it three seconds later, your real latency is three seconds wearing a tiny AURYN necklace.

RTM grants powerful wishes. It doesn’t pay their cost for you.

🧪 Lab 13: Make a Wish, Measure the Price

[**lab_13__tradeoff_matrix.ipynb](https://github.com/SauronShepherd/the-neverending-real-time-story/blob/main/lab_13__tradeoff_matrix.ipynb)** models RTM’s benefits against micro-batch and multi-engine alternatives using p99 latency, throughput, infrastructure cost, team familiarity, complexity, and operational continuity, showing when RTM is the balanced choice and when micro-batch or a streaming-first engine is more honest.

🪞 Every Wish Costs a Memory

The Invoice Arrives

Bastian’s wishes are powerful. That’s the problem.

With AURYN, nothing is free. Spark Real-Time Mode works the same way, just with more checkpoints and fewer luckdragons.

RTM grants real wishes: lower latency, familiar APIs, fewer engines, and Spark operational continuity. But every wish sends an invoice.

Sometimes the price is dedicated compute. Sometimes it’s a strict support matrix. Sometimes it’s longer replay after failure. Sometimes it’s idempotent sink design. Sometimes it’s state growing teeth in the dark.

RTM is not dangerous because it’s fake. It’s dangerous because it works well enough to tempt teams into skipping the boring parts.

This is the mirror: not the launch slide, but the systems view at 02:00 after a failed restart.

The Support Matrix Has Teeth

A query that runs in micro-batch mode doesn’t automatically deserve a real-time trigger and a little crown.

RTM has stricter rules around sources, sinks, output modes, operators, UDFs, joins, stateful logic, and table writes. The exact list depends on your Spark distribution and runtime version, so “it worked in my notebook” is not a certification strategy.

Narrow transformations are friendly: filters, projections, simple enrichment, row-level routing.

Complex joins, windows, stateful APIs, unsupported sinks, and anything that reintroduces buffering need careful checks.

And then there is foreachBatch. Beloved. Dangerous. Micro-batch-shaped.

If your pipeline depends on custom batch-level merges, arbitrary multi-sink writes, or hand-rolled deduplication inside foreachBatch, you don’t have a one-line RTM migration. You have homework.

Dedicated Resources Are Not Optional

RTM keeps the kingdom awake. Sources, downstream stages, and streaming shuffle paths must stay alive together so records can move immediately.

That reduces waiting, but pins compute. If the cluster cannot hold the active graph, real time becomes an expensive queue.

The trigger keyword does not manufacture cores.

Checkpoints Become a Trade-Off Dial

RTM does not abolish checkpointing. It turns the interval into a recovery dial.

  • Longer intervals mean less bookkeeping but more replay after failure.
  • Shorter intervals mean fresher progress but more metadata pressure.

Pick blindly, and the mirror laughs.

Fast Sinks Can Still Lie

A fast Spark pipeline writing to a non-idempotent sink is a corruption cannon.

RTM does not erase retries, failures, duplicate delivery, or replay. Your sink must survive seeing the same event twice.

That means deterministic output IDs and safe writes: transaction ID, event ID, decision ID, model version, rule version, or another stable business key.

An upsert can survive replay. A blind append may duplicate truth. A REST call may trigger damage twice.

Low latency without idempotency is fast regret. And the slowest participant owns the budget.

If the sink takes 150 milliseconds to acknowledge, you do not have a 4-millisecond system. You have a 150-millisecond system with excellent Spark posture.

State Still Gets Heavy

Stateless RTM pipelines are cottages: parse, filter, enrich, route, sleep peacefully. Stateful RTM pipelines are castles with basements.

Once you track history — fraud velocity, sessions, profiles, baselines, rolling counters — you inherit state-store physics. State needs TTLs, checkpoints, recovery, skew handling, and schema migration discipline.

A missing TTL grows a cursed forest. A hot key wrecks p99. A giant checkpoint turns recovery into archaeology.

RTM moves records faster. It doesn’t make bad state design healthy. It makes it hurt sooner.

Observability Must Become Record-Level

Micro-batch metrics still help, but RTM needs a sharper lens.

A long-running query can look alive while individual records suffer. Average latency can behave while p99 screams into a pillow.

Track percentile latency, source lag, state delay, shuffle delay, sink acknowledgment time, and source-to-action latency. Otherwise, RTM becomes fast in demos, mysterious during incidents, and very good at hiding where the dragon actually lives.

AURYN punishes wishes without memory. Spark RTM is the same.

Use it when the decision expires fast enough to justify the price. Otherwise, keep the boring micro-batch pipeline, monitor the tail, and go home before the Swamps of Sadness open a Jira ticket.

🧪 Lab 14: The Price of Wishes

[**lab_14__price_of_wishes.ipynb](https://github.com/SauronShepherd/the-neverending-real-time-story/blob/main/lab_14__price_of_wishes.ipynb)** simulates failure recovery and replay duplication in a local streaming decision pipeline, showing how deterministic output keys protect business truth when records are processed again after rollback; the lab compares raw row counts with unique business transactions, then applies idempotent deduplication to prove that low latency is only safe when the sink can survive seeing the same event twice.

🤴 The City of Old Emperors

It Worked in the Prototype

The City of Old Emperors is where streaming projects go after one fatal sentence: It worked in the developer prototype.

RTM is dangerous not because it’s broken. It’s dangerous because it can make bad architecture look promising for fifteen minutes.

Mistaking the Interval for a Micro-Batch Timer

The RTM interval is not a tiny batch timer. It’s the recovery diary: checkpoint, metrics, and progress cadence.

  • Longer intervals reduce bookkeeping but increase replay distance.
  • Shorter intervals improve freshness but add metadata pressure.

Tune it as an operational trade-off, not a panic knob.

Under-Provisioning Task Slots

RTM needs the active graph to stand up at once.

A cluster that was fine for micro-batch may be too small for real-time execution.

If there aren’t enough slots, records queue, p99 climbs, and the magic becomes traffic. Twelve goblins. Four chairs.

Trusting Average Latency

Average latency is where production incidents hide. A beautiful p50 can still fail the business if p99 is awful.

Measure the full path: source queue, Spark processing, state, shuffle, sink acknowledgment, and final action.

Real time is won in the tail. Not the average.

Hot Keys and Partition Skew

A hot key is one key that ruins everyone’s day.

RTM does not exorcise skew. Sometimes it makes skew hurt faster.

The fix is data design: salting, rate limits, quarantine, pre-aggregation, better partitioning, or separating pathological streams.

A low-latency trigger is not a partitioning strategy.

Python in the Hot Path

Python is wonderful. Python is also where latency sometimes goes to fill out customs forms.

  • Use Spark SQL expressions, built-in functions, and Catalyst-friendly logic on the hottest path.
  • Use Python only when measured p99 proves it survives the deadline.

Affection is not a benchmark.

Slow Sinks and Non-Idempotent Chaos

Your pipeline is only as fast as the slowest system that must acknowledge the result.

RTM gets records to the sink faster. It doesn’t make the sink sane.

Slow sinks hurt latency. Non-idempotent sinks corrupt truth.

Use deterministic IDs, safe upserts, and replay-tolerant writes. Otherwise, low latency just delivers regret sooner.

Unsupported Operator Roulette

Changing the trigger is not a migration plan. It’s gambling with stack traces.

RTM support depends on runtime, language, source, sink, output mode, operator, and state API.

Validate the full production plan, not the toy query.

The analyzer is not being rude. It’s saving you from becoming an Old Emperor.

Believing RTM Kills Flink

Spark RTM DOES NOT kills Flink

RTM keeps more low-latency workloads inside Spark. Flink still fits streaming-native systems with deep state, timers, side outputs, and event-time-heavy flows.

Leave framework wars to marketing slides. RTM is not a shortcut around distributed systems. It’s a sharper tool inside them.

Use it with contracts, measurements, compatibility checks, and failure drills. Otherwise, the City of Old Emperors is always accepting new residents.

🌧️ Falkor Flies, but Please Check the Weather

RTM lets compatible Spark streams fly closer to the operational path: fraud, personalization, ML features, alerts, telemetry, and AI context.

But faster flight still needs weather checks.

Start Stateless

Begin with parsing, validation, filters, projections, enrichment, and routing. If p99 is unstable here, state will only make the problem worse.

Add State Only With Rules

State needs an owner key, TTL, eviction, recovery, skew handling, and schema migration plan. No TTL means cursed forest. One hot key means p99 pain.

Benchmark Real Traffic

Synthetic streams are classrooms. Production is the zoo. Test real payloads, bursts, skew, malformed records, late data, and poison messages. Measure p50, p95, p99, and max end-to-end latency.

Make Sinks Idempotent

Retries and replay still happen. Every output needs a deterministic business key. Prefer upserts, deduplication, overwrites, or transactional sinks. Blind appends and irreversible REST calls are where fast regret begins.

Tune the Interval Deliberately

The RTM interval is a recovery and progress dial. Longer means less overhead, more replay distance. Shorter means fresher progress, more metadata pressure. Choose with load tests and failure drills.

Split Hot and Cold Paths

Use RTM only where the event expires quickly. Keep ingestion, compaction, BI, reporting, data quality, and offline training in micro-batch or batch. A Kafka topic can feed both worlds.

Ship With a Flight Plan

Before production, confirm runtime support, validate the full query, model task slots, test replay, prove sink idempotency, instrument end-to-end latency, and keep a rollback path.

Falkor may fly. Production requires weather radar.

🧭 The Final Gate: Decision Framework

Requirements choose architectures. Engines are where the consequences land.

Before choosing Spark micro-batch, Spark RTM, Flink, or anything else with a booth, answer:

What is the p99 deadline? What correctness guarantee is required? How much state exists, and how does it expire? Which sources, sinks, and operators are mandatory? How fast must recovery happen? What can the team operate safely? What action changes because this event arrived now?

Choose Spark Micro-Batch When

Choose Spark micro-batch when seconds or minutes are good enough.

Use it for ingestion, CDC, medallion pipelines, logs, BI refreshes, data quality, historical features, and compaction.

It’s the boring machine that survives weekends.

Choose Spark RTM When

Choose Spark RTM when the decision expires fast and the query fits the supported RTM envelope.

Use it for fraud gates, live recommendations, online features, urgent alerts, session routing, and AI context refresh.

But the contract must pass: runtime support, enough slots, idempotent sink, production-shaped p99, and safe recovery.

RTM is lower-latency Spark with a price tag.

Choose Flink When

Choose Flink when the stream is the product.

Use it when the system depends on continuous dataflow, complex keyed state, fine-grained timers, event-time-heavy logic, backpressure, and long-running state machines.

That doesn’t mean Spark failed. It means the workload belongs in a streaming-first engine.

The Sorting Hat of Sadness

  • Spark batch handles historical truth.
  • Spark micro-batch handles reliable lakehouse settlement.
  • Spark RTM handles compatible operational hot paths.
  • Flink handles streaming-native systems.

Give every workload a contract: latency, correctness, state, sink, recovery, cost, and action. Then choose the least dramatic engine that keeps the promise.

Never let marketing choose your trigger.

🌅 The Nothing Was Not the Enemy

The real danger was never that vendors market low latency. Of course they do. Every kingdom sells dragons. The danger is believing the poster instead of reading the contract.

“Real time” is not a personality trait. It’s not a sticker on a runtime, a keynote phrase, or a trigger with heroic lighting. It’s a promise with consequences: support boundaries, recovery behavior, sink semantics, state growth, cost, and the tiny humiliating truth of p99.

That’s why the ending is not Spark wins, Flink wins, or micro-batch was fake all along. The ending is quieter.

Choose the tool whose failure mode you understand. Choose the engine whose contract matches the decision. And never let marketing name your latency budget.

Because in streaming, as in Fantastica, every wish has a price.

And one question remains at the edge of the kingdom:

🤔 Dynamic allocation in streaming sounds obvious — until the stream must stay awake. How do you scale down without scaling away readiness?

But that is another story. And shall be told another time.

“Reach for Spark. Sounds like fantasy Stream real-time. And what you see will be” (adapted from **“The neverending story” by Limahl**)

Spark RTM grants lower latency wishes… then quietly hands you the invoice: p99, task slots, idempotent sinks, and one very judgmental support matrix.

Spark RTM grants lower latency wishes… then quietly hands you the invoice: p99, task slots, idempotent sinks, and one very judgmental support matrix.


메타데이터
post_id
bfc0d5a2eed4
slug
apache-spark-wtf-the-neverending-real-time-story-bfc0d5a2eed4
url
https://medium.com/towards-data-engineering/apache-spark-wtf-the-neverending-real-time-story-bfc0d5a2eed4
canonical_url
https://medium.com/towards-data-engineering/apache-spark-wtf-the-neverending-real-time-story-bfc0d5a2eed4
author_url
https://medium.com/@angel.alvarez.pascua
status
ok
fetched_at
2026-06-16 19:09:56