GOLDENMATCH_* environment-variable table. For each, you get what it does, its default, and when to set it (or leave it alone).
If you are tuning a large run and just want the answer, jump to Recommended setups. If something ran far slower than you expected, read Gotchas that cost a day first — every item there is a real footgun.
Most GoldenMatch knobs are environment variables, not config fields, because they switch a code path (native vs Python, in-memory vs distributed) rather than describe your data. Data-shape settings — matchkeys, blocking keys, golden rules — live in Configuration. This page is the operational layer on top of that.
Recommended setups
You almost never need to set anything for correctness — auto-config and the planner choose sane defaults. These blocks are about speed at scale and reproducibility, not behavior.Local / interactive (up to ~500K rows)
Set nothing. Zero-config is the supported path. If you want the compiled kernels for a measurable speed-up on the scoring/clustering hot loops, install the optional native wheel:GOLDENMATCH_NATIVE=auto already runs every component that has a kernel natively (native is the reference; pure-Python is the lossy fallback). You do not need to set GOLDENMATCH_NATIVE=1.
Single box at scale (1M–200M rows, 16-core / 64 GB class)
backend=bucket for this lane automatically when the native kernel is enabled. This is the exact env used by the quality-invariant scale ladder (1K → 100M), so it is the validated single-box recipe.
Distributed (Ray cluster, 50M+ rows)
__row_id__ column, and it writes golden records to output_path rather than returning an in-memory clusters dict. The recall-complete path (block-shuffle scoring + randomized-contraction WCC) is default-on and was validated end-to-end at 100M rows on a real 5-node GCP cluster (9.2 min, 20M clusters recovered exactly, 0.36 GB driver RSS). See Distributed pipeline for the full contract. At single-box scale, prefer bucket — only reach for a Ray cluster when the data genuinely won’t fit one box.
Native acceleration
Rust is the reference implementation (as of the 2026-07 reference-mode change). The Rust/PyO3 kernel is the source of truth for GoldenMatch’s hot-path primitives; pure-Python is the lossy fallback used only when a native wheel isn’t installed (the wheel ships aspip install goldenmatch[native]). A central gate (core/_native_loader.py) selects per component.
GOLDENMATCH_NATIVE (core/_native_loader.py):
Under
auto, native runs for every component that has a kernel symbol — clustering, block_scoring, pairs, featurize, hashing, autoconfig, sketch, pprl_bloom, and the multimodal perceptual hashes. Each cleared bit-/ULP-level parity against the Python reference, so the fallback is output-identical (golden-vector verified). (The former per-component allowlist, _GATED_ON, is retained in the loader as sign-off documentation but no longer governs auto.) The sketch component covers both MinHash/LSH (#1081) and SimHash-over-embeddings (#1090) band hashing (~29x faster; the single source of truth across Python/Rust/TS, so semantic blocking runs on the Rust sketch-core kernel by default). Fellegi-Sunter block scoring is native-authoritative by default as of goldenmatch 2.6.0 (GOLDENMATCH_FS_NATIVE=0 opts out to the reproducible numpy path); it is scoped to the probabilistic path only.
The autoconfig component is the shared, pyo3-free decision core (goldenmatch-autoconfig-core) behind auto-config — the planner, column classifier (with the adaptive 1 − 1/√n identifier-cardinality floor), the sample→full pair-count extrapolation (the corrected ratio² + Chao1 estimate), and the adaptive decision thresholds (the sparse-match floor and the per-type exact-matchkey floor). It runs native under auto and is byte-identical to the pure-Python oracle (golden-vector parity across Python / native / wasm-TS). The same crate compiles to WebAssembly so the TypeScript surface consumes the identical decision logic — one source of truth, zero cross-surface drift.
When to set =1: any large or benchmarked run where a silent Python fallback would be a disaster. The cost of a missing wheel is enormous (the scoring kernel is the dominant stage), and auto hides it.
When to leave it auto: normal local use. If the wheel is installed you already get native; if it isn’t, you get working Python.
GOLDENMATCH_NATIVE_ADDRESS_NORMALIZE
Default off. When set to 1, address-normalize transforms run as a Polars-native expression instead of the per-row Python refdata plugin. On address-heavy data at scale the per-row plugin is a real bottleneck in the prep stage; the native path removes it.
- Set it (
=1) when you haveaddresscolumns and are running at scale. Output is parity-tested on representative inputs. - Leave it off for small runs, or if you hit an address-canonicalization edge case (it is opt-in precisely because one integration-level cluster-count drift hasn’t been fully root-caused).
GOLDENMATCH_NATIVE_RAYON_MIN_PAIRS
Default 20000000. Candidate-pair count per kernel call below which the native block-scorer runs in the calling thread instead of fanning out to rayon. This exists to avoid a futex-park stall observed on some 8-core Linux CPUs (issue #688). The Python caller already parallelizes across buckets, so the sequential path loses nothing on small calls. Leave it at the default unless you are reproducing #688; 0 forces rayon always, a huge value forces sequential always.
GOLDENMATCH_NATIVE_SKETCH_RAYON_MIN_ROWS
Default 10000. Row count below which the native MinHash/LSH batch sketcher (#1081) runs in the calling thread instead of fanning out to rayon — the same #688 calling-thread-vs-fanout guard, applied to the sketch kernel. The SimHash batch sketcher (#1090) shares this threshold. Relevant whenever the sketch kernel runs native — which is the default under auto since #1090 (set GOLDENMATCH_NATIVE=0 to force the Python path), or under GOLDENMATCH_NATIVE=1. 0 forces rayon always, a huge value forces sequential always.
Backend selection
Same pipeline, different execution engine. Auto-config picks one from your row count; you can override.
Force one in config or on the CLI:
The old single-threshold
GOLDENMATCH_AUTOCONFIG_BACKEND / GOLDENMATCH_AUTOCONFIG_BACKEND_THRESHOLD env overrides were removed in v2.0. Backend selection is now the v3 planner’s job — it decides from (ComplexityProfile, RuntimeProfile). To force a backend, set it explicitly on GoldenMatchConfig(backend=...) or the CLI --backend.ANN blocker backend (embedding/semantic blocking)
The embedding ANN blocker (ANNBlocker) resolves one of three inner-product backends per index build: native HNSW → FAISS → numpy. Native HNSW is the optional goldenmatch-hnsw wheel — a pure-Rust IndexHNSWFlat with zero C dependencies (no FAISS); FAISS (goldenmatch[embeddings]) is exact IndexFlatIP; numpy is the always-available all-pairs fallback. Scores are the raw inner product on the HNSW/FAISS paths and cosine on numpy — identical when vectors are L2-normalized (the default embedder output).
In auto mode HNSW is chosen only where it wins: a large corpus and a small neighbor count per probe. Below that gate, brute force is both faster and exact, so small indexes (and the retrieve-all pattern behind the persistent VectorIndex) stay on the exact path unchanged.
goldenmatch-hnsw is an optional wheel, installed separately (pip install goldenmatch-hnsw) exactly like FAISS. Without it, the ANN blocker uses FAISS if present, else the numpy fallback — no behavior change.Distributed pipeline
This is the knob most people get wrong, so read the contract before you run a 100M job.GOLDENMATCH_DISTRIBUTED_PIPELINE selects how run_dedupe_pipeline_distributed (the Ray Dataset entry point) executes:
Phase 5 (
=2) contract — different from in-memory dedupe:
- Input must carry a global
__row_id__column. Without it, each partition synthesizes local IDs that collide and the weakly-connected-components step merges unrelated clusters. - Output is written to
output_pathas parquet golden records. It does not return an in-memory clusters dict the waydedupe_dfdoes. - It still materializes two things on the driver: the ~5K-row auto-config sample, and cluster aggregates for golden-field synthesis. These are the last “cheat-lines” being retired.
GOLDENMATCH_ENABLE_DISTRIBUTED_RAY (default 0) is separate: it only lets the v3 planner pick ray during backend selection. It does not turn on the streaming pipeline. Explicit backend="ray" always works without it. The planner stopped auto-picking ray after the Distributed Plan v1 stack failed the 5M kill criterion against the bucket backend, so ray is opt-in.
Recall-complete clustering at scale (#844, shipped 2026-06-11). Phase 5’s default path now block-shuffles scoring so true duplicates co-locate, then runs a relational randomized-contraction weakly-connected-components pass that clusters correctly across input-partition boundaries. This replaced two earlier WCC implementations that died at 100M (a driver head-wedge and a Ray streaming-executor deadlock). It is default-on and validated end-to-end at 100M (9.2 min, 20M clusters exact). On a multi-node cluster the WCC checkpoints each round to GOLDENMATCH_DISTRIBUTED_WCC_SCRATCH, which must be a shared path (e.g. gs://...) — a node-local scratch breaks the cross-node parquet reads and now raises rather than silently under-merging.
Only scoring needs distribution at this scale; clustering usually doesn’t. The scored pair set is small (~
(id_a, id_b, score) per match), so at 100M rows it’s ~1.8 GB — it fits one node. Clustering therefore routes to the in-memory scipy connected-components pass (seconds) automatically whenever the pairs fit driver RAM, and only falls back to the distributed WCC (a multi-hour tail) when they genuinely don’t. You don’t need to set GOLDENMATCH_DISTRIBUTED_CLUSTERING_THRESHOLD for this — it’s the default.If the distributed score stage leaves workers idle (
MapBatches(_score): … [backpressured:tasks(ResourceBudget)] with the object store well under capacity), _score is reservation-capped, not CPU-bound. Lower GOLDENMATCH_DISTRIBUTED_OP_RESERVATION (→ 0.2) and/or raise GOLDENMATCH_DISTRIBUTED_SHUFFLE_PARTS, then re-measure — the optimal pair is cluster-shaped. (Shrinking Ray Data’s target_max_block_size is not a safe lever: _score keeps each co-located partition whole via batch_size=None, so a split block would split a group and under-score.)You bring the Ray cluster — GoldenMatch ships the pipeline, not the bootstrap. The GCP recipe is in
docs/distributed-ray-cluster-setup.md. At single-box scale the bucket backend is validated through 200M and is simpler — use it unless the data won’t fit one box.Fast path at scale (distribute only what doesn’t fit)
The single most expensive distributed-tuning mistake is distributing a stage that fits in memory. “The job is big” does not mean “every stage must be distributed.” Ask it per stage:
Concrete: don’t force the distributed WCC on a pair set that fits. By default
GOLDENMATCH_DISTRIBUTED_CLUSTERING_THRESHOLD is unset and the routing is memory-aware — clustering stays on the fast in-memory scipy.csgraph path whenever the scored pair set fits available driver RAM, and only distributes when it genuinely won’t. So at 100M (~110M pairs / ~1.76 GB) the default keeps clustering in-memory on a 64–256 GB head. Set an explicit integer only if you want to force a hard pair-count boundary either way:
0 does the opposite — it forces the slow distributed WCC on everything.
Recover a result without re-running. The Phase-5 connected-components clustering is independent of the WCC algorithm, so a completed run’s cluster assignments (written to gs:// via the assignments_output_path hook) are the result. If a run finishes but the output is lost, or you want to re-score, download the assignments and score them against your ground truth on a 64 GB box — no cluster, no re-run. The 100M group_by for that scoring OOMs a 16 GB box, so run it on a large runner, not your laptop.
Prep & scoring perf opt-ins
These trade nothing for speed/memory at scale and are safe to leave at their defaults. Two are already on.
Diagnostic-only — these can SLOW the run:
Fused kernel auto-routing
The pipeline can route covered slow-path runs to the fused Arrow-native kernels, byte-identically, with no config change. There are two seams with very different maturity: golden routing is a broad default-on win; match routing is a narrow capacity-survival path that fires only under memory pressure.Auto-config & planning
If auto-config raises
ControllerNotConfidentError, that’s by design at ≥100K rows on a RED profile — it’s refusing to run a noise-result dedupe. See Passing an explicit config for recovery (the in-repo docs/explicit-config.md has the failing-sub-profile table).
There are also a number of experimental blocking and probabilistic knobs (GOLDENMATCH_BLOCKING_*, GOLDENMATCH_FS_*, GOLDENMATCH_NOISE_AWARE_*, GOLDENMATCH_QUALITY_AWARE_BLOCKING, GOLDENMATCH_FD_NEGATIVE_EVIDENCE). These are research/tuning levers, default-safe, and listed in the full table below. Most users never touch them.
Config-suggestion (healer) knobs
These tune the healer — thereview_config config-suggestion loop, which is now wired into the default dedupe_df pipeline as an advisory surface (a free controller signal gates a cheap attach; see GOLDENMATCH_SUGGEST_ON_DEDUPE). The healer needs goldenmatch[native] and degrades gracefully without it. You almost never need to change these — the defaults are the values the suggester gym selected.
Servers, security & storage
When you expose any server (MCP, REST, A2A, web UI) on a non-loopback host, it is fail-closed: it refuses to start without the matching bearer token.
Storage / persistence:
Product analytics (opt-in):
Anonymous usage analytics — off by default. Emits only when
GOLDENMATCH_ANALYTICS is on and a POSTHOG_API_KEY is set, so a user’s machine stays silent unless they opt in (the project’s own hosted services set the flag server-side). PII-free by construction: only coarse scalars (scale bands, backend, version) ever leave, never record values, column names, or paths. Full collection list + guarantees in packages/python/goldenmatch/ANALYTICS.md.
Gotchas that cost a day
Read these before a big run. Each is a real trap that produces a slow or wrong result with no error.backend='ray' ran in-memory and never used the cluster
backend='ray' ran in-memory and never used the cluster
backend="ray" (or dedupe_df(backend="ray")) alone runs the Phase 2 cheat-line: it collects the whole frame to the driver and dedupes in memory. The cluster sits idle. To actually stream across workers you need GOLDENMATCH_DISTRIBUTED_PIPELINE=2, a Ray Dataset input with a global __row_id__, and you read golden output from output_path — not the return value. On a multi-node cluster also set a shared GOLDENMATCH_DISTRIBUTED_WCC_SCRATCH (e.g. gs://...). For single-box scale, use bucket, not ray.It ran 100x slower than the docs because native silently fell back to Python
It ran 100x slower than the docs because native silently fell back to Python
Under the default
GOLDENMATCH_NATIVE=auto, if the native wheel isn’t importable (not installed, ABI mismatch, stale build) the scoring kernel silently runs in Python — orders of magnitude slower. On any run where speed matters, set GOLDENMATCH_NATIVE=1 so a missing wheel raises instead of crawling. Confirm with python -c "import goldenmatch_native" or check goldenmatch.core._native_loader.native_available().The prep stage dominated wall on address-heavy data
The prep stage dominated wall on address-heavy data
The per-row Python address-normalize plugin is a real bottleneck at scale. Set
GOLDENMATCH_NATIVE_ADDRESS_NORMALIZE=1 to use the Polars-native expression instead. It’s off by default, so you have to opt in.A distributed 100M run took hours instead of minutes
A distributed 100M run took hours instead of minutes
The scored pair set is small (~1.76 GB at 100M) and fits one node. By default
GOLDENMATCH_DISTRIBUTED_CLUSTERING_THRESHOLD is unset and routing is memory-aware — clustering stays in-memory (scipy.csgraph, ~60s) whenever the pair set fits driver RAM, so a 100M run should already keep clustering on the fast path. The multi-hour tail happens if something forces the distributed WCC (whose gs://-checkpoint rounds are slow): check you haven’t set the threshold to 0 (forces the slow path on everything) or to a value below your pair count. See Fast path at scale. Distribute the scoring (the rows don’t fit), not the clustering (the pairs do).Results changed between runs for no reason
Results changed between runs for no reason
Cross-run auto-config memory (
GOLDENMATCH_AUTOCONFIG_MEMORY=1, the default) lets a prior run influence the next. For reproducible benchmarks set GOLDENMATCH_AUTOCONFIG_MEMORY=0.Python hung at import on Windows
Python hung at import on Windows
Polars runs a WMI CPU probe at import that can hang on some Windows boxes. Set
POLARS_SKIP_CPU_CHECK=1. (Add PYTHONIOENCODING=utf-8 if non-ASCII output mangles the console.)Throughput tier (sketch-then-verify)
New in #1083, builds on #1080. An opt-in high-throughput path for large text corpora where you care about near-duplicate recall but can trade precision for speed. Instead of the full fuzzy/Fellegi-Sunter pipeline, it blocks with MinHash/LSH (lsh) or SimHash (simhash) and confirms candidate pairs by cheap
sketch distance before any field-level scoring. Think: training data dedup, web
crawl dedup, paper-corpus cleaning.
Enabling it
Passthroughput= to dedupe_df:
What it does
- Auto-selects blocking strategy. Picks
simhash(cosine/semantic) when an embedder is reachable, otherwise falls back tolsh(MinHash/Jaccard on shingles). Uses the longest text column in the frame. - Blocks with sketch buckets. Records that share at least one LSH band become
candidates. The
recall_targetcontrols the banding split via the LSH probability curve. - Verifies by sketch distance only. Pairs that clear
similarity_thresholdare kept; the field-level fuzzy/FS scorer does not run. - Raises
ThroughputNotApplicableErrorwhen the frame has no text column. Fix: pass an explicitBlockingConfigto direct it to a column.
Posture and honest reporting
The throughput tier does not measure F1 or precision.DedupeResult.throughput_posture
(and the controller telemetry throughput block) carry the metrics the tier can
honestly report:
expected_recall is the LSH-theoretic probability that a pair at exactly
similarity_threshold Jaccard/cosine collides in at least one band. It is
not a measured F1 or recall against ground truth. For pairs above threshold
the curve is tighter; for pairs below it, collision probability drops rapidly.
This is an honest bound, not a precision measurement.
Measured throughput (benchmark)
Thebench-corpus-dedup harness (scripts/bench_corpus_dedup/,
.github/workflows/bench-corpus-dedup.yml) measures the tier end-to-end against a
real public corpus, with injected ground-truth near-dups so recall is measurable.
On a slice of FineWeb (web crawl), the zero-config dedupe_df(df, throughput=…)
path measured:
docs/sec rises with scale because the wall is dominated by a fixed ~50s of
zero-config auto-config at these sizes; the sketch dedup itself is ~9s at 70k
(≈7,800 docs/sec raw). Measured recall ≈ 0.43 is the honest LSH recall over a
mix of exact / partial-overlap / paraphrase near-dups — the paraphrase and partial
dups often do not collide in the LSH bands. (Contrast
expected_recall above: the
per-pair collision probability at the similarity threshold, not a recall over a
real corpus — which is exactly why the bench measures recall directly.)
A deterministic per-PR CI perf gate (throughput-gate job, via
scripts/bench_corpus_dedup/throughput_perf_gate.py) guards regression on the
machine-independent cost — candidate pairs, reduction ratio, and measured recall on
a vendored offline corpus — against a committed baseline, so a change can’t quietly
blow up the candidate count or drop recall.
Scale note: the published numbers are the validated 14k–70k range. 100k+ corpora currently bottleneck on the survivorship/golden stage; lifting that ceiling (and a datatrove head-to-head) are tracked follow-ups.
Scope
The throughput tier is single-node only. Distributed throughput (#1084) and the product surface (#1085) are follow-on issues. Default-off (throughput=None) is
byte-identical to today in every metric.
Configuration precedence
When the same setting is reachable multiple ways, the more specific wins:- Explicit API / CLI argument (
dedupe_df(..., backend=...),--backend) — highest. - Config field (
GoldenMatchConfig(backend=...), YAML). - Environment variable (
GOLDENMATCH_*). - Built-in default (auto-config / planner choice) — lowest.
Full environment-variable reference
EveryGOLDENMATCH_* variable the package reads (plus the one GOLDEN_* exception, GOLDEN_DIAGNOSTICS), grouped by who should care. Defaults verified against source. Most users only ever touch the recommended setups.
Everyday & scale
Distributed sub-knobs
Perf opt-ins & diagnostics
Auto-config & blocking (advanced)
Probabilistic Fellegi-Sunter (advanced)
Atype: probabilistic matchkey trains EM once, then its candidate pairs are scored by one of four paths, chosen automatically — you rarely set these by hand, but this is the map when you need to:
- bucket (default) — memory-bounded field-hash buckets; the #1798-safe route for
static/multi_passblocking on the default backend. - external-blocks — memory-bounded scorer for strategy-generated candidates (
lsh/ann/learned/canopy/sorted_neighborhood) that buckets can’t re-derive from field hashes. - batched — the legacy per-block path. Reached by
GOLDENMATCH_FS_DEFAULT_BUCKET=0or an explicit non-default backend. Eagerbuild_blocks, so memory grows with dataset size. - fused — a single Arrow-native FFI call (block + score + dedup + cluster) under est-RSS pressure on an artifact-free config; 2–19× leaner peak RSS. See Fused kernel auto-routing.
GOLDENMATCH_FS_NATIVE), falling back to the vectorized numpy scorer (GOLDENMATCH_FS_VECTORIZED) and then the scalar loop. Every knob below is an escape hatch or an advanced tuning lever — the defaults are the recommended path.
Path routing
Scoring engine (native vs numpy vs scalar)
Model & math
Other advanced (scorer / golden / cluster — not FS-specific)
Servers, security, storage, LLM/embedding
Not a
GOLDENMATCH_* var but relevant: set OPENAI_API_KEY or ANTHROPIC_API_KEY for LLM scoring and the LLM auto-config fallback, and POLARS_SKIP_CPU_CHECK=1 on Windows to skip the Polars import-time CPU probe.Backends and scale
The measured numbers behind each backend choice.
Scale envelope
The block-size failure modes that matter more than the backend.
Distributed routing
The planner decides scoring, clustering, and golden routing per stage, keyed off driver RAM (not cluster total). Thelint_routing / explain_routing MCP
tools surface these decisions and flag overrides that force a slow path; a
slow-path override at scale refuses unless allow_slow_path=true.
Single box
With no Ray cluster connected, every stage runs in-memory / in-process. There is nothing to distribute to, so the projections below do not apply.Driver-RAM projection
When a cluster is present, each materializing stage distributes only when its working set exceeds the driver-RAM budget (available_ram_gb × 0.6):
- Clustering / WCC distributes when the projected edge set
(
estimated_pair_count × 16 bytes) exceeds the budget. At 100M rows the ~1.76 GB edge set fits, so WCC runs in-memory (scipy) — forcing the distributed path there is a multi-hour tail. - Scoring and golden distribute when the row-frame estimate
(
n_rows × 512 bytes) exceeds the budget. The same data therefore distributes scoring on a 48 GB-driver cluster but keeps it in-memory on a 256 GB box.
Overrides
distributed_routing.* config pins and the legacy
GOLDENMATCH_DISTRIBUTED_CLUSTERING_THRESHOLD env var are honored but linted.
CLUSTERING_THRESHOLD=0 forces distributed WCC regardless of the projection; at
scale the linter flags it ERROR and the runtime refuses unless
allow_slow_path=true.