Lakehouse Digest
ISSUE 025 · OCTOBER 13, 2026

Your bad rows didn't fail — they went to _rescued_data

Auto Loader can move unknown columns, type mismatches, and case mismatches into _rescued_data while the load stays green. The nulls do not mean the values were missing. They mean the values were moved.

THE THREE-PART READ

The load stayed green. The evidence moved.

Rescued fields are not missing data. They are data that failed the declared shape and were moved into a JSON blob. Detect them in bronze, triage them by field, and choose whether to promote or quarantine them before silver.

TIP

Monitor the rescue column

One query reveals rows whose unknown, mismatched, or mis-cased fields were moved while the load stayed green.

See the monitor →
IMPLEMENTATION

Make a bad type visible

Two deterministic input rows become two output rows — one clean and one rescued — without turning the read red.

Run the demo →
DEV Q&A

Broken syntax is not a broken type

Malformed records follow parser mode or bad-record routing. Type mismatches can stay green and move into _rescued_data.

Read the answer →

Last verified against official Databricks docs: October 13, 2026. Corrections: none yet — first published as Issue 025, October 13, 2026.

The values moved

Auto Loader's rescued data column quietly quarantines anything the schema can't parse — unknown columns, type mismatches, case mismatches — into _rescued_data as JSON, and the load stays green. The nulls don't mean the values were missing. They mean the values were moved.

The one-line monitor that belongs on every bronze table you ingest with Auto Loader:

SELECT * FROM bronze_events WHERE _rescued_data IS NOT NULL

If that query returns rows, your load didn't fail — it buried the evidence. The JSON blob also carries the source file path, so you can trace every rescued record back to the exact file that carried it.

Trap inside the trap: if you supply an explicit schema and never set the rescuedDataColumn option, you get no rescue column at all — malformed data then behaves however your parser mode says (dropped or failed), and the quiet quarantine you were counting on never existed.

Illustrative demo — deterministic from the setup

Not an observed production result: the setup writes exactly two JSON records. The row counts and outcomes below follow deterministically from those inputs and the declared schema.

Setup — idempotent, creates every object it references

dbutils.fs.rm("dbfs:/tmp/demo_rescue", True)
dbutils.fs.put("dbfs:/tmp/demo_rescue/clean.json", '{"id": 1, "amount": 100}\n', True)
dbutils.fs.put("dbfs:/tmp/demo_rescue/dirty.json", '{"id": 2, "amount": "one hundred"}\n', True)
bronze = (spark.read.schema("id INT, amount INT").option("rescuedDataColumn", "_rescued_data").json("dbfs:/tmp/demo_rescue"))
bronze.createOrReplaceTempView("demo_bronze")

Inspect the outcome

SELECT id,
       amount,
       _rescued_data IS NOT NULL AS was_rescued
FROM demo_bronze
ORDER BY id;
Expected output — deterministic from the setup: 2 rows. Row 1: id=1, amount=100, was_rescued=false. Row 2: id=2, amount=NULL, was_rescued=true — the "one hundred" string did not become the amount; it was moved into the _rescued_data JSON blob (which also records the source file path), and amount was set to NULL. The read reported success. Two rows in, two rows out — and one of them is lying.

Where malformed data goes

Routing, per official docs:

  1. Column not in schema, type mismatch, case mismatch: _rescued_data JSON; the load stays green.
  2. Whole record malformed — broken JSON or CSV: the legacy _corrupt_record option, or badRecordsPath.
  3. Corrupt file — cannot be opened: skipped and counted in numSkippedCorruptFiles.
  4. COPY INTO ingestion: no rescue column at all — badRecordsPath only.

The quarantine pattern for production bronze

  1. Ingest with rescuedDataColumn set explicitly, even with a declared schema.
  2. Schedule the detector query: count rows where _rescued_data IS NOT NULL per batch, and alert — do not merely log.
  3. Triage each rescued field: extract it with get_json_object(_rescued_data, '$.<field>'). Promote it by fixing the upstream producer or adding the column via addNewColumns evolution and reprocessing the file; or quarantine it by routing to a DQ table and holding it from silver.
  4. Know your mode: addNewColumns fails the stream on a new column and evolves on restart; rescue mode never evolves — new columns park in the rescue column forever. Pick deliberately; rescue is not a schema-evolution strategy.
Derived note on evolution — not a documentation quote: the docs describe no backfill or automatic migration of previously rescued JSON. Committed rows persist as-is; only new batches use the new schema.

Bronze rescue monitor: run per batch, alert on any rows

SELECT COUNT(*) AS total_rows,
       SUM(CASE WHEN _rescued_data IS NOT NULL THEN 1 ELSE 0 END) AS rescued_rows
FROM bronze_events;

Triage the rescued value

SELECT get_json_object(_rescued_data, '$.amount') AS rescued_amount,
       _rescued_data AS rescue_blob
FROM bronze_events
WHERE _rescued_data IS NOT NULL;
Remember: the blob contains the rescued fields plus the source file path.
Why didn't my Auto Loader stream fail? The JSON was broken — half the records were garbage.

Because there are two different kinds of broken, and they behave differently. Broken syntax — a truncated line or an unclosed quote — is genuinely malformed: with FAILFAST it errors, with DROPMALFORMED it drops, and with badRecordsPath it gets written there.

But broken types — a string where the schema says INT — and unknown columns are not “malformed” to the parser. With a rescue column configured, they are quarantined into _rescued_data while the load sails on green. Your “garbage” records arrived as rows with NULLs where the bad values were, plus a JSON blob holding the originals.

So: syntax breaks the load; types quietly move house. If you want the stream to fail on bad values, that is a deliberate configuration — explicit schema, no rescue column, strict parser options — not the default.

Can one bronze layer cover both failure classes?

Practitioner note — not official documentation: a Databricks Community technical blog recommends pairing _rescued_data with the legacy _corrupt_record option to cover both failure classes in one bronze.

Use this when

  1. Messy JSON or CSV: ingesting from sources you do not control.
  2. Loss-sensitive bronze: losing data is worse than a green load with clearly flagged rows.
  3. Backfill resilience: you would rather quarantine an unexpected field than abort at 3 a.m.

Avoid this when

  1. A null key is worse than a failed load: strict contracts want an explicit schema, no rescue column, and FAILFAST so the deviation wakes someone up.
  2. Downstream consumers cannot tolerate nullable-everything bronze: fail before ambiguity becomes a contract.
Assumptions: Databricks Runtime with Auto Loader (cloudFiles) on JSON/CSV; any compute; Delta bronze tables.
Applies to: Databricks Runtime with Auto Loader (cloudFiles), JSON/CSV ingestion, Delta bronze.
Last verified against official Databricks docs: October 13, 2026.
Corrections: none yet — first published as Issue 025, October 13, 2026.
RECOMMENDED LEARNING

Build the ingestion foundation.

Structured practice for Databricks and data engineering workflows.

Partner links: we may earn a commission if you enroll through these recommendations, at no extra cost to you.

Coming soon. Email signup is not active yet. Browse the issue archive in the meantime.