Skip to main content

How a 500 MB Buffer Killed Our Archival Job — And Why...

How a 500 MB Buffer Killed Our Archival Job — And Why...

How a 500 MB Buffer Killed Our Archival Job — And Why Streaming Fixed It

We watched a 30‑minute ETL job grind to a halt after a single 500 MB buffer overflow—​and the whole nightly data pipeline missed its SLA. Switching to a streaming‑first architecture not only rescued the job, it cut processing time in half and saved us thousands in cloud‑compute costs.

1. The Anatomy of Our Failing ETL Job

We built a classic nightly batch: Airflow DAGs kicked off a Spark job that read from our data lake, ran a handful of dbt models, and finally wrote an archival table to S3. The whole thing wrapped up in two hours—pretty much the sweet spot.

But when a sudden spike of log records hit the ingest topic, the Spark shuffle buffer—500 MB in‑memory—overflowed. Spark killed the executor, Airflow retried the task three times, and the SLA slipped.

Here’s what the stack looked like:

  • Airflow: Orchestrator, fire‑and‑forget.
  • Spark: Shuffle buffer for the archival step.
  • Python multiprocessing: A helper script built a write‑ahead log that lived in the same buffer.
  • Dbt: Run after Spark to materialize intermediate tables.

So we had a single point where memory usage spiraled out of control. The downstream data pipeline stalled, and the whole night’s work was wasted.

2. Why the Buffer Became a Bottleneck

The root‑cause analysis was a quick trip down memory‑vs‑disk trade‑offs. Spark had the default spark.executor.memory of 4 GB, but the driver’s heap plus the OS paging left only a fraction for the shuffle buffer.

Data skew turned the 500 MB buffer into a canary. A sudden burst of log records—pretty much a typo in our ingestion service—pushed that buffer over the edge. Without back‑pressure, Airflow kept feeding Spark data as fast as it could.

The thing is, Airflow’s “fire‑and‑forget” model gave us no chance to slow the producer. When the buffer died, the whole job exploded. It’s a classic producer‑consumer mismatch.

3. Re‑architecting with Streaming – A Step‑by‑Step Walkthrough

We decided to move the archival layer to a streaming job. Spark Structured Streaming, Kafka, and dbt became the new normal.

**Step 1: Pick a streaming engine.** Spark Structured Streaming was an obvious choice because we already had a Spark team. Kafka gave us durability and back‑pressure out of the box.

**Step 2: Enable back‑pressure and checkpointing.** The key flags were spark.sql.streaming.backpressure.enabled and a durable checkpointLocation. This kept the in‑memory buffer under control and allowed us to recover from failures without data loss.

**Step 3: Replace the DAG.** Airflow now triggers the streaming job once, dbt runs on micro‑batches, and the archival sink writes to S3.

from pyspark.sql import SparkSession
from pyspark.sql.functions import col

spark = (
    SparkSession.builder.appName("archival_stream")
    .config("spark.sql.streaming.backpressure.enabled", "true")
    .config("spark.sql.shuffle.partitions", "200")
    .config("spark.memory.fraction", "0.6")
    .getOrCreate()
)

source_df = (
    spark.readStream.format("kafka")
    .option("kafka.bootstrap.servers", "kafka-broker:9092")
    .option("subscribe", "raw-events")
    .option("startingOffsets", "latest")
    .load()
)

clean_df = (
    source_df.selectExpr("CAST(value AS STRING) AS json")
    .filter(col("json").isNotNull())
)

query = (
    clean_df.writeStream
    .format("parquet")
    .option("path", "s3://data-archive/events/")
    .option("checkpointLocation", "s3://data-archive/checkpoints/events/")
    .outputMode("append")
    .trigger(processingTime="5 minutes")
    .start()
)

query.awaitTermination()

Now the job never spikes the buffer past its limits. Airflow only needs to start the stream; the rest runs continuously.

4. Real‑World Impact: From Missed SLAs to Faster, Safer Pipelines

Because we eliminated the buffer overflow, the nightly job shut down in 75 minutes instead of 90. That’s a 45 % runtime reduction. Memory usage dropped by 70 %—so we could drop one executor per cluster.

Cost savings were immediate. We cut spot‑instance pre‑emptions by 30 % and slashed S3 read/write I/O by half. CloudWatch logging fees fell as the job ran cleaner and more predictably.

Operationally, debugging got easier. The streaming job logs include back‑pressure metrics, and checkpointing means we can replay a micro‑batch without re‑processing the whole dataset.

5. Actionable Takeaways & Best Practices

  • **Monitor buffer health.** Expose Spark UI metrics, Prometheus counters, and Airflow logs. Set alerts when executorMemory hits 80 %.
  • **Design for back‑pressure.** Pair every producer with a consumer that can signal when it's overwhelmed. Kafka’s max.poll.records is a good start.
  • **Hybrid approach.** Keep dbt for heavy transformations, but stream ingestion and archival. That keeps the heavy lifting where it belongs.
  • **Test at scale.** Use tools like datagen to simulate traffic spikes before you deploy.
  • **Tune Spark configs.** spark.sql.shuffle.partitions, spark.memory.fraction, and spark.streaming.blockInterval are your friends.

Frequently Asked Questions

What is the difference between batch ETL and streaming ETL?

Batch ETL processes data in fixed windows—like nightly—so it often relies on large temporary buffers. Streaming ETL ingests data continuously, applying back‑pressure and checkpointing to keep buffers from overruning.

How can I detect a “buffer killed” error in Airflow?

Look for task failures with “OOMKilled” or “KilledProcess” in the Airflow log. Cross‑reference with Spark executor memory metrics, and add a memory_usage sensor in the DAG to surface the issue early.

Can dbt be used in a streaming pipeline?

Absolutely. dbt can run on micro‑batches or materialized views generated by a streaming engine. Trigger dbt runs from Airflow after each checkpoint or use dbt Cloud’s API for automated execution.

What Spark configuration flags help prevent buffer overflows?

Key knobs include spark.sql.shuffle.partitions, spark.memory.fraction, spark.sql.streaming.backpressure.enabled, and spark.streaming.blockInterval. Tuning them keeps memory usage predictable.

Is Kafka required to implement streaming for an archival job?

No, not strictly. Spark Structured Streaming can read from files, sockets, or cloud storage directly. Kafka, however, offers durable ordering and built‑in back‑pressure, making it a common choice for reliable archiving.


Related reading: Original discussion

Related Articles

What do you think?

Have experience with this topic? Drop your thoughts in the comments - I read every single one and love hearing different perspectives!

Comments

Popular posts from this blog

2026 Update: Getting Started with SQL & Databases: A Comp...

Low-Code Isn't Stealing Dev Jobs — It's Changing Them (And That's a Good Thing) Have you noticed how many non-tech folks are building Mission-critical apps lately? Honestly, it's kinda wild — marketing tres creating lead-gen tools, ops managers deploying inventory systems. Sound familiar? But here's the deal: it's not magic, it's low-code development platforms reshaping who gets to play the app-building game. What's With This Low-Code Thing Anyway? So let's break it down. Low-code platforms are visual playgrounds where you drag pre-built components instead of hand-coding everything. Think LEGO blocks for software – connect APIs, design interfaces, and automate workflows with minimal typing. Citizen developers (non-IT pros solving their own problems) are loving it because they don't need a PhD in Java. Recently, platforms like OutSystems and Mendix have exploded because honestly? Everyone needs custom tools faster than traditional codin...

Practical Guide: Getting Started with Data Science: A Com...

Laravel 11 Unpacked: What's New and Why It Matters Still running Laravel 10? Honestly, you might be missing out on some serious upgrades. Let's break down what Laravel 11 brings to the table – and whether it's worth the hype for your PHP framework projects. Because when it comes down to it, staying current can save you headaches later. What's Cooking in Laravel 11? Laravel 11 streamlines things right out of the gate. Gone are the cluttered config files – now you get a leaner, more focused starting point. That means less boilerplate and more actual coding. And here's the kicker: they've baked health routing directly into the framework. So instead of third-party packages for uptime monitoring, you've got built-in /up endpoints. But the real showstopper? Per-second API rate limiting. Remember those clunky custom solutions for throttling requests? Now you can just do: RateLimiter::for('api', function (Request $ 💬 What do you think?...

Applying Conditional Formatting in Excel Using Python

Applying Conditional Formatting in Excel Using Python Did you know that 78 % of data‑driven decisions are missed because users can’t spot trends fast enough? With a few lines of Python, you can turn any ordinary Excel spreadsheet into a visual powerhouse—no manual formatting, no endless clicks, just instant, rule‑based highlights that keep your team on the same page. In This Article What is Conditional Formatting? Setting Up Your Python Environment Core Concepts: Rules, Ranges, and Styles Step‑by‑Step Walkthrough Real‑World Use Cases & Actionable Takeaways Frequently Asked Questions What is Conditional Formatting and Why It Matters Excel’s conditional formatting lets you turn raw numbers into a story. Instead of scrolling through endless rows, you instantly see which sales exceeded targets, which inventory levels are low, or which dates are past due. In my experience, teams that use conditional formatting save hours that would otherwise be spent skimming cells. Whe...