File Drop Ingestion

A file drop is a push with the slowest rhythm. A partner, a bank or another team writes files to a bucket or shared folder you can read, on a schedule they control. Your job notices the files, checks them, loads them, and remembers that it did. Each step can go wrong, and the fixes are conventions you agree with the partner before the first file lands.

The half-written file

Uploading a 2GB file takes minutes. A job that lists the folder at 02:03 and finds settlements_2026-10-22.csv cannot tell whether the upload finished or whether the file holds the first 1.3GB of 2. Loading it produces a row count that passes any loose check and a daily total short by a third.

Two conventions solve this; ask the partner for at least 1. With write-then-rename, the partner uploads to settlements_2026-10-22.csv.tmp and renames it when the upload ends, and your job ignores anything ending in .tmp. With a sentinel, an empty file named _SUCCESS by convention is written only after every data file in the batch has landed; if it is missing, your job exits and tries again on its next schedule.

The manifest

A sentinel says the batch is complete. A manifest says what complete means: a small file listing every data file in the batch with its row count and a checksum. With a manifest your job can prove it received the batch the partner sent, rather than whatever happened to be in the folder when it looked.

{
  "batch_date": "2026-10-22",
  "generated_at": "2026-10-23T01:47:12Z",
  "files": [
    {"name": "settlements_2026-10-22_part1.csv", "rows": 184220, "sha256": "9f2c7a..."},
    {"name": "settlements_2026-10-22_part2.csv", "rows": 184301, "sha256": "41be03..."}
  ],
  "total_rows": 368521
}
def load_batch(folder, store):
    if not folder.exists("_SUCCESS"):
        return "waiting"
    manifest = folder.read_json("manifest.json")
    checksums = [f["sha256"] for f in manifest["files"]]
    if store.already_loaded(manifest["batch_date"], checksums):
        return "already loaded"
    all_rows = []
    for entry in manifest["files"]:
        name = entry["name"]
        if not folder.exists(name):
            raise ValueError(f"missing file: {name}")
        if folder.sha256(name) != entry["sha256"]:
            raise ValueError(f"{name}: checksum does not match manifest")
        rows = folder.read_csv(name)
        if len(rows) != entry["rows"]:
            raise ValueError(f"{name}: expected {entry['rows']} rows, got {len(rows)}")
        all_rows.extend(rows)
    store.replace_partition("raw.settlements", manifest["batch_date"], all_rows)
    store.record_loaded(manifest["batch_date"], checksums)
    return "loaded"

A missing file, a checksum that differs from the manifest, or a row count that differs from it raises and blocks the load: the manifest and the data disagree, and loading either version in silence would be wrong. Replacing the whole batch_date partition makes a rerun safe. Recording the checksums means a partner who re-uploads yesterday's batch, sentinel and all, triggers a run that reads the manifest and stops.

Defensive validation

CheckWhat it catchesAction
Sentinel presentA batch still uploadingWait, retry next schedule
Every manifest file exists in the folderA file lost in transferBlock, alert the partner
Row count equals the manifestA truncated or half-written fileBlock
Header matches expected columnsA renamed or reordered columnBlock on rename, warn on an added column
File is not emptyThe partner's export produced nothingBlock
Checksums not seen beforeA re-upload of an old batchSkip and log

The header check is the schema drift lesson applied to files. A CSV carries its column names in line 1. A partner who adds currency at the end has made a backward-compatible change you accept with a warning; one who renames amount to amount_cents has broken the contract, and you stop and talk before loading a row of the new shape.

A bank drops 3 settlement files nightly plus _SUCCESS. On Thursday the sentinel appears and the manifest lists 3 files, but only 2 are in the folder, and the row counts of those 2 match the manifest. What should the loader do?

About this lesson

This section is part of the Ingestion Patterns: Beginner lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.