Datanest Digital — https://datanest.dev
Streaming pipelines fail. The difference between a minor incident and a major outage is how quickly and correctly you recover. This guide provides step-by-step recovery procedures for the most common failure scenarios in Databricks Structured Streaming and Delta Live Tables.
Each section follows the same structure: symptoms, root cause, impact assessment, recovery steps, and prevention measures.
StreamingQueryExceptionOffsetOutOfRange, FileNotFoundException in the checkpoint path, or metadata file is corrupted| Checkpoint Component | Data Risk |
|---|---|
metadata corrupt | Stream cannot start; no data loss if repaired |
offsets/ missing entries | Potential data skip or replay |
commits/ missing entries | Last uncommitted batch replays (safe — idempotent) |
state/ corrupt | Stateful results incorrect; windowed aggregations lost |
Step 1: Stop the stream and back up the checkpoint.
for q in spark.streams.active:
if q.name == "your_stream_name":
q.stop()
q.awaitTermination(timeout=60)
from datetime import datetime
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
dbutils.fs.cp(checkpoint_path, f"{checkpoint_path}_backup_{ts}", recurse=True)Step 2: Diagnose the corruption.
def diagnose_checkpoint(path: str) -> dict:
result = {}
try:
result["metadata"] = dbutils.fs.head(f"{path}/metadata", 1024)
except:
result["metadata"] = "MISSING_OR_CORRUPT"
for subdir in ["offsets", "commits"]:
try:
files = dbutils.fs.ls(f"{path}/{subdir}")
ids = sorted([int(f.name) for f in files])
result[subdir] = {"count": len(ids), "range": f"{min(ids)}-{max(ids)}" if ids else "empty"}
except:
result[subdir] = "MISSING"
return result
import json
print(json.dumps(diagnose_checkpoint(checkpoint_path), indent=2))Step 3: Apply the appropriate repair.
config/checkpoint_management.md for details).state/ directory; the stream will rebuild state from scratch.startingOffsets.Step 4: Restart the stream and verify.
# After repair, restart the stream
query = (
stream_df.writeStream
.option("checkpointLocation", checkpoint_path)
.start()
)
# Verify the first few batches complete successfully
import time
time.sleep(30)
print(json.dumps(query.lastProgress, indent=2, default=str))spark.sql.streaming.minBatchesToRetain=100 for automatic checkpoint compactionmonitoring/streaming_dashboard.sqlAnalysisException: Cannot resolve column name after source schema changesmergeSchema errors on Delta writes| Change Type | Impact | Auto-Recovery? |
|---|---|---|
| New nullable field added | Low — nulls in existing rows | Yes, with mergeSchema |
| Field renamed | High — old field becomes null, new field ignored | No |
| Field type changed | High — cast failures or silent data corruption | No |
| Field removed | Medium — stream may fail or column becomes all null | Depends on query |
Step 1: Identify the schema change.
# Compare current source schema to the schema in the streaming query
source_df = spark.read.table("analytics.bronze.raw_events")
source_df.printSchema()
# Check the target table schema
target_df = spark.read.table("analytics.silver.processed_events")
target_df.printSchema()Step 2: Handle the change based on type.
For new fields (additive change):
# Ensure mergeSchema is enabled on the sink
query = (
stream_df.writeStream
.option("mergeSchema", "true")
.start()
)For renamed or removed fields:
# Update the stream's transformation logic to map old name to new name
stream_df = stream_df.withColumnRenamed("old_field", "new_field")
# Or handle missing fields gracefully
from pyspark.sql.functions import lit
if "removed_field" not in stream_df.columns:
stream_df = stream_df.withColumn("removed_field", lit(None).cast("string"))For type changes:
# Cast the field to the expected type with error handling
from pyspark.sql.functions import col, when
stream_df = stream_df.withColumn(
"amount",
when(col("amount").cast("double").isNotNull(), col("amount").cast("double"))
.otherwise(None)
)Step 3: Reset the checkpoint if the schema change is incompatible with the existing state.
Stateful operations (aggregations, dedup) store data in the old schema. If the schema change affects columns used in state, you must reset:
# Archive old checkpoint
dbutils.fs.mv(checkpoint_path, f"{checkpoint_path}_pre_schema_change", recurse=True)
# Restart stream — will reprocess from startingOffsetsStep 4: Backfill if needed.
If the stream was down during the schema transition, use availableNow to catch up:
query = (
stream_df.writeStream
.trigger(availableNow=True)
.option("checkpointLocation", checkpoint_path)
.start()
)
query.awaitTermination()mergeSchema=true on all streaming sinks as a defaultdlt/expectations_library.py)ExecutorLostFailure (executor X exited caused by one of the running tasks) Reason: Container killed by YARN for exceeding memory limitsjava.lang.OutOfMemoryError: Java heap space or GC overhead limit exceededOOM failures cause the micro-batch to fail and retry. With checkpointing, no data is lost, but the pipeline stalls until the OOM condition is resolved. Repeated OOM on the driver can crash the entire cluster.
Step 1: Identify the memory consumer.
# Check state store size
progress = query.lastProgress
for op in progress.get("stateOperators", []):
print(f"State rows: {op.get('numRowsTotal', 0)}")
print(f"Memory used: {op.get('memoryUsedBytes', 0) / 1024 / 1024:.1f} MB")# Check micro-batch size
print(f"Input rows: {progress.get('numInputRows', 0)}")
print(f"Batch size bytes: {progress.get('sources', [{}])[0].get('numInputRows', 0)}")Step 2: Apply immediate mitigation.
For state store OOM — tighten the watermark:
# Reduce watermark from 24 hours to 1 hour (evicts old state faster)
stream_df = stream_df.withWatermark("event_timestamp", "1 hour")For batch size OOM — limit data per trigger:
# Kafka: reduce max offsets per trigger
.option("maxOffsetsPerTrigger", "10000") # was 100000
# Auto Loader: reduce max files per trigger
.option("cloudFiles.maxFilesPerTrigger", "100") # was 1000For executor OOM — increase memory:
spark.executor.memory=32g
spark.executor.memoryOverhead=8g
spark.sql.streaming.stateStore.rocksdb.blockCacheSizeMB=512Step 3: Switch to RocksDB state store if using the default provider.
spark.sql.streaming.stateStore.providerClass=com.databricks.sql.streaming.state.RocksDBStateStoreProviderRocksDB stores state on local SSD instead of the JVM heap, dramatically reducing OOM risk for stateful operations.
Step 4: Restart the stream.
After applying config changes, restart the cluster and the streaming query. The query will resume from the last committed checkpoint.
maxOffsetsPerTrigger or equivalent to cap micro-batch sizemonitoring/streaming_alerts.sql)config/autoscaling_config.md)KafkaException: Failed to construct kafka consumer or TimeoutExceptionEventHubException: The connection was inactive for more than the allowed idle timeoutquery.status shows isTriggerActive: false with no errors (silent stall)Structured Streaming will retry indefinitely on transient source failures. The stream pauses but does not lose its checkpoint position. Data produced during the outage accumulates at the source and will be processed when connectivity resumes (assuming source retention is sufficient).
| Source | Default Retry Behavior | Data at Risk |
|---|---|---|
| Kafka | Retries with backoff; fails after request.timeout.ms | None, if Kafka retention covers outage duration |
| Event Hubs | Retries with backoff; reconnects automatically | None, if Event Hub retention covers outage duration |
| Auto Loader | Polls for new files; resumes when storage accessible | None, files persist in storage |
Step 1: Verify source availability.
# Kafka — test broker connectivity
import socket
broker_host, broker_port = kafka_brokers.split(",")[0].split(":")
try:
sock = socket.create_connection((broker_host, int(broker_port)), timeout=10)
sock.close()
print("Kafka broker reachable")
except Exception as e:
print(f"Kafka broker unreachable: {e}")# Auto Loader — verify storage path
try:
files = dbutils.fs.ls(source_path)
print(f"Storage accessible: {len(files)} files found")
except Exception as e:
print(f"Storage unreachable: {e}")Step 2: Check for credential expiration.
# Test secret access
try:
secret = dbutils.secrets.get(scope="kafka", key="sasl_password")
print("Secret accessible (value redacted)")
except Exception as e:
print(f"Secret access failed: {e}")If credentials have expired, rotate them in the Databricks secret scope and restart the stream.
Step 3: Check the streaming query status.
for q in spark.streams.active:
print(f"Query: {q.name}")
print(f" Active: {q.isActive}")
print(f" Status: {q.status}")
if q.exception():
print(f" Exception: {q.exception()}")Step 4: Handle extended outages.
If the source was down longer than its retention period, data may be lost. For Kafka:
# Check if offsets are still available
# If you get OffsetOutOfRange on restart, you must decide:
# Option A: Reset to earliest available offset (may cause duplicates)
.option("startingOffsets", "earliest")
# Note: this only applies to a NEW checkpoint. For existing checkpoints,
# set failOnDataLoss=false to skip past unavailable offsets:
.option("failOnDataLoss", "false")Step 5: Restart and monitor catch-up.
After source recovery, monitor the stream's catch-up progress:
import time
for _ in range(10):
time.sleep(30)
progress = query.lastProgress
if progress:
input_rate = progress.get("inputRowsPerSecond", 0)
process_rate = progress.get("processedRowsPerSecond", 0)
print(f"Input: {input_rate:.0f} rows/sec | Processed: {process_rate:.0f} rows/sec")When processedRowsPerSecond stabilizes near inputRowsPerSecond, the backlog is cleared.
failOnDataLoss=false for Kafka sources in production to survive offset expiration gracefullyDo not wait for a production failure to discover your recovery process is broken. Test these procedures regularly in a staging environment.
| Test | Procedure | Expected Result |
|---|---|---|
| Checkpoint corruption | Delete a commit file, restart stream | Stream replays the affected batch and continues |
| State reset | Delete the state/ directory, restart stream | Stream rebuilds state; verify aggregation results |
| Full checkpoint loss | Delete entire checkpoint, restart stream | Stream starts from startingOffsets; verify no data loss from source |
| Schema addition | Add a nullable column to source, observe stream | Stream continues; new column appears in target |
| Schema type change | Change a column type in source | Stream fails; apply cast fix, restart |
| OOM simulation | Reduce executor memory to 2g, run high-volume batch | Stream fails; increase memory, verify recovery |
| Kafka broker down | Stop one broker in a multi-broker cluster | Stream retries and reconnects to remaining brokers |
| Credential rotation | Rotate secret, restart stream | Stream authenticates with new credentials |
# 1. Record current stream position
pre_test_progress = query.lastProgress
pre_test_batch = pre_test_progress.get("batchId", 0)
print(f"Pre-test batch: {pre_test_batch}")
# 2. Stop the stream
query.stop()
query.awaitTermination(timeout=60)
# 3. Introduce the failure (e.g., delete a commit file)
dbutils.fs.rm(f"{checkpoint_path}/commits/{pre_test_batch}")
# 4. Restart the stream
query = stream_df.writeStream.option("checkpointLocation", checkpoint_path).start()
# 5. Verify recovery
import time
time.sleep(60)
post_test_progress = query.lastProgress
post_test_batch = post_test_progress.get("batchId", 0)
print(f"Post-test batch: {post_test_batch}")
assert post_test_batch >= pre_test_batch, "Stream did not recover"
print("Recovery test PASSED")Stream failed?
├── Check query.exception()
│ ├── Checkpoint error? → See Scenario 1
│ ├── Schema/Analysis error? → See Scenario 2
│ ├── OOM / memory error? → See Scenario 3
│ └── Connection / timeout error? → See Scenario 4
│
├── Stream running but not progressing?
│ ├── query.status shows waiting for data → Source may be empty (normal)
│ ├── query.status shows processing → Batch is slow (see performance tuning guide)
│ └── No lastProgress updates → Stream is stuck; restart the query
│
└── Stream running but producing wrong results?
├── Duplicates appearing → Check dedup watermark and checkpoint health
├── Missing data → Check source retention and failOnDataLoss setting
└── Wrong aggregations → State store may be corrupt; consider state reset
*Real-Time Streaming Toolkit — Datanest Digital — https://datanest.dev*
Datanest Digital — https://datanest.dev
Streaming performance tuning is an iterative process. Small configuration changes can have outsized impact on throughput, latency, and cost. This guide covers the primary levers available in Databricks Structured Streaming and Delta Live Tables, with concrete recommendations for each.
The goal is a pipeline that processes data within your latency SLA while using the fewest resources possible.
The trigger interval controls how frequently Spark initiates a new micro-batch.
# Fixed interval — most common for production
query = stream_df.writeStream.trigger(processingTime="10 seconds").start()
# Available once — process all available data, then stop (batch-like)
query = stream_df.writeStream.trigger(availableNow=True).start()
# Continuous processing (experimental) — sub-millisecond latency
query = stream_df.writeStream.trigger(continuous="1 second").start()| Interval | Throughput | Latency | Resource Usage | Best For |
|---|---|---|---|---|
| 1-5 seconds | Lower | Lowest | Highest | Real-time dashboards, alerting |
| 10-30 seconds | Medium | Low | Medium | Event processing, CDC pipelines |
| 1-5 minutes | Highest | Medium | Lowest | Log aggregation, batch-like streaming |
availableNow | Maximum | N/A (one-shot) | Burst | Catch-up processing, backfill |
availableNow for backfill operations instead of lowering the trigger interval — it processes all available data in a single run without the overhead of repeated scheduling.Partitioning determines parallelism and directly affects throughput.
For Kafka and Event Hubs, the number of source partitions sets the upper bound on read parallelism.
# Override minimum read partitions (useful when source has few partitions)
stream_df = (
spark.readStream
.format("kafka")
.option("minPartitions", "16") # Read parallelism independent of topic partitions
.load()
)| Source Partitions | Cluster Workers | minPartitions | Notes |
|---|---|---|---|
| 4 | 2-4 | 8-16 | Over-partition to utilize all cores |
| 16 | 4-8 | 16 (default) | Source partitions match worker count |
| 64+ | 8-16 | Omit | Source already well-partitioned |
Control the number of output files per micro-batch:
# Repartition before writing to control file count
stream_df = stream_df.repartition(8)
# Or use Delta's optimizeWrite to handle it automatically
spark.conf.set("spark.databricks.delta.optimizeWrite.enabled", "true")Recommendation: Enable optimizeWrite and let Delta handle output file sizing. Manual repartitioning is only needed for specific partitioning schemes (e.g., date-based).
The spark.sql.shuffle.partitions setting controls parallelism for operations that require data shuffling (joins, aggregations, deduplication).
# Default is 200 — often too high for streaming, too low for large batch
spark.conf.set("spark.sql.shuffle.partitions", "auto") # DBR 14.3+
# Or set explicitly
spark.conf.set("spark.sql.shuffle.partitions", "64")| Cluster Cores | Steady-State Throughput | Recommended Shuffle Partitions |
|---|---|---|
| 8-16 | Low (<1K events/sec) | 16-32 |
| 16-64 | Medium (1K-10K events/sec) | 64-128 |
| 64-256 | High (10K-100K events/sec) | 128-256 |
| 256+ | Very high (100K+ events/sec) | 256-512 |
On Databricks Runtime 14.3+, use adaptive shuffle:
spark.sql.shuffle.partitions=auto
spark.sql.adaptive.enabled=true
spark.sql.adaptive.coalescePartitions.enabled=trueThis automatically adjusts partition count based on data volume per micro-batch, avoiding the overhead of too many empty partitions during low-traffic periods.
Stateful operations (windowed aggregations, deduplication, stream-stream joins) maintain state across micro-batches. State store size is the most common cause of streaming OOM failures.
The default in-memory state store does not scale. Switch to RocksDB:
spark.sql.streaming.stateStore.providerClass=com.databricks.sql.streaming.state.RocksDBStateStoreProvider# Block cache — increase for large state (default 64MB)
spark.sql.streaming.stateStore.rocksdb.blockCacheSizeMB=256
# Write buffer — increase for high write throughput
spark.sql.streaming.stateStore.rocksdb.writeBufferSizeMB=64
# Compaction style
spark.sql.streaming.stateStore.rocksdb.compactOnCommit=falseWatermarks control when state is evicted. A tighter watermark means less state but more late-arriving events are dropped.
# 1 hour watermark — state for each key lives at most 1 hour past max event time
stream_df = stream_df.withWatermark("event_timestamp", "1 hour")| Watermark Delay | State Size Impact | Late Event Tolerance | Use Case |
|---|---|---|---|
| 10 minutes | Small | Low | Well-ordered sources (e.g., database CDC) |
| 1 hour | Medium | Medium | Event streams with moderate delays |
| 24 hours | Large | High | IoT devices with intermittent connectivity |
Track state store metrics via the streaming query progress:
progress = query.lastProgress
state_info = progress.get("stateOperators", [])
for op in state_info:
print(f" Num rows total: {op.get('numRowsTotal', 0)}")
print(f" Memory used: {op.get('memoryUsedBytes', 0) / 1024 / 1024:.1f} MB")
print(f" Rows updated: {op.get('numRowsUpdated', 0)}")
print(f" Rows removed: {op.get('numRowsRemovedByWatermark', 0)}")If numRowsTotal grows continuously without corresponding removals, your watermark is too wide or state is not being evicted.
Control how much data each micro-batch processes.
# Maximum offsets (messages) per trigger across all partitions
.option("maxOffsetsPerTrigger", "100000")
# Fetch size per partition
.option("kafka.max.partition.fetch.bytes", "1048576") # 1 MB# Maximum files per trigger
.option("cloudFiles.maxFilesPerTrigger", "1000")
# Maximum bytes per trigger
.option("cloudFiles.maxBytesPerTrigger", "10g")# Maximum events per trigger
.option("maxEventsPerTrigger", "50000")1. Start conservative: Set maxOffsetsPerTrigger to a value that completes within 50% of your trigger interval.
2. Measure batch duration: Check query.lastProgress["durationMs"] for the processing time breakdown.
3. Increase gradually: Double the batch size and verify that batch duration stays within the trigger interval.
4. Watch for OOM: If executor memory usage spikes, reduce batch size or increase executor memory.
Each micro-batch has measurable phases:
progress = query.lastProgress
durations = progress.get("durationMs", {})
# Typical output:
# {
# "addBatch": 4500, <-- actual data processing
# "commitOffsets": 50,
# "getBatch": 200,
# "latestOffset": 100,
# "queryPlanning": 150,
# "triggerExecution": 5100,
# "walCommit": 100
# }| Phase | If Slow, Check... |
|---|---|
latestOffset | Source connectivity (Kafka brokers, Event Hub endpoints) |
getBatch | Source read performance; increase minPartitions |
queryPlanning | Query complexity; simplify transformations or cache schemas |
addBatch | Transformation logic; shuffle partitions; sink write performance |
walCommit | Checkpoint storage latency; use faster storage tier |
# Combine small files automatically
spark.databricks.delta.optimizeWrite.enabled=true
# Auto-compact after writes
spark.databricks.delta.autoCompact.enabled=true
spark.databricks.delta.autoCompact.minNumFiles=50
# Target file size (default 128MB — reduce for streaming)
spark.databricks.delta.targetFileSize=64mbEnsure filters are pushed to the source to reduce data volume:
# Good — filter is pushed to Kafka (topic-level)
stream_df = spark.readStream.format("kafka").option("subscribe", "events.clicks").load()
# Bad — reading all topics and filtering in Spark
stream_df = spark.readStream.format("kafka").option("subscribePattern", "events.*").load()
stream_df = stream_df.filter(col("topic") == "events.clicks")| Metric | Source | Alert Threshold | Meaning |
|---|---|---|---|
inputRowsPerSecond | query.lastProgress | Sudden drop >50% | Source throughput changed |
processedRowsPerSecond | query.lastProgress | Below inputRowsPerSecond sustained | Pipeline falling behind |
addBatch duration | query.lastProgress.durationMs | >80% of trigger interval | Batch processing too slow |
numRowsTotal (state) | query.lastProgress.stateOperators | Continuous growth | State not being evicted |
| Executor memory usage | Spark UI / Ganglia | >80% of allocated | Risk of OOM |
| Checkpoint directory size | Storage monitoring | >10,000 files | Needs compaction |
Enable metrics reporting for external monitoring systems:
spark.sql.streaming.metricsEnabled=trueThis exposes metrics to the Spark metrics system, which can forward to Prometheus, Datadog, or other collectors configured on the cluster.
SELECT
batch_id,
trigger_execution_ms,
add_batch_ms,
num_input_rows,
ROUND(num_input_rows / (add_batch_ms / 1000.0), 0) AS rows_per_second
FROM streaming_metrics_log
WHERE stream_name = '${stream_name}'
ORDER BY batch_id DESC
LIMIT 100;Use this checklist when deploying a new streaming pipeline or investigating performance issues:
maxOffsetsPerTrigger or equivalent batch size limit setoptimizeWrite and autoCompact enabled for Delta sinksmetricsEnabled=true)*Real-Time Streaming Toolkit — Datanest Digital — https://datanest.dev*