Read the checkpoint before you touch the config
Inspect the checkpoint's committed offsets, then compare them with Kafka's current earliest offsets before deciding whether to restart or reset.
Read the diagnostic →A Kafka-backed stream restarts with “offset was changed from X to Y, some data may have been missed.” The checkpoint and the topic disagree — and the tempting one-line fix silently skips the gap instead of recovering it. The tactic: read the checkpoint's offsets file, restart on a versioned checkpoint, and backfill the missing window from an independent replayable raw source.
Built for engineers who need an answer they can test in the next notebook, job run, or incident review.
Inspect the checkpoint's committed offsets, then compare them with Kafka's current earliest offsets before deciding whether to restart or reset.
Read the diagnostic →Reset on a versioned checkpoint, keep the old path for forensics, and backfill the missing window from an independent replayable raw source.
See the recovery pattern →The checkpoint outranks the config on a restart. Use the decision table to know when a new checkpoint is required.
Get the answer →The error message is the whole diagnosis: the checkpoint recorded offset 48,210 for the partition, and the topic now offers something lower — or nothing at that position at all. Spark can't tell a recreated topic from aged-out retention, so it assumes the worst: data may have been lost. Failing loudly is the entire point of the default.
Three things cause it, and they all mean the same thing — the checkpoint and the topic describe different worlds: (1) the topic was deleted and recreated; (2) Kafka's retention aged the offsets out while the stream was stopped; (3) the topic was reconfigured — partitions added or moved — while the checkpoint pointed at the old layout. In every case, the offsets the stream wants no longer exist in Kafka.
import json
ckpt = "/Volumes/catalog/schema/vol/checkpoints/events_stream"
# The offsets/ directory holds one JSON file per committed batch:
# {"events": {"0": 48210, "1": 48177}}
offset_files = sorted(
dbutils.fs.ls(ckpt + "/offsets/"), key=lambda f: int(f.name)
)
expected = json.loads(dbutils.fs.head(offset_files[-1].path))
print(json.dumps(expected, indent=2))
# Illustrative expected output:
# {"events": {"0": 48210, "1": 48177}}
Recommended learning: Databricks docs: Structured Streaming checkpoints
Illustrative scenario: A team streams click events from a Kafka topic into a Delta bronze table — append mode, hourly trigger, checkpoint on a Unity Catalog volume. Friday evening they stop the job for a runtime upgrade. The topic's retention is 48 hours — fine for a normal weekend, but the upgrade slips to Monday morning. The restarted query dies immediately with the IllegalStateException. The checkpoint's last committed offsets (illustrative: partition 0 → 48,210) sit below the topic's new earliest offset (illustrative: 51,003) — about 2,800 offsets of events are simply gone from Kafka. Restarting again changes nothing: the offsets don't exist to resume from.
Run the offsets-file read from the tip and establish two numbers: what the checkpoint expects, and the topic's current earliest offset. If the expected offsets are still within the topic's range, restart as-is — the query resumes from the checkpoint. If they sit below the earliest offset, the data is gone from Kafka: reset, don't retry.
# Illustrative values: replace KAFKA, catalog, and paths with your own
KAFKA = "kafka-broker1:9092"
CKPT = "/Volumes/catalog/schema/vol/checkpoints/events_stream"
recovered = (spark.readStream.format("kafka")
.option("kafka.bootstrap.servers", KAFKA)
.option("subscribe", "events")
.option("startingOffsets", "latest") # new query: only new data from here
.load()
.selectExpr("CAST(key AS STRING) AS event_key",
"CAST(value AS STRING) AS payload",
"topic", "partition", "offset",
"timestamp AS kafka_ts"))
q = (recovered.writeStream.format("delta")
.outputMode("append")
.option("checkpointLocation", CKPT + "_v2") # versioned: never reuse _v1
.trigger(processingTime="1 hour")
.toTable("catalog.bronze.events"))
Two things matter here. startingOffsets=latest on the new query means no replay — it picks up only records arriving from here on. And the _v2 path keeps the reset auditable: _v1 stays behind as the forensic record of what the old query committed. Per the docs: “When you delete the files in a checkpoint directory or change to a new checkpoint location, the next run of the query begins fresh.”
# Illustrative independent replay source. Replace the path, format, columns,
# and timestamps with your landing-zone contract and observed missing window.
RAW_REPLAY = "/Volumes/catalog/raw/landing/events"
MISSING_FROM = "2026-09-25T18:00:00Z"
MISSING_TO = "2026-09-28T08:00:00Z"
gap = (spark.read.format("json")
.load(RAW_REPLAY)
.where(f"event_time >= timestamp '{MISSING_FROM}' "
f"AND event_time < timestamp '{MISSING_TO}'")
.select("event_key", "payload", "event_time"))
# Dedupe by event key before writing. Valid only when event_key is unique
# per event; duplicate keys would collapse distinct events.
(gap.dropDuplicates(["event_key"])
.write.format("delta").mode("append")
.saveAsTable("catalog.bronze.events_backfill"))
The expired offsets (illustrative: 48,210 and 48,177) are below Kafka's earliest available offset (illustrative: 51,003) and cannot be read from Kafka. This batch read uses an explicitly illustrative independent raw landing-zone source, filtered to the missing event-time window. Dedupe by event key before writing because the backfill can overlap the new stream's first micro-batch; the dropDuplicates is valid only when the event key is unique per event — duplicate keys would collapse distinct events. Validate the backfilled row count against the gap size (illustrative: ~2,800) before unioning it into the bronze table.
Use this when: Kafka retention expired during downtime and an independent replayable raw source exists for the missing event-time window.
If no replayable source exists: the range is unrecoverable — declare the gap explicitly, document the missing window, advance the checkpoint past it deliberately (never silently skip it), and fix retention and monitoring so it does not recur.
Avoid this when: the committed offsets are still inside Kafka's retention window — just restart; or your only changes were filters, rate limits, trigger intervals, or UDF logic, which the docs list as safe on the same checkpoint.
Never set failOnDataLoss=false in production to clear this error. It converts a loud, diagnosable failure into silent skipping. Keep the default true so the failure stays loud.
Streaming recovery you can't afford to get wrong? I run checkpoint-recovery reviews — the offsets audit, the retention math, and the backfill drill, on your pipeline. Get in touch through the contact in About.
Recommended learning: Data engineering workflows — pipelines, orchestration, and streaming practice
The config changed. The stream ignored it. Here's why — and the decision table for what actually needs a new checkpoint.
startingOffsets only applies when a NEW streaming query starts. A restart from an existing checkpoint ignores the option entirely and resumes from the committed offsets — silently. Your config change did nothing because the checkpoint outranks it. If you want different offsets, you need a new checkpoint first (the _v2 reset in the pattern above) — then the option takes effect on that new query.
The docs draw a hard line on which query changes force a new checkpoint (“Recover after changes in a Structured Streaming query”): new checkpoint required for the number or type of input sources, subscribed Kafka topics or Auto Loader paths, stateful operation types, state schema, or output sink type. Safe to keep the checkpoint: adding or removing filters, rate limits (e.g. maxOffsetsPerTrigger), trigger intervals, or UDF logic inside mapGroupsWithState (semantics may change).
Put together: changed the topics or the sink and want different offsets? New checkpoint, then set the offsets on the new query. Only tuned the trigger or a filter? Keep the checkpoint — a restart is enough. And hand-editing the offsets/ file — “checkpoint surgery” — is the third option some engineers reach for: it can work for stateless micro-batch reads, but the checkpoint also holds commits/ and state, and editing one file breaks their consistency. The versioned reset is the supported path.
Recommended learning: Databricks Community — checkpoint changes not working on a Databricks job
Applies to: Databricks Runtime 14.x and above (Spark 3.5+); Structured Streaming Kafka source (self-managed Kafka, MSK, Confluent, or Event Hubs' Kafka endpoint); Delta Lake sinks in append output mode. The scenario and all figures in it are illustrative. Last verified September 30, 2026 against official Databricks and Apache Spark documentation. Corrections: Sep 30, 2026 — corrected the Kafka batch-read step: expired offsets cannot be re-read from Kafka; backfill must come from a replayable source, otherwise the range is unrecoverable.
Issues, newest first.
Contact: sbogale@sb9tech.com
Lakehouse Digest is a short, practical newsletter for Databricks engineers, published by the Lakehouse Digest team. Each issue delivers one tactic to try, one production pattern to understand, and one hard developer question answered with code and primary-source links. It is an independent publication and is not affiliated with Databricks, Inc.
Topic-led learning paths for readers who want to turn this issue’s tactics into repeatable Databricks practice.
Strengthen the platform basics behind notebooks, jobs, data workflows, and collaborative development.
Browse DataCamp Databricks courses →The official guide behind this issue: Kafka source options, batch reads with explicit offsets, and the Kafka-to-Delta example.
Read the Databricks docs →Pipelines, orchestration, and streaming practice that turn this issue's tactics into repeatable habits.
Browse DataCamp data engineering →Partner links: we may earn a commission if you enroll through these recommendations, at no extra cost to you.
A technical audience of Databricks engineers, reading implementation notes they can test the same day.
Launch inventory is one clearly labeled, problem-first placement per issue — built for products that help data teams build, govern, observe, or ship more reliably.
Sponsor Lakehouse Digest →