Exact matching happens in stages deliberately ordered from cheapest to most expensive, so that expensive work only ever runs on files that could still be duplicates.
Stage one: group by size. Files are bucketed by exact byte length and any bucket holding a single file is discarded. This costs no file reads at all — the size already came from the directory enumeration. On a typical drive this removes the large majority of files immediately, because being byte-identical requires being the same length first.
Stage two: collapse hardlinks. Before hashing anything, entries that are the same physical file reached through different paths are folded together, identified by volume serial number and file index rather than by path. Without this step a set of hardlinks would be reported as duplicates you could reclaim, which would be both wrong and pointless — they already share their storage.
Stage three: partial hash. Each remaining candidate gets a BLAKE3 hash of its first 64 KB. Partial digests are grouped, and again anything unique is discarded. This is where most surviving non-duplicates are eliminated, because files that share a size usually diverge early — different headers, different metadata, different first block. Reading 64 KB is dramatically cheaper than reading a multi-gigabyte file, so this stage buys most of the remaining speed.
Stage four: full hash. Only files that share both a size and a partial digest are read in full and hashed with BLAKE3 through a reusable buffer. A group is reported only when full digests match, which means the files are byte-identical. This is the difference between "probably a duplicate" and "the same bytes".
The guard between stages. Immediately before each hash pass, DupeDoctor re-checks that the file's size and modification time still match what enumeration recorded. A file that changed mid-scan is dropped rather than hashed, because a digest computed from a file that is being written is meaningless and acting on it would be dangerous. Scans of live directories are normal, and this is what keeps that safe.
Two related properties are worth knowing. Hashing runs in parallel across cores, so the full-hash stage is bounded by your storage rather than by a single thread. And every third-party parser the scanner touches runs inside a panic guard, so one malformed file is skipped and recorded rather than ending the scan — which is also why the release build deliberately keeps stack unwinding enabled instead of aborting on panic.