New Case Study: How NVIDIA Parabricks Accelerates Alzheimer's Research with STAR RNA-seq Analysis
More

Diagnose Spark Performance Issues Using Spark UI and Query Plans

Spark jobs running slow but you don't know why? This guide teaches you how to map Spark UI metrics to physical query plans, so you can explain exactly what's causing bottlenecks and validate that your fixes actually work.

const metadata = ; Getting Oriented: What “Spark Performance” Actually Means Spark performance is often misunderstood as a question of executor sizing or CPU usage. In practice, performance means how quickly Spark can move data through stages and complete useful work with predictable cost. A job that finishes fast once and slow the next time is not performing well, even if the average runtime looks acceptable. The most important shift for readers, especially those newer to Spark, is understanding where performance evidence comes from. The Spark UI shows timing, stage boundaries, and resource usage summaries. It answers the question “where does the job appear to be slow.” It does not explain why that slowness exists. The physical query plan answers a different question: how Spark decided to execute the work, where data is reshuffled, and which operators dominate cost. This article is deliberately scoped around that distinction. The Spark UI is treated as a symptom detector. Query plans are treated as the explanation layer. Tuning decisions are made only after both are read together. Logs, intuition, and historical fixes are intentionally excluded from the early stages because they tend to bias decisions before the problem is understood. It is also important to be clear about what this article will and will not do. This is not a reference of every Spark configuration flag, and it does not attempt to cover every workload type. The goal is to give readers a repeatable way to reason about performance, explain their decisions to others, and validate that changes actually worked. If you finish a job faster but cannot explain why, you have not solved the problem yet. Scope and Assumptions Before looking at Spark UI screenshots or query plans, it is necessary to lock scope. Spark performance advice fails most often when guidance meant for one class of workload is applied blindly to another. This section exists to prevent that failure. What This Article Is About This guide focuses on Spark workloads where performance is dominated by data movement and execution planning, not by custom JVM logic. The workflow and examples in this article apply to: - Spark SQL and DataFrame workloads - Batch and scheduled jobs, including daily and hourly pipelines - Columnar storage formats such as Parquet and ORC - Performance evidence derived from the Spark UI and physical query plans These are the environments where Spark’s execution engine, not user code, is the primary determinant of performance. What This Article Intentionally Does Not Cover Some Spark performance problems require different tools and different mental models. Mixing them into one guide would reduce clarity. Out of Scope This article does not attempt to cover: - RDD-only pipelines with heavy custom transformations - Streaming workloads with micro-batch or continuous processing semantics - Source-level Spark internals or JVM tuning - Vendor-specific configuration guides or platform marketing advice If a performance issue cannot be explained using stages, shuffles, and physical plans, it likely belongs outside the scope of this workflow. Readers are expected to: - Run Spark jobs in an environment they control - Access the Spark UI for completed applications - Generate query plans using EXPLAIN This article does not assume deep Spark expertise, but it does assume operational access. If those conditions are not met, diagnosis becomes guesswork and the workflow breaks down. Why Spark Performance Matters Early, Not at the End Many teams treat performance as a final polish step, something addressed after a pipeline is correct. In distributed systems, this ordering is backwards. Spark performance decisions shape cost, reliability, and scheduling behavior long before anyone labels them “performance work.” Slow Jobs Quietly Translate Into Real Spend In managed Spark environments, runtime is money. A job that runs twice as long consumes twice the core-hours. Even when clusters autoscale, the billing meter does not care why a job is slow. Longer execution directly increases infrastructure spend, often without triggering alerts. Performance issues that look tolerable in development become expensive in production because they repeat. Variance Is More Expensive Than Slowness A consistently slow job can be planned around. A job that sometimes finishes in forty minutes and sometimes in two hours cannot. Variance forces teams to size clusters for the worst case. That decision locks in higher baseline cost, even when most runs do not need it. Performance diagnosis is therefore not just about speed, but about reducing unpredictability. Cost Belongs Inside Performance Diagnosis Treating cost as a separate concern leads to shallow validation. A change that reduces wall-clock time but increases shuffle volume or total core-hours may look like a success from a developer perspective, while quietly increasing cloud spend. This article treats cost signals, especially total core-hours, as first-class validation metrics. Spark UI Shows Symptoms, Query Plans Explain Causes The Spark UI is the first place engineers look when a job is slow, and for good reason. It shows timelines, stages, and task behavior in a way that is immediately accessible. The problem is not that teams look at the Spark UI, but that they often stop there. To diagnose performance correctly, it is necessary to understand what the Spark UI can prove and what it fundamentally cannot. The Spark UI Says (Symptom) The Query Plan Reveals (Cause) "Shuffle Write: 10GB" A SortMergeJoin forced a full data exchange. "Task Duration: 40m (Max)" A skewed join key concentrated data on one partition. "Input Size: 50GB" A Scan operator read all columns instead of pruning. "Spill (Memory): 5GB" An Aggregation buffer exceeded executor memory limits. Why Most Spark Performance Tuning Fails in Practice Most Spark performance problems do not persist because teams lack knowledge of configuration flags. They persist because teams intervene too early, before they can explain what is slow and why. Once a change is made without a diagnosis, every result that follows becomes ambiguous. If performance improves, no one knows which change mattered. If it gets worse, rollback becomes guesswork. Over time, this erodes confidence in the platform rather than improving it. Acting Before Understanding the Symptom A common pattern plays out in production environments. A job runs longer than expected. Someone opens the Spark UI, sees a long-running stage, and concludes that Spark needs more resources. Executor memory is increased. More cores are added. Sometimes the job finishes faster, sometimes it does not. What is missing in this loop is explanation. The team cannot say whether the original slowdown was caused by data movement, skew, I/O pressure, or a planning decision. Without that explanation, the tuning action is detached from the problem it claims to solve. The Gap Between Reported Time and Real Progress Another reason tuning fails is that Spark reports time generously. A task can be marked as running while doing very little useful work. It may be waiting on input, blocked on a shuffle fetch, or stalled due to memory pressure elsewhere in the system. From the Spark UI alone, these situations often look identical. Long task duration is interpreted as heavy computation, even when the executor is mostly waiting. Decisions made on that assumption usually target the wrong bottleneck. If you don't connect the UI signal to the execution plan, you confuse elapsed time with productive work. Why Guess-Based Tuning Increases Cost and Variance When tuning is driven by guesses, the safest response seems to be overprovisioning. More memory, more cores, and larger clusters reduce the chance of failure, but they also lock in higher cost. Worse, guess-based tuning increases variance. Each new configuration interacts with data size, input distribution, and cluster load in subtle ways. Jobs become unpredictable. Pipelines that were stable for weeks suddenly spike in runtime, forcing emergency interventions. At that point, performance work stops being engineering and starts being firefighting. The Only Workflow That Scales: Observe → Diagnose → Fix → Validate To escape the "guess-and-check" cycle, Spark performance work needs a fixed order of operations. This is not a set of tips; it is a constraint. You must not move to the next step until the current one is complete. 1. Observe: Start With Evidence - Input: Spark UI (Stages Tab), Event Logs. - Action: Identify the stage that consumes the most time or resources. - Exit Criteria: You have a specific Stage ID (e.g., "Stage 4"). - Note: Ignore logs and intuition here. They bias you toward solutions. 2. Diagnose: Explain With the Plan - Input: Physical Query Plan (df.explain()). - Action: Map the Stage ID from Step 1 to a specific Operator. - Exit Criteria: You can complete this sentence: "The job is slow..." 3. Fix: Apply One Lever - Input: Application Code or Spark Configuration. - Action: Apply a single change targeting the diagnosed operator. - Exit Criteria: A new deployment artifact (JAR/Python file). - Note: Never apply two fixes at once. 4. Validate: Prove the Metric Changed - Input: New Run Metrics vs. Baseline Metrics. - Action: Compare the specific metric identified in Step 1. If your job runs 10% faster but the Shuffle Bytes (or your diagnosed metric) is exactly the same, you did not fix the problem. You likely benefited from a noisy cluster or transient network speed. A fix is only real if the underlying metric moves. Exact Spark UI Navigation for Observation Spark UI → Applications → [Application ID] → Stages tab → Sort by Duration (descending) → Identify top blocking stage → Click Stage ID (e.g., Stage 4) Metrics to Capture From the Stage Details page record: - Stage ID - Duration (min / max) - Shuffle Read (total) - Shuffle Write (total) - Input Size - Spill (Memory) - Spill (Disk) - Number of Tasks Do not proceed until you can write down a single Stage ID and a single dominant symptom. How to Read the Spark UI Without Being Misled The Spark UI is often treated as a scoreboard. Stages are sorted by duration, the longest one is blamed, and tuning begins. This approach feels logical, but it is misleading more often than it is helpful. Used correctly, the Spark UI is not a performance oracle. It is a diagnostic instrument that needs interpretation. Jobs, Stages, and Tasks Are Answering Different Questions Confusion often arises because engineers expect one layer of the UI to explain everything. Jobs represent intent. They correspond to actions triggered by user code and tell you when Spark decided to execute something. They are rarely useful for performance diagnosis beyond establishing boundaries. Stages represent data movement. A new stage usually means a shuffle or an exchange. From a performance perspective, stages are the most important layer because they mark where Spark had to repartition or reorganize data. Tasks represent parallel execution. They show how evenly work was distributed and whether some partitions lagged behind. Tasks explain variance and stragglers, but not structure. Performance diagnosis fails when conclusions from one layer are applied to another. When looking at the Stages tab: - Is shuffle volume high relative to input size? - Are task durations uneven (tail latency)? - Is the stage blocking downstream stages? - Are tasks running long without a high CPU? Finding this stage requires looking at duration, parallelism, and dependency structure together. Once identified, the Spark UI has done its job. Explaining why that stage behaves the way it does requires moving to the query plan. Use Query Plans to Map Data Movement If the Spark UI answers where time is spent, the physical query plan answers how Spark decided to spend it. Without this second view, performance work remains reactive. Query plans describe execution structure. They show where Spark scans data, where it reshuffles it, and where it aggregates or joins intermediate results. These decisions dominate performance far more than most configuration settings. Logical Plans Versus Physical Plans The logical plan represents intent. It reflects the operations expressed in code, such as filters, projections, and joins, without committing to an execution strategy. The physical plan represents reality. It shows the concrete operators Spark chose, the order in which they execute, and where data must move across the cluster. Performance diagnosis belongs here. For tuning purposes, logical plans explain correctness. Physical plans explain cost. Reading EXPLAIN Output With Purpose EXPLAIN output can be overwhelming if read line by line. The goal is not to understand every node, but to identify operators that force data movement or coordination. Operators such as scans, exchanges, and joins deserve immediate attention. Exchanges are especially important because they introduce shuffles and create new stages. Once an exchange appears in the plan, network and disk behavior become part of the performance story. Formatted or extended explain modes are usually worth the extra verbosity because they make stage boundaries and operator hierarchy explicit. `python // Spark SQL / DataFrame df.explain("formatted") ` - Look for Exchange boundaries - Identify parent operators: SortMergeJoin, HashAggregate, Scan - Ignore optimizerDD nodes and formatting noise Tracing Stage Boundaries Back to the Plan Most stage boundaries in the Spark UI correspond to a point in the physical plan where Spark had to materialize or redistribute data. The most common cause is an exchange operator. By tracing a slow stage back to the exchange that created it, the plan reveals why the stage exists at all. At that point, performance work shifts from guessing about resources to reasoning about structure. This connection between UI stages and plan operators is where Spark performance becomes explainable. Common Silent Failure Modes the Spark UI Does Not Explain Some of the most expensive Spark performance problems share the same surface symptoms. A long-running stage, uneven task durations, or inflated shuffle metrics often look identical in the Spark UI, even when the underlying causes are very different. This is where many teams stall. The UI shows something is wrong, but it does not explain what kind of wrong it is. Shuffle Explosion Caused by Late Filtering One of the most common silent failures is a shuffle that grows far larger than expected. In the Spark UI, this appears as high shuffle read and write volumes and long stage durations. The natural reaction is to assume that the dataset is simply large. `python val filteredCustomers = customers.filter(col("region") === "EU") val joined = orders.join(filteredCustomers, "customer_id") ` Expected Metric Change - Shuffle Read and Write decrease - Same join operator, lower data volume The physical plan often tells a different story. Filters that could have reduced data volume are applied after a join or aggregation, forcing Spark to repartition and move far more data than necessary. The cost comes not from the data itself, but from when Spark is allowed to discard it. Data Skew Hidden by Average Metrics Skew is another failure mode that the Spark UI partially exposes but rarely explains well. Average task duration may look reasonable, while a small number of partitions run far longer than the rest. `python val repartitioned = df.repartition(200, col("join_key")) ` Expected Metric Change - Narrower task duration spread - Reduced max task duration - Same average task time Repartitioning increases shuffle volume; use only when skew is confirmed. From the UI alone, this can be mistaken for transient slowness or infrastructure noise. The physical plan, combined with task-level metrics, often reveals a join key or aggregation that concentrates data unevenly. CPU Saturation That Produces Little Progress High CPU utilization is frequently interpreted as proof that a job is compute-bound. In practice, this assumption fails often. In stalled executions, CPUs may be busy managing memory, coordinating threads, or waiting on data movement. From the Spark UI, this still appears as active computation. The physical plan helps distinguish between operators that perform real work and those that exist primarily to coordinate or materialize data. I/O Wait That Masquerades as Slow Code Another common pattern is a stage that runs slowly without obvious skew or shuffle anomalies. Task durations are long, but no single metric looks extreme. `python val projected = spark.read.parquet("/data/events") .select("event_time", "user_id") ` What to Verify in Plan - Scan parquet shows column pruning - Input size reduced relative to full schema Expected Metric Change - Input Size decreases - Stage duration decreases without shuffle change In these cases, the physical plan often shows wide scans feeding exchanges or aggregations. The cost is dominated by reading data rather than processing it. Storage latency, file layout, or excessive small files become the limiting factor. Symptom → Cause → Fix as a Repeatable Pattern By this point, the shape of the diagnosis process should be familiar. What remains is to formalize it so it can be reused consistently. This pattern is not a heuristic. It is a constraint on how performance work is done. If You See Large Shuffle Read and Write Times The Spark UI shows high shuffle volumes and a stage dominates runtime. The physical plan almost always contains one or more exchange operators feeding joins or aggregations. The cause is not “Spark is slow,” but that Spark was forced to repartition large amounts of data. Fixes target the structure that introduced the exchange, such as join strategy, join order, or earlier reduction of input size. Symptom Reference - Spark UI: High Shuffle Write - Plan: SortMergeJoin preceded by Exchange `python import org.apache.spark.sql.functions.broadcast val joined = largeDf.join( broadcast(smallDf), Seq("user_id") ) ` Expected Metric Change - Shuffle Write → near zero - Stage count → reduced by one - Core-hours → reduced Only valid if the broadcast side comfortably fits in executor memory. If You See Long Task Completion Tails A small number of tasks extend stage duration far beyond the median. The UI makes this visible, but not explainable. The plan and input characteristics often reveal skewed keys or uneven partitioning. Fixes focus on redistributing work, not increasing resources. Validation means narrowing the task duration spread, not merely reducing the average. If CPU Usage Is High but Runtime Does Not Improve Executors appear busy, but performance plateaus even as more cores are added. The UI suggests saturation, but progress does not scale. The plan often shows coordination-heavy operators or wide exchanges that limit parallelism. The fix lies in changing execution shape, not provisioning. Validation must show improved scaling behavior, not just higher utilization. This pattern repeats. The details change, but the logic does not. The Spark UI surfaces the symptom. The plan explains the cause. The fix targets the cause. Validation confirms the outcome. Optimization Risk Levels: What to Touch First and What to Avoid Once a diagnosis points to a likely cause, the temptation is to reach for every available tuning lever. This is where otherwise solid performance work often unravels. Not all optimizations carry the same risk. Some are safe to test early. Others require strong evidence. A few routinely make performance worse, even when they look reasonable on paper. Defaults Worth Re-Evaluating Early Spark’s defaults are designed to work across a wide range of workloads, which means they are rarely optimal for any specific one. Re-evaluating them is reasonable once a bottleneck is understood. Partition counts, join strategies, and file layout decisions often sit directly on the critical path exposed by the query plan. Adjusting these levers changes how data is moved and grouped, which directly affects stage structure. The key constraint is intent. These changes should be made because they address a diagnosed cause, not because they are commonly recommended. Changes That Require Evidence Before Use Some tuning actions are powerful but dangerous when applied blindly. Manual executor memory adjustments, speculative execution, and aggressive caching can all reduce runtime in the right circumstances. They can also increase pressure elsewhere in the system, mask real bottlenecks, or inflate cost. These changes should never be applied without a clear hypothesis and a validation plan. If the expected metric does not move in the expected direction, the change should be reverted. Tuning That Commonly Makes Performance Worse Certain actions appear helpful but frequently backfire. Increasing parallelism without addressing skew, adding executors to hide shuffle overhead, or caching large intermediate datasets without understanding reuse patterns often increase variance and cost. These actions treat symptoms while reinforcing the underlying cause. In mature Spark environments, they account for a large share of long-term performance debt. Making Performance Improvements Explainable and Repeatable A performance fix that lives only in someone’s head is not a fix. It is an anecdote. For Spark performance work to scale across teams and time, it must be explainable to others and repeatable under similar conditions. Recording the Symptom, Evidence, and Decision Every performance change should be traceable back to a concrete observation. At a minimum, this means recording what symptom was observed in the Spark UI, what plan structure explained it, and what change was made as a result. This creates a chain of reasoning that another engineer can follow without re-diagnosing the job from scratch. Over time, this documentation becomes more valuable than any individual optimization. Designing Before-and-After Comparisons That Hold Up Validation only works if comparisons are fair. Comparing a new run against an older job with different input data, a modified cluster, or unrelated configuration changes invalidates the result. Improvements measured this way cannot be trusted and should not be propagated. Explicitly rejecting invalid comparisons is part of maintaining discipline. A slower but explainable pipeline is preferable to a faster one that no one understands. A Practical Spark Performance Diagnostic Cheat Sheet By this point, the workflow should feel natural. This section exists to make it reusable under pressure. The table below is a decision aid that maps visible symptoms to the first place you should look and the safest initial action. Spark UI or Plan Symptom Likely Root Cause First Diagnostic Check Safe First Action Stage dominates total runtime Forced data movement Physical plan for Exchange operators Reduce input earlier or change join order High shuffle read and write Join or aggregation repartitioning Join strategy in physical plan Evaluate broadcast or reduce shuffle keys Long task completion tails Data skew Task-level duration spread Repartition or address skewed keys High CPU usage, poor scaling Coordination or memory pressure Operator type and parallelism limits Change execution shape, not core count Slow stages with no clear skew I/O dominated execution Scan operators and file layout Consolidate files or adjust data layout Frequent spill to disk Memory pressure from plan shape Aggregation and sort operators Reduce intermediate data size This cheat sheet is intentionally conservative. It points you toward diagnosis, not shortcuts. If a symptom does not map cleanly to a cause, stop and inspect the plan again. Guessing is how performance debt accumulates. Conclusion Spark performance problems persist because teams accept slow jobs without being able to explain them. The Spark UI will always show you which stages ran long. That alone is not enough to act. Until you can point to a physical plan and say “this operator caused this stage,” any tuning decision is a guess, even when it works. The workflow in this article is intentionally rigid. Observe in the UI. Diagnose in the plan. Change one thing. Prove that the expected metric moved on the same data and cluster. Anything less produces results that cannot be repeated, defended, or trusted six months later. If you cannot explain why a Spark job became faster, you did not fix it. You got lucky. Production systems do not tolerate luck for long. FAQs 1. Does this approach require changes to application code? No. Most performance issues diagnosed in this workflow are exposed through execution structure, not business logic. Code changes may be the fix, but diagnosis does not require rewriting pipelines. Tracer provides the missing visibility layer when execution behavior needs to be explained beyond what Spark exposes. 2. Is this workflow applicable to Databricks and EMR? Yes. The Spark UI and physical query plans behave consistently across managed Spark environments. Platform-specific defaults may differ, but the diagnosis logic remains the same. Tracer provides the missing visibility layer across these environments. 3. How is this different from traditional metrics systems? Traditional metrics systems report resource usage. They do not explain execution behavior or why Spark made specific planning decisions. This workflow focuses on causality, not utilization. Tracer provides the missing visibility layer by connecting execution behavior to infrastructure signals. 4. Can this help justify infrastructure cost decisions? Yes. By tying performance changes to core-hours, shuffle volume, and variance, this workflow makes cost impact explicit. Improvements can be validated and explained to FinOps and platform stakeholders. Tracer provides the missing visibility layer that makes those justifications concrete.
Background

Get Started Now

Ready to See
Tracer In Action?

Start for free or
Tracer Logo

Tracer is the first pipeline monitoring system purpose-built for high-compute workloads that lives in the OS.

2025 The Forge Software Inc. | A US Delaware Corporation, registered at 99 Wall Street, Suite 168 New York, NY 10005 | Terms & Conditions | Privacy Policy | Cookies Policy