Lakehouse Digest
ISSUE 016 · OCTOBER 4, 2026

Your watermarked dedup is still hoarding state

A Structured Streaming dropDuplicates wearing a watermark can still grow its state store forever. Two silent traps: a withWatermark placed after dropDuplicates never reaches the dedup operator, and an event-time column missing from the dedup keys keeps the dedup global. The fix is two lines.

THE THREE-PART READ

Two traps, one state store.

A watermark is useful only when it reaches the stateful operator — and plain watermarked dedup is bounded only when event time participates in the key.

TIP

The watermark is a direction, not a decoration

Operator order and key choice determine whether old dedup state can leave. Get either one wrong and the store keeps growing.

Understand the traps →
IMPLEMENTATION

Broken, still broken, fixed

Compare the reversed chain, the missing-key chain, the documented fix, and the bounded dropDuplicatesWithinWatermark alternative.

Compare the patterns →
DEV Q&A

Watermarked but unbounded?

Check the chain, check the keys, then inspect state. Pick global dedup only when arbitrary lateness is a real requirement.

Get the answers →

Last verified against official Databricks documentation: October 4, 2026. Correction history: first publication — no corrections.

Two conditions must be true

First, apply withWatermark before dropDuplicates. Every official example uses this order, and the Databricks documentation says it plainly: # Apply watermark before distinct operation. Reverse the chain and the watermark never reaches the dedup operator. Spark documents the consequence under “Without watermark”: “the query stores the data from all the past records as state.” No error is documented for the reversed order. The dangerous outcome is a query that runs while state keeps accumulating.

Second, put the event-time column in the dedup key list. Correct order alone is not enough for dropDuplicates. In the Databricks Community deep-dive's documented run, the chain was .withWatermark("event_time", "5 minutes").dropDuplicates(["event_id"]): the order was right, but event_time was missing from the keys. After the watermark advanced to 10:02, keys A and B remained in the state store. The write-up calls this global deduplication: the state store grows continuously, potentially causing out-of-memory issues.

The two-line fix: follow Spark's documented pattern: streamingDf.withWatermark("eventTime", "10 seconds").dropDuplicates(["guid", "eventTime"]). With event time in the key, a record at event time T is removed from state when the watermark reaches or passes T.

The A/B result above is the outcome of that documented run, not a deterministic per-record arrival guarantee. Watermarks are threshold semantics: they control when state is eligible to be evicted as the engine's watermark advances.

Setup

events = (
    spark.readStream
         .format("delta")
         .table("main.bronze.events")
         .select("event_time", "event_id", "payload")
)

REVERSED — the watermark never reaches dedup

reversed_order = (
    events
      .dropDuplicates(["event_id", "event_time"])
      .withWatermark("event_time", "5 minutes")
)
Outcome: the watermark is downstream of the dedup operator, so it cannot bound that operator's state. This behaves as Spark's documented “without watermark” case: data from all past records is kept as state.

MISSING-KEY — correct order, still global

global_dedup = (
    events
      .withWatermark("event_time", "5 minutes")
      .dropDuplicates(["event_id"])
)
In the deep-dive's documented run: the watermark advanced to 10:02, but keys A and B stayed in the state store. The order was correct; the event-time column was not a dedup key, so the dedup remained global.

FIXED — Spark's documented key pattern

deduplicated = (
    streamingDf
      .withWatermark("eventTime", "10 seconds")
      .dropDuplicates(["guid", "eventTime"])
)
Outcome: this is the Spark guide's verbatim pattern. With eventTime in the dedup keys, a record with event time T is removed from the state store once the watermark is at or beyond T.

SAFER ALTERNATIVE — bound duplicate lateness directly

bounded_dedup = (
    streamingDf
      .withWatermark("eventTime", "10 seconds")
      .dropDuplicatesWithinWatermark(["guid"])
)

dropDuplicatesWithinWatermark requires Databricks Runtime 13.3 LTS or above. It guarantees deduplication only for duplicates arriving within the watermark threshold. A key is removed when the watermark reaches or passes event_time + delay. Set the threshold greater than the maximum timestamp difference between duplicate events. It needs a watermark to work at all.

Verify it yourself

Use the state-store reader from the deep-dive to list stored keys. Run it against the checkpoint for your query, advance the input past the watermark, and confirm old keys disappear.

state_keys = (
    spark.read
         .format("statestore")
         .load("<checkpointLocation>")
)

state_keys.show(truncate=False)
What success looks like: keys whose eviction threshold is behind the advancing watermark no longer appear. Do not infer bounded state from the presence of withWatermark in the code; verify the state store.
  1. Check the chain: withWatermark is applied before dropDuplicates.
  2. Check the key: the event-time column is in the dedup key list — or use dropDuplicatesWithinWatermark on Databricks Runtime 13.3 LTS+.
  3. Check the state: inspect the state store and confirm old keys are actually evicted as the watermark advances.

Use this when

Deduplicating a stream where duplicates arrive within a known, bounded delay. Choose a watermark threshold that exceeds the maximum timestamp difference between duplicate events, then verify eviction in state.

Avoid this when

Duplicates can arrive arbitrarily late. Plain global dropDuplicates without a watermark is the documented option, but it stores all past records as state. Treat that as a memory budget, not a config flag. Do not use dropDuplicatesWithinWatermark below Databricks Runtime 13.3 LTS.

I set a watermark — why is my state store still growing?
Check two things in order: is withWatermark applied before dropDuplicates, and is the event-time column in the dedup keys? Then inspect the state store with the statestore reader to confirm whether old keys are actually evicting.
Should I use dropDuplicatesWithinWatermark instead?
If you are on Databricks Runtime 13.3 LTS or above: yes, when a bounded duplicate delay is acceptable. It bounds deduplication to the threshold, removes a key when the watermark reaches or passes event_time + delay, and requires a watermark to work at all.
When is global dedup actually what I want?
When duplicates have no bounded lateness. Plain dropDuplicates without a watermark is Spark's documented “without watermark” option. It stores everything, so budget the state store and checkpoint accordingly.
Assumptions: Databricks Runtime — any LTS for dropDuplicates with withWatermark; Databricks Runtime 13.3 LTS+ for dropDuplicatesWithinWatermark; a Delta or other streaming source; standard compute.
Provenance: Last verified against official Databricks docs: 2026-10-04. Correction history: first publication — no corrections.
RECOMMENDED LEARNING

Bound the state — then verify the eviction.

Start with the two-line repair, then strengthen the platform and streaming skills behind it.

GO DEEPER

The three-step dedup audit

Check the chain, check the key, then inspect the state store and prove that old keys leave.

Save the audit →
BUILD FOR PRODUCTION

Data engineering workflows

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.

KEEP READING

Read the next / newest issue

Issue 016 is the newest issue. Go back one issue, return to the latest issue, or browse the full archive.