Skip to main content
Data Lake Anti-Patterns

Zombie Jobs in Your Data Lake: 4 Queries That Eat Budgets Whole

Somewhere in your data lake, a query is running right now that nobody asked for. It started as a good idea—maybe six months ago, maybe two years. The team that built it has moved on. No one remembers why it exists, but it keeps running, every hour, scanning terabytes and burning credits. These are zombie jobs. They're not evil. They're just forgotten. And they're eating your budget whole. Why Your Data Lake Is Bleeding Money Slowly The hidden cost of orphaned pipelines Somewhere in your data lake, a query is running right now. It's not serving a dashboard. It's not feeding a model. Nobody remembers why it exists, but it consumes compute every single hour. I've walked into engineering orgs where the team genuinely believed their infrastructure costs were 'just how cloud works.' Then we pulled the job history.

Somewhere in your data lake, a query is running right now that nobody asked for. It started as a good idea—maybe six months ago, maybe two years. The team that built it has moved on. No one remembers why it exists, but it keeps running, every hour, scanning terabytes and burning credits.

These are zombie jobs. They're not evil. They're just forgotten. And they're eating your budget whole.

Why Your Data Lake Is Bleeding Money Slowly

The hidden cost of orphaned pipelines

Somewhere in your data lake, a query is running right now. It's not serving a dashboard. It's not feeding a model. Nobody remembers why it exists, but it consumes compute every single hour. I've walked into engineering orgs where the team genuinely believed their infrastructure costs were 'just how cloud works.' Then we pulled the job history. The orphaned pipelines weren't a rounding error — they were the line item.

That's the ugly truth about data lakes. Storage is cheap, so cheap that the real expense hides in plain sight. Compute is where the money evaporates. A forgotten job that scans a few terabytes on a loop doesn't scream for attention. It just quietly appears on the bill, month after month, until someone finally asks why the finance dashboard looks like a horror story.

The math gets worse with scale. A single zombie query running hourly might cost you the equivalent of a junior developer's coffee budget. Run it for a year, and you've paid for a conference ticket. But multiply that by forty forgotten jobs, and suddenly you're looking at a six-figure annual burn that produces zero business value. Wrong order? No — the order is precisely this: small leaks, left alone, become the budget crisis nobody wants to own.

Sticker shock from cloud bills

Cloud providers don't help. Their billing dashboards show you numbers, not narratives. You see a spike in 'Amazon Athena — scanned bytes' or 'BigQuery — analysis' and your first instinct is to blame the data science team. That's usually wrong. The real culprit is often a pipeline that survived three reorgs and now runs on an orphaned service account with no owner.

Here's the trade-off nobody mentions at architecture review: data lakes are designed for flexibility, which means nothing ever gets deleted. Schemas evolve, teams move on, and the access patterns that made a job critical in 2022 are ancient history by 2025. The job, however, keeps running. That's the pitfall — the system rewards adding things, never removing them.

Cost transparency tools exist, sure. But they surface symptoms, not causes. A spike in compute gets flagged, the on-call engineer tags it 'investigating,' and then the ticket dies in a backlog. The zombie survives because nobody has the context to kill it safely. That hurts.

'The most expensive query you'll ever run is the one you forgot you had.'

— data engineer, post-incident review

How zombies survive team changes

Team churn is the zombie's best friend. When the person who wrote a pipeline leaves, the knowledge leaves with them. The job doesn't care. It runs on schedule, oblivious to the fact that its only advocate has gone to another company. I've seen jobs outlive three managers, two platform migrations, and one complete warehouse re-platforming.

The tricky bit is that cleaning up feels risky. Nobody wants to be the person who kills a job that actually matters. So the default move is to leave everything running — a strategy that feels safe and costs a fortune. Most teams skip this question: 'What breaks if we stop it?' They'd rather pay for ambiguity than test it.

We fixed this once by tagging every job with a 'last meaningful output' date. Anything older than six months got flagged. The list was embarrassing — thirty-one jobs, several of which were actively failing for weeks without anyone noticing. That's when you realize the zombie problem isn't technical. It's organizational. And until you treat it that way, your budget will keep bleeding in slow motion.

What a Zombie Query Actually Looks Like

Common traits of dead-but-running jobs

A zombie query looks perfectly normal in the queue. It has a name, an owner, a schedule. It runs every night at 2 AM, churns through terabytes, and produces a result that nobody opens. You can spot them by their fingerprints: predictable runtime, stable resource draw, and zero downstream consumers. The job finishes its work and dies — but the compute bill keeps coming.

These jobs don't crash. That's the problem. A failing pipeline gets fixed or killed; a zombie just hums along. It might read from a table that nobody updates anymore. It might join two datasets that were decommissioned three quarters ago. The output lands in a folder with a date stamp, and the folder fills up like a voicemail box no one checks.

I have seen a job that aggregated ad impressions from 2021. Someone ran it once for a marketing retrospective, then set it on a weekly schedule out of habit. Two years later, it was still spinning up fifty Spark executors every Monday. The original requester had left the company.

The lifecycle of a data pipeline

Every pipeline starts with intent. Someone wants an answer, so they write a query. The query gets scheduled, automated, and eventually forgotten. That's the lifecycle — birth, use, neglect. The tricky bit is that neglect doesn't look like failure. It looks like stability. The job runs, the metrics stay green, and nobody questions the monthly cost because it's baked into the baseline.

What usually breaks first is not the code but the context around it. The dashboard it feeds gets archived. The person who built it moves to another team. The business question it answered becomes irrelevant, but the query never hears the news. It keeps running on autopilot, burning GPU hours or warehouse credits like a porch light left on all summer.

Wrong order — that's how most zombie hunts go. Teams look at query runtime first, not relevance. They optimize a SQL monster that should have been deleted months ago. Faster is not the same as necessary.

Why no one pulls the plug

The human reasons are predictable. Fear of breaking something invisible. Ownership ambiguity — is it the data engineering team's job or the analytics team's? A general reluctance to touch a system that isn't causing visible pain. Pulling the plug on a zombie feels risky. What if someone still needs that output? What if the job is actually a dependency for something else, hidden behind a dozen undocumented layers?

That caution is reasonable, but it has a cost. Every week you leave a zombie running, you're paying for a question nobody asked. The fix is simple in theory — trace dependencies, confirm zero readers, kill the schedule. In practice, tracing dependencies across a messy lake is often harder than rebuilding the whole pipeline from scratch. So the zombie lives. It's cheaper in effort to let it run than to verify it's safe to stop.

Most zombie queries survive because the cost of proving they're dead exceeds the cost of feeding them.

— data platform engineer, after a three-week audit

The catch is that accumulated zombies don't just waste money. They crowd out real work. Your cluster fills with undead jobs, so legitimate queries wait in line. Latency creeps up. Users complain. And the response is usually more hardware — which lets even more zombies spawn. That's the spiral. The only way out is to make a habit of asking one question before you schedule anything: if this output vanished tomorrow, would anyone notice? If the answer is no, you've just found your first zombie.

Under the Hood: How Queries Burn Compute

Scanning vs. Partitioning: The Meter Is Always Running

Every query engine charges you in bytes, not in seconds. When a job scans a table, it reads every file under that table's directory—even the ones holding data from 2019 that nobody touches. Partitioning changes the deal: the engine skips whole folders based on your WHERE clause. The catch? Partitioning only helps if your filters actually match the partition columns. I have seen teams partition by event_date but then query by user_id alone. That's like organizing a library by cover color and then searching for a title. The engine reads everything anyway.

The cost compounds quietly. A 5 TB table with poor partitioning can make a simple daily job read 5 TB instead of 200 GB. That's a 25x multiplier on your compute bill, and it happens every single run. Most teams don't notice until the finance report arrives—the one with the line item that grew 300% month over month.

Shuffle and Join Costs: Where Queries Go to Die

Joins are the real budget-eaters. When two large tables meet, the engine must shuffle rows across nodes so matching keys land on the same machine. That shuffle writes intermediate data to disk, transfers it over the network, and then reads it back. A join on 10 billion rows can easily produce 3–4x the original data in shuffle traffic. Wrong join order? That's another 20% overhead for free.

Broadcast joins sound like a lifesaver—until one side exceeds memory and spills to disk. Then you get disk I/O thrashing, retries, and a job that runs 40 minutes instead of 6. The killer move is often a sorted merge join, but only if your data is pre-sorted on the join key. It rarely is. So the engine falls back to hash joins, which are fast but memory-hungry. That's the trade-off: memory pressure or shuffle traffic. Both cost you.

The Role of Query Engines: Not All Optimizers Are Equal

Your engine's optimizer decides the execution plan, and that plan determines your bill. Spark's Catalyst, Trino's CBO, Hive's legacy planner—they all make different choices. Some will push filters down to the storage layer; others will load everything into memory first. The pitfall is assuming the optimizer always picks the cheapest path. It doesn't. Statistics get stale, or the engine simply lacks them for a new table.

An optimizer with bad stats is like a GPS with a 2010 map—confident, wrong, and expensive to follow.

— data engineer, on why they now run ANALYZE TABLE after every major load

What usually breaks first is the cost model for skewed data. If one join key appears 10,000 times more often than others, the engine's default assumptions fall apart. That single hot key can saturate one node while the rest idle. The fix—salting the key or manual repartitioning—requires reading the plan, not just the query. That's the hard part. You don't see the shuffle bytes until you EXPLAIN the job. The EXPLAIN output is dense, ugly, and often the only thing that saves your budget.

Spotting wasted compute boils down to three questions: Are you reading less than you store? Is the shuffle smaller than the input? Does the plan match the way your data is actually distributed? If any answer is no, you've found a zombie. Next, we'll trace one through real numbers—so you can see exactly where the money leaks. Then you can start looking at your own job history with fresh eyes. That's the move: check the plan, check the bytes, kill the query.

A Walkthrough: One Zombie Job, Real Math

Anatomy of a wasteful query

Let’s build one. You have a `processed_events` table sitting in S3, partitioned by date. Every morning, a scheduled job runs this:

SELECT user_id, COUNT(*) FROM processed_events WHERE event_date BETWEEN '2025-01-01' AND '2025-12-31' GROUP BY user_id

Field note: data plans crack at handoff.

Looks innocent. But here’s the kicker—that table has 4 billion rows, and the partition filter isn’t pushed down because the column is stored as a string. The engine scans every single partition, every single day, to answer a question that only needs last week’s data.

Field note: data plans crack at handoff.

I’ve seen this exact pattern at three different companies. The job “works fine” for months, then the bill arrives.

Cost breakdown: compute and storage

Run the math on a typical setup. Say you’re using Athena or Spark on 10 nodes. Each node processes about 200 MB/s. Your full scan touches 2.1 TB of data—that’s the entire table, not the 3% you actually need.

Compute: 2.1 TB / (10 × 200 MB/s) = ~1,050 seconds per run. At $0.15 per node-hour, that’s $0.44 per run. Doesn’t sound terrible, right? Until you realize it runs hourly. That’s $10.50 a day, $315 a month, just for one query.

Storage side is sneakier. Every scan rewrites intermediate results to temp buckets, and those get billed at standard S3 rates. Add another $40 a month. The total? Roughly $355 for a query that should cost $11 if it only touched the relevant partitions.

Wrong order of magnitude. That hurts.

Simple fixes that cut waste

The fix isn’t glamorous. First, change the filter to use the partition column directly—`WHERE event_date >= current_date - 7`. That alone drops the scan to 145 GB. Compute falls to 72 seconds per run; cost drops to $0.03.

“Most zombie jobs aren’t evil. They’re just lazy—built once, never questioned, and left to graze on your budget.”

— field note from a data engineer, after killing 14 such jobs

Second, add a materialized view for the weekly rollup. Precompute the aggregation once, then query the 10-row result. That cuts the hourly job to a minute of work. Third—and most teams skip this—set a budget alarm on the query’s cost. If it exceeds $0.50 per run, page someone.

The trade-off is maintenance. Materialized views need refresh schedules, and partition pruning only works if your data lands on time. Late arrivals break the filter silently. You’ll trade compute waste for a bit of operational vigilance.

One pitfall: don’t over-index on micro-optimizations. I once saw a team spend two days rewriting a query that ran twice a day. The savings? $18 a month. Not worth it. Prioritize the hourly and daily jobs first—that’s where the zombies breed.

After the fix, watch the cost graph for a week. If it flattens, you’re done. If it spikes again, check for new jobs that copied the old pattern. Zombies have a way of coming back.

When a Query Looks Dead but Isn't

Temporary Tables and Short-Lived Jobs

The cheapest-looking query in your entire lake might be the one you should never touch. A temp table that lives for ninety seconds, gets read by one downstream step, then vanishes—on paper that's pure waste. Compute spent, storage written, nothing durable to show for it. But I have seen teams kill those jobs and break a nightly reconciliation that nobody had documented. The temp table was the only clean snapshot of a source system that changed mid-batch. Remove it, and your next run silently compares apples to oranges.

Short-lived doesn't mean worthless. The real signal is whether anything *waits* on the output. Check the dependency graph, not the runtime. A five-minute job that gates a three-hour pipeline is doing heavy lifting. A five-minute job that runs alone, writes nowhere, and feeds nothing—that's a zombie wearing a productivity costume.

Scheduled Jobs with Hidden Dependencies

Here's the pattern that fools everyone: a query that runs every hour, returns the same row count, and appears to do absolutely nothing. Kill it, and three days later a dashboard in another department starts throwing errors. The catch is that the 'useless' job refreshes a cached table that other queries read *implicitly*—they don't join to it, they just assume the data is fresh because the cache expires. Wrong order, and you lose a day debugging what looks like a schema change.

The trick is to map the *side effects*, not the query plan. Does it write to a metastore? Update a partition? Trigger a webhook? Most zombie hunts fail because people only look at what a query reads. What it writes—even to a silent log table—can be the seam that holds production together. That said, if you trace all outputs and they lead nowhere, you've got a real corpse on your hands.

Odd bit about data: the dull step fails first.

Dead queries are easy to spot. The dangerous ones just *look* dead while the whole business leans on their shadow.

— field note from a data engineer, after restoring a dropped job at 2 AM

Odd bit about data: the dull step fails first.

The Fine Line Between Redundant and Necessary

Duplicate logic in two pipelines is a classic cleanup target—until you realize one of them is the audit trail. The query that recomputes the same aggregation as its neighbor isn't wasting money; it's the only record of what the numbers *were* before the source system corrected itself. Delete the redundant copy, and your compliance team loses their historical anchor. I've watched this exact trade-off stall a cost-cutting sprint for two weeks.

How do you tell the difference without guessing? Look at who consumes the output—not the query, the *result*—and what they do when it disappears. If a consumer has a fallback, you're safe to trim. If the consumer just fails silently, you're not. The distinction between redundant and necessary is almost never in the SQL; it's in the blast radius of removal. Test that by running the kill in a staging environment and watching which alerts fire. Most teams skip this and burn a production incident instead.

That hurts more than the compute ever did. So before you pull the plug on anything that looks lazy, ask one question: what breaks if this runs forever? If the honest answer is 'nothing,' then—and only then—you've earned the right to kill it. Otherwise, leave the zombie alone. It might be the only thing holding your data's heartbeat together.

Why Cleaning Up Is Harder Than It Sounds

Organizational resistance: the zombie’s camouflage

You’d think killing a dead query is a technical problem. It’s not. The real friction shows up in the standup, when the data engineer who wrote the job six months ago says “that’s still in development” and the analyst who depends on it swears it’s critical. Nobody wants to admit they don’t know what a thing does. So the job stays alive, warm, and hungry.

I have watched teams circle a query for weeks, each person assuming someone else owns it. The catch is that ownership got murky the day the original author left for another company. Now the job runs nightly, costs $400 a pop, and everyone hopes it’s someone else’s problem. That’s not laziness—it’s the cost of unclear accountability. Cleanup demands a name on the line, and few volunteers step forward when the reward is “you deleted something that might have mattered.”

“Deleting a zombie query feels like throwing away a box of cables. Someone, somewhere, might need it. Probably not. But maybe.”

— data platform lead, post-incident retro

Fear of breaking downstream consumers

The bigger blocker is fear, and it’s justified. A query that looks dead often feeds a dashboard that nobody checks until the last week of the quarter. Then it’s suddenly the only source of truth for a revenue number. You can’t exactly ask the dashboard owner to confirm—they’re on vacation, or they’ve left the org, or they never existed. Every dependency you can’t trace becomes a reason to keep the job running.

What usually breaks first isn’t the query itself but the trust in your own tooling. If your data catalog lacks lineage, you’re flying blind. And rebuilding lineage retroactively? That’s a project, not a weekend chore. In practice, most teams default to “leave it running until something screams.” That’s safe, sure—but it’s also how you end up with 40% of compute serving nobody.

Lack of ownership and documentation

The deepest cut is documentation, or the absence of it. A zombie query isn’t born dead; it starts as a quick experiment, a scratch pad that grew legs. Nobody wrote a comment because it was temporary. Then it became permanent, and the original intent evaporated. By the time you find it, the only clue is a table name like tmp_agg_v3_final_real and a WHERE clause that references a column nobody remembers.

That’s the real hurdle: you can’t kill what you can’t explain. Cleaning up requires understanding, and understanding requires time you don’t have. The honest move is to cap the damage—set a time-to-live on new jobs, enforce tags for owner and purpose, and treat any query older than 90 days with a label as a candidate for review. It won’t fix the past, but it stops the next zombie from spawning. And if your team pushes back, remind them: the alternative is paying for a graveyard that grows every sprint.

Frequently Asked Questions About Zombie Queries

How do I find zombie jobs?

Start with your query history, not your gut. Most platforms expose a system table or log that tracks every query run, its duration, and the bytes it scanned. Sort by cost, not runtime — a 30-second query that reads 40 TB can cost more than a two-hour query that reads 200 GB. I’ve seen teams ignore this and chase the longest-running jobs instead, leaving the real budget-eaters untouched for weeks.

The pattern to hunt for is repetition. A query that runs every 15 minutes, scanning a full year of data, for a dashboard nobody opens — that’s your zombie. Look for jobs with similar text, scheduled or triggered, and check their output. If the result feeds a table that nothing downstream reads, you’ve found the corpse.

Should I kill every long-running query?

No, and that’s a trap. Long-running doesn’t mean wasteful. A monthly full-table aggregation that powers a compliance report may run for hours and cost real money, but it’s doing its job. The question isn’t “how long did it run?” — it’s “what did it produce, and did anyone use it?”

Kill the query that scans 10 TB to filter down to five rows, then re-scans the same 10 TB an hour later because the result wasn’t cached. That’s the zombie. But if you start terminating everything over a certain runtime, you’ll break legitimate batch processes and spend more time restoring them than you saved.

The catch is that “not used” is hard to define. You can check table lineage, but sometimes a query feeds a BI tool’s cache you didn’t know about. Before you kill anything, pause it for a week and watch for complaints. Silent? Then bury it.

What tools help monitor query costs?

Your cloud provider’s native console is the baseline — it shows per-query cost and scan size, but the UI gets clumsy beyond a few hundred jobs. Open-source options like Apache Airflow can log durations and SLAs, but they don’t see the underlying compute spend. The practical move is to build a simple daily report that joins query logs with storage metadata, then flag anomalies: scan size up 3x, runtime doubling, same query run twice in an hour.

“Zombie queries don’t announce themselves. They just sit there, chewing through credits, until someone checks the bill.”

— data engineer, fintech startup

Worth flagging—most teams skip this because it feels like a side project. But a single afternoon of writing that join saves more than a month of manual digging. We fixed this at my last job by setting a weekly Slack alert for any query scanning over 5 TB. Within two weeks, we’d killed four recurring jobs and cut compute by 18%. That’s the whole game: visibility first, then ruthless pruning.

Share this article:

Comments (0)

No comments yet. Be the first to comment!