SaveMyCert
Log in
5 of 5 free questions left today·for 30 a day
Troubleshooting, Monitoring, and Optimization

Monitoring, Troubleshooting and Optimizing Pipelines: Lakeflow Jobs Run History, Spark UI, Liquid Clustering and Predictive Optimization (Databricks Data Engineer Associate)

20 min readDatabricks DEA · Troubleshooting, Monitoring, and OptimizationUpdated

Monitoring, troubleshooting and optimizing Databricks pipelines means reading job health and run-time trends in the Lakeflow Jobs UI, finding data skew, shuffle and disk spill in the Spark UI's stage metrics, keeping Delta tables fast with Liquid Clustering and predictive optimization, and diagnosing compute that will not start, conflicting libraries and out-of-memory errors. This lesson walks through each tool in the order you would use it on a bad night: which run or task is slow or failing, what the Spark stage metrics say about why, which layout and maintenance features prevent the problem, and how to tell one failure type from its look-alikes so the fix matches the cause.

What you’ll learn
  • Interpret job and task statuses and use the workspace Runs tab filters and Top 5 error types to track failures across jobs
  • Compare current run times with historical baselines using the matrix view, run duration thresholds and the system.lakeflow tables
  • Use the Graph, Timeline and List views of a run to find upstream blockers and long tasks
  • Identify shuffle, data skew and disk spill from stage-level metrics in the Spark UI and choose a fix that matches the cause
  • Explain Liquid Clustering, automatic liquid clustering and predictive optimization, including their requirements and limits
  • Diagnose cluster startup failures, library conflicts and driver versus executor out-of-memory errors

Job health in the Lakeflow Jobs UI: statuses and the workspace Runs tab

Pipeline health in Lakeflow Jobs is read from run and task statuses, and the fastest workspace-wide view of them is the Runs tab under Jobs & Pipelines. It lists the job and pipeline runs of every job in the workspace for the last 60 days, so an on-call engineer can triage many failures without opening jobs one by one or writing SQL.

Statuses you need to read correctly

StatusWhat it meansWhat to do with it
SucceededThe task or run finished without errorNot the cause of a failure
FailedThe task ran and raised an errorThis is where the root cause lives: open its output
Timed outThe task or run hit its configured Timeout and was stoppedWork was cut off, not broken: check the duration trend
CanceledA user, API call or the system canceled the runNot a code error
Upstream failed / Upstream canceledThe task never ran because a dependency failed or was canceledFollow the DAG back to the task marked Failed

The whole run's status follows its leaf tasks (Run if rules and repair are covered in the Lakeflow Jobs control-flow lesson). For troubleshooting, the key habit is that Upstream failed tasks hold no error of their own; their output will not explain anything.

What the Runs tab gives you

  • A finished-runs graph of successes and failures, covering the last 48 hours by default; the time filter matches completed runs on their end time. Non-admins choose Run as: me to see it.
  • A Top 5 error types table for the range, which tells you whether a wave of failures shares one cause.
  • A runs list with filters: status (for example Failed), time range, error code, and the run-as identity (a user or a service principal).

Pick the filter that matches the question. To isolate failures caused by one identity losing access, filter by run-as: the job owner is a different property, and an error-code filter both pulls in unrelated jobs and misses that identity's runs that failed with a different code. To review every failure of the week, filter by Failed status and widen the time range to the week.

Finding upstream blockers inside one run: Graph, Timeline and List views

Inside a single job run, the Graph view is the tool for spotting upstream blockers: it draws the run's task DAG and colours each task by status, so you follow the edges back from a waiting or Upstream failed task to the task that is still running or marked Failed.

  • Graph view: dependencies and per-task status. Use it to answer "what is this task waiting on?" and "where did the failure start?"
  • Timeline view: every task on a time axis. Use it to see which task stretched the end-to-end time and whether tasks meant to run in parallel actually overlapped.
  • List view: tasks with status and duration in a table. Sorting by duration finds the slowest task, but it cannot show when tasks ran relative to each other.

Keep the levels apart. Lakeflow job tasks are not Spark tasks: the Spark UI's Jobs and Stages tabs describe the Spark work of one execution and say nothing about job task dependencies. The job's Tasks tab edits the definition, and the schedule and trigger settings only decide when runs start.

Reading stage metrics in the Spark UI: shuffle and data skew

The Spark UI exposes the three classic bottlenecks named in the exam guide through stage-level metrics: shuffle appears as Shuffle Write and Shuffle Read between stages, skew as one task far slower and larger than its peers in the stage's Summary Metrics, and spill as spill statistics on the stage page.

Shuffle: where stages begin and end

Wide transformations (joins, groupBy, distinct, window partitions, repartition) need all rows with the same key together, so Spark writes Shuffle Write data at the end of one stage and reads it as Shuffle Read at the start of the next. Narrow transformations (filter, select, withColumn) work inside each partition and stay in one stage. Reading source files shows as Input; writing a table shows as Output; neither is a shuffle.

In the SQL / DataFrame tab's query plan, the shuffle is an Exchange node, for example Exchange hashpartitioning(order_id#7, 200): rows are repartitioned by key across executors and a new stage begins. FileScan, Filter and Project nodes move no data between executors. (Photon plans use Photon-named operators, but the idea is the same.)

Skew: compare Max with the 75th percentile

Each stage page has a Summary Metrics table with Min, 25th percentile, Median, 75th percentile and Max for task duration, shuffle read size/records, spill and more. Its Peak Execution Memory row shows the most execution memory each task used; read it next to the spill rows, since tasks that spill are the ones whose peak hit the memory available to them. The quick skew test from Databricks' Spark UI guide: if the Max task duration is far above the 75th percentile (around 50% higher or more), suspect skew; in a healthy stage the two are close. A few unusually fast tasks (a small Min) do not matter, because the slowest task decides when the stage ends.

Confirm the cause with the data columns. True key skew means the slow task also read far more data: its Shuffle Read Size / Records at Max is far above the 75th percentile. If the slowest task read a normal amount of data, it is not key skew, and salting will not help; look at that task's executor instead (garbage collection, a slow or failing node).

Patterns that look similar but are not skew: too many shuffle partitions produces lots of tiny tasks with scheduling overhead; a retried task after a lost executor reprocesses a normal-sized partition; a large scheduler delay points at waiting for resources.

Disk spill: telling general memory pressure from skew-driven spill

Spill means a task's data did not fit in execution memory and was written to disk, which slows the stage. Spill statistics appear in the stage details at the top of the stage page only when the stage spilled; if they are absent, the stage did not spill. Other tabs are not spill views: the Storage tab shows cached data, the Executors tab's storage memory is for cached blocks, and the Environment tab lists configuration.

Spill (Memory) versus Spill (Disk)

The two figures describe the same spilled data measured twice. Spill (Memory) is its deserialized size while it was in memory; Spill (Disk) is its serialized, usually compressed, size once written to disk. That is why the memory figure is normally the larger one. Neither figure is data "kept safely in memory", and they are not a split between shuffle reads and writes.

Where the spill sits decides the fix

Summary Metrics patternDiagnosisFix that matches
Spill and duration similar at every percentile up to MaxGeneral memory pressure: most partitions are too large for execution memoryMore memory per core (a memory-optimized worker type), or smaller partitions
Zero spill up to the 75th percentile, huge spill at Max, Max task many times longerSkew-driven spill: one oversized partition, usually one hot keySplit the hot key (salting, AQE skew join for joins); bigger workers will not fix it

For even spill, adding more workers of the same type does not help: each task still gets the same execution memory for a partition of the same size. A worker type with more memory per core does, and needs no code or Spark configuration change. For skew-driven spill, more memory leaves the one huge task dominating the stage, and fewer shuffle partitions makes every partition bigger without separating the hot key.

Fixing skew and cutting shuffle volume

Skew is fixed by spreading the oversized partition's rows over more tasks; which tool fits depends on the operation and on what you are allowed to change.

Adaptive query execution (AQE) skew join

With AQE on, Spark splits skewed partitions in sort-merge and shuffle-hash joins at runtime. A partition counts as skewed only when it exceeds both spark.sql.adaptive.skewJoin.skewedPartitionFactor times the median partition size (default 5) and spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes (default 256 MB). A 200 MB partition that is 30 times the median therefore is not split until the byte threshold is lowered below its size; raising the factor makes detection stricter, not looser. AQE skew handling does not apply to aggregations (groupBy) or broadcast joins.

Salting and two-stage aggregation

When AQE cannot help, for example a groupBy on a hot key, add a random salt to the key, aggregate by key plus salt, then aggregate again by the key alone. The hot key's rows are spread over many tasks in the first pass.

NULL join keys

All NULL keys hash to the same partition, so a column full of NULLs creates a hot partition that more shuffle partitions cannot break up. NULLs never match in an equi-join, so in a left join you can join only the rows with a key and union the NULL-key rows back with null columns. This keeps every row and removes the hot partition; simply filtering NULLs out would drop rows the output needs.

The REBALANCE hint for skewed writes

When the stage that writes a table has a few huge tasks and many tiny output files, add a REBALANCE hint to the query that produces the rows. With AQE on, it evens out output partition sizes, splitting large ones and combining small ones. COALESCE only merges partitions, BROADCAST changes a join, and turning AQE off removes the mechanism.

Shuffling less data

The biggest shuffle win is usually in the code: aggregate or filter large inputs before the join, so far fewer rows cross the Exchange. Repartitioning by the join key is itself a full shuffle, and caching avoids rereading files but still shuffles every row. Shuffle partition counts and broadcast thresholds are covered in the Spark tuning lesson of the transformation domain.

Liquid Clustering: flexible data layout that replaces partitioning and Z-ordering

Liquid Clustering is the Delta Lake data layout that co-locates rows by up to four clustering keys for data skipping, and lets you change those keys later without rewriting existing data. Databricks recommends it for new tables instead of partitioning and ZORDER.

Turning it on and changing keys

  • CREATE TABLE ... CLUSTER BY (col1, col2) sets keys at creation (also available in CTAS and the DataFrame writer's clusterBy).
  • ALTER TABLE t CLUSTER BY (new_col) enables clustering on an existing unpartitioned Delta table or changes its keys. CLUSTER BY NONE turns clustering off.
  • Key order does not matter: clustering works on the set of keys.

OPTIMIZE versus OPTIMIZE FULL

Clustering is applied by OPTIMIZE, which is incremental: it clusters data that has not yet been clustered. Changing keys affects new clustering work only. To recluster all existing records by the new keys in one operation, run OPTIMIZE t FULL. ANALYZE TABLE only collects statistics, and REORG TABLE ... APPLY (PURGE) rewrites files to purge soft-deleted data.

What it does not combine with

A table cannot be both partitioned and liquid clustered, and Z-ordering cannot be used on a clustered table. Plain ALTER TABLE ... CLUSTER BY is for unpartitioned tables and fails on a partitioned one. A partitioned table is converted either in place with ALTER TABLE t REPLACE PARTITIONED BY WITH CLUSTER BY (cols) (or AUTO), available on Databricks Runtime 18.1 and above for managed and external tables, or by rebuilding it without partitions (for example with CTAS). Moving off partitions is the right call when a partition column is high-cardinality or uneven, for example a handful of very large regions next to hundreds of near-empty ones, which creates both oversized and small-file partitions that compaction or Z-ordering inside partitions cannot fix.

LayoutChange keys laterHigh-cardinality columnsTypical problem
PARTITIONED BYRewrite the tableMany tiny partitionsSmall files, uneven partitions
Partitions + ZORDER BYRewrite for partitions; ZORDER rerun each timeZ-order within partitions onlyPartition problems remain
CLUSTER BY (keys)ALTER TABLE, no rewrite of existing dataHandledKeys chosen by you
CLUSTER BY AUTODatabricks evolves the keysHandledNeeds UC managed table + predictive optimization

Automatic liquid clustering

CLUSTER BY AUTO (at create time or through ALTER TABLE) lets Databricks choose the clustering keys from the table's query history and change them when the expected data-skipping gains outweigh the cost of reclustering. It requires Unity Catalog managed tables with predictive optimization enabled. An external table can still be liquid clustered, but only with keys you set yourself. Predictive optimization never picks keys for a table without CLUSTER BY AUTO.

Predictive optimization: automatic OPTIMIZE, VACUUM and ANALYZE for managed tables

Predictive optimization is a Databricks feature that decides which Unity Catalog managed tables benefit from maintenance and runs OPTIMIZE, VACUUM and ANALYZE on them automatically, on serverless compute, so you no longer schedule a maintenance job that misses new tables and wastes work on tables that do not need it.

Scope and inheritance

  • It runs only on Unity Catalog managed tables. External tables are never maintained by it, whatever the setting, so they still need a scheduled job that runs OPTIMIZE and VACUUM. Shortening a retention property does not run VACUUM for you.
  • It can be enabled for an account, a catalog, a schema or a single table. An account admin sets the account-level default; catalogs, schemas and tables then inherit from their parent unless overridden: ALTER CATALOG c ENABLE | DISABLE | INHERIT PREDICTIVE OPTIMIZATION and ALTER SCHEMA c.s ENABLE | DISABLE | INHERIT PREDICTIVE OPTIMIZATION.
  • Set it at the lowest level you mean. Enabling one schema overrides the inherited setting for that schema's managed tables only; enabling the catalog would switch on every schema that inherits from it; INHERIT just takes the parent's value.

How it relates to nearby features

FeatureWhat it doesWhat it does not do
Predictive optimizationRuns OPTIMIZE, VACUUM and ANALYZE when useful on managed tablesMaintain external tables
Auto compaction / optimized writesReduce small files during writes; set as table properties or session configurations, and enabled by default for some operationsRun VACUUM or collect statistics
Liquid clusteringDefines the data layoutRun maintenance by itself (OPTIMIZE still has to run)
CLUSTER BY AUTOLets Databricks choose clustering keysWork without predictive optimization

Checking what it did

The system table system.storage.predictive_optimization_operations_history records every operation it ran: table, operation type (compaction, vacuum, clustering, statistics), time and metrics, so one query covers a whole catalog. DESCRIBE HISTORY works one table at a time, and system.billing.usage shows what the feature consumed, not what it did to each table. Predictive optimization does not run as Lakeflow jobs, so it never appears in job run tables.

Diagnosing cluster startup failures and library conflicts

When classic compute fails to start, the first place to look is the compute event log, which records the failed start and its reason; the Spark UI cannot help because Spark never started, and job run history only tells you that the run failed.

Common startup causes

EvidenceLikely causeFix
INIT_SCRIPTS_STARTED without INIT_SCRIPTS_FINISHED, then terminationA cluster-scoped init script exited non-zero, which fails the launchRead the script's stdout/stderr in the delivered init script logs and fix the command
Cloud provider cannot supply the instance type in the zoneCloud capacityChoose another available instance type or allow another zone
Every classic cluster fails after a network change; serverless keeps workingOutbound rules block classic nodes from reaching the Databricks control planeRestore the required egress

The event log records which init scripts started and finished, not their output. To see why a script failed, configure cluster log delivery: init script output is then written under <log-path>/<cluster-id>/init_scripts and survives termination. Removing the script and starting the cluster produces no log of the failing command. A runtime version change, a longer job timeout or an init script that "retries" cannot create cloud capacity that does not exist.

Library conflicts

Python libraries come from three layers, and the higher one wins for a notebook: notebook-scoped (%pip install) over compute-scoped (installed on the cluster's Libraries tab) over the libraries bundled in the Databricks Runtime.

  • Need a different version in one notebook on a shared cluster? %pip install pkg==x.y.z in that notebook, then dbutils.library.restartPython(), because %pip does not restart the Python process and an already-imported version stays loaded. Other notebooks keep the compute-scoped version.
  • Import fails after adding a compute-scoped library? Check the compute's Libraries tab, which shows each library's version, source and install status, including failed installs.
  • Swapping a compute-scoped version? An uninstalled library shows Uninstall pending restart and stays loaded until the cluster restarts; detaching and reattaching notebooks is not enough.
  • Job broke with no code change? A PyPI library listed by name only installs the latest release on every new job cluster. Pin versions for repeatable jobs.
  • Databricks does not recommend installing libraries through init scripts, and a second compute-scoped version of the same package conflicts for every notebook on the cluster.

Diagnosing out-of-memory errors: driver versus executor

Out-of-memory failures come from either the driver, which coordinates the job and receives any data you bring back to it, or the executors, which process partitions; the symptoms differ and so do the fixes.

Driver OOMExecutor OOM
Typical message"The spark driver has stopped unexpectedly and is restarting", "Driver is temporarily unavailable"ExecutorLostFailure (executor exited caused by one of the running tasks), tasks retried
Spark UINo failed tasks in any stage; workers may show low memory useFailed and retried tasks in a stage; executors removed
Common causescollect() or toPandas() on large data, very large results returned to the notebookOversized partitions, a broadcast table too big for each executor, heavy skew
FixesKeep work distributed (DataFrame functions, df.write), or a larger driverFix the cause (remove an outdated broadcast hint, split skew), or more memory per executor

Driver OOM: stop pulling data to one machine

collect() and toPandas() gather every row into the driver. Repartitioning first changes nothing, because all partitions still end up in one place, and raising spark.driver.maxResultSize only lifts a cap; the driver still has to hold everything. If a bigger driver is not an option, rewrite the work with DataFrame functions and write results with df.write, so executors do the work and the driver never holds the data.

Executor OOM: rule out lost nodes first

An executor can also disappear because autoscaling removed it or a spot instance was reclaimed. Check the compute event log for resize and instance-loss events during the stage before concluding that memory ran out.

A classic real memory cause is a stale /*+ BROADCAST(t) */ hint: a broadcast copies the whole table to every executor, so when the table grows to gigabytes the join stage loses executors. An explicit hint is honoured regardless of spark.sql.autoBroadcastJoinThreshold, more workers only add more copies, and more shuffle partitions do not split a broadcast table. Remove the hint so the planner can choose a shuffle join.

Tip. Expect scenario items in which a data engineer sees a concrete symptom, such as a job slowing over weeks, a wave of weekend failures, a task stuck waiting, Spark UI percentile figures, a failed cluster launch, an import returning the wrong version or a driver restart message, and must choose the view, setting or change that fits. Near-twin options are the norm: matrix view versus Timeline versus runs list, Warning versus Timeout, run-as versus owner filters, skew versus general spill, AQE skew join versus salting, OPTIMIZE versus OPTIMIZE FULL, catalog versus schema enablement, managed versus external tables, and driver versus executor memory. Harder items add a constraint (no SQL, code cannot change, budget rules out bigger compute, rows must be kept, history older than 60 days) that rules out a fix which would otherwise work, so learn exactly what each tool shows and what each fix changes.

Key takeaways
  • Upstream failed tasks never ran: follow the Graph view back to the task marked Failed for the root cause.
  • The workspace Runs tab shows 60 days of runs across jobs, with Top 5 error types and filters for status, error code and run-as identity.
  • Matrix view compares tasks across runs; Timeline view shows long and overlapping tasks inside one run; system.lakeflow.job_run_timeline and job_task_run_timeline hold up to a year of run history.
  • A Run duration Warning only notifies; a Timeout stops the run and marks it Timed out.
  • Skew shows as a Max task far above the 75th percentile that also read far more data; spill everywhere means memory pressure, spill only at Max means skew.
  • AQE splits a skewed join partition only above both the factor times the median and the byte threshold, and does not handle aggregations or broadcast joins.
  • Liquid Clustering keys change without rewriting data, OPTIMIZE FULL reclusters existing records, and it cannot be combined with partitioning or ZORDER.
  • Predictive optimization and CLUSTER BY AUTO work only on Unity Catalog managed tables; external tables need scheduled OPTIMIZE and VACUUM.
  • Startup failures: event log first, delivered init script logs for the reason. Libraries: notebook-scoped beats compute-scoped beats runtime; pin versions.
  • Driver OOM comes from collect() or toPandas() on large data; lost executors need autoscaling and spot loss ruled out before blaming memory.

Frequently asked questions

How do I see whether a Databricks job is getting slower over time?

Open the job's Runs tab in Lakeflow Jobs and use the matrix view. It shows recent runs side by side with a Run total duration row and one row per task, so the task whose duration is growing stands out. For history older than the 60 days the Jobs UI keeps, query system.lakeflow.job_task_run_timeline, which keeps up to 365 days of task runs.

What is the difference between the Warning and Timeout values of a job's run duration threshold?

The Warning value is an expected completion time: when a run passes it, Lakeflow Jobs raises a duration-warning event that can send a notification, and the run keeps going. The Timeout value stops the run when reached and marks it Timed out. Use a Warning when a slow run must be allowed to finish.

How do you spot data skew in the Spark UI?

Open the stage page and compare the Max with the 75th percentile in its Summary Metrics. A Max task duration far above the 75th percentile, together with a Max shuffle read size far above the 75th percentile, means one partition holds far more data than the others, which is skew. If the slowest task read a normal amount of data, look at its executor instead.

What do Spill (Memory) and Spill (Disk) mean on a Spark stage page?

Both measure the same spilled data. Spill (Memory) is its deserialized size while it was in memory, and Spill (Disk) is its serialized, usually compressed, size after being written to disk. Spill appears only when a stage's data did not fit in execution memory.

Can Liquid Clustering be used on a partitioned Delta table?

Not while it stays partitioned: a Delta table cannot be both partitioned and liquid clustered, and Z-ordering cannot be used on a clustered table. Plain ALTER TABLE ... CLUSTER BY works only on unpartitioned tables. A partitioned table is converted either in place with ALTER TABLE ... REPLACE PARTITIONED BY WITH CLUSTER BY (Databricks Runtime 18.1 and above), or by recreating it without partitions, for example with CREATE TABLE AS SELECT and a CLUSTER BY clause.

Does predictive optimization work on external tables?

No. Predictive optimization runs OPTIMIZE, VACUUM and ANALYZE only on Unity Catalog managed tables, so external tables need their own scheduled maintenance job. Automatic liquid clustering (CLUSTER BY AUTO) depends on predictive optimization and is likewise limited to managed tables.

Why does %pip install not change the library version my notebook imports?

On current Databricks Runtime versions, %pip does not restart the Python process, so a version that was already imported stays loaded. Run dbutils.library.restartPython() after the %pip cell. Notebook-scoped libraries then take precedence over compute-scoped libraries for that notebook only.

How do you tell a driver out-of-memory error from an executor one in Databricks?

A driver OOM shows messages such as 'The spark driver has stopped unexpectedly and is restarting' or 'Driver is temporarily unavailable', with no failed tasks in the Spark UI, and usually follows collect() or toPandas() on large data. An executor problem shows ExecutorLostFailure and retried tasks; check the compute event log for autoscaling or spot instance loss before concluding that executor memory ran out.

Source

This lesson covers the "Troubleshooting, Monitoring, and Optimization" domain of the official Databricks DEA exam guide. Vendors revise their guides — check the source for the current version.

Test yourself on this topic
Databricks DEA practice questions, with full explanations.
Practice now

Sign up free to mark lessons complete, bookmark topics and track your exam readiness.

Spotted a mistake in this lesson?