When to Use Broadcast Joins in Apache Spark: A Decision Guide
A decision guide for Spark broadcast joins: when the small side is small enough, how to tune autoBroadcastJoinThreshold safely, and how to verify the plan actually changed.
08 Jul 2025, 08:27 UTC

Joining a large fact table against a small dimension table is one of the most common Spark workloads, and the default sort-merge join pays a full shuffle of both sides even when one side would fit in a few megabytes. A broadcast join eliminates that shuffle by sending the small side to every executor. The decision you face is not whether broadcast joins are good — they usually are — but whether your small side is actually small enough, and how far you can safely raise the threshold.
The decision and its constraints
By default, Spark's Catalyst optimizer broadcasts a join side when its estimated size is below spark.sql.autoBroadcastJoinThreshold, which defaults to 10MB. Your real constraints are:
- Executor memory. The broadcasted relation is materialized in memory on the driver first, then on every executor. If it does not fit, you get out-of-memory failures, not a graceful fallback.
- Driver memory. The driver collects the broadcast side before distributing it, so the driver must also hold the full relation.
- Size estimate accuracy. Catalyst uses statistics from the metastore or computed via
ANALYZE TABLE. Stale or missing stats can cause Spark to miss a broadcast opportunity — or attempt one it should not. - Join type. Broadcast hash joins support inner joins and most outer joins where the broadcast side is the appropriate one (e.g., you cannot broadcast the left side of a left outer join and get correct semantics — Spark will not plan it).
Comparing the join strategies
| Strategy | Shuffle? | Best when | Main risk |
|---|---|---|---|
| Broadcast hash join | No shuffle of either side | One side fits comfortably in executor memory | Executor/driver OOM if the estimate is wrong |
| Sort-merge join | Both sides shuffled and sorted | Both sides are large; default for big-big joins | Shuffle I/O and sort spill dominate runtime |
| Shuffled hash join | Both sides shuffled | Large joins where one partition-side fits in memory per partition; rarely chosen by default since Spark 3 prefers sort-merge | Memory pressure during hash table build |
The trade-off is straightforward: broadcast joins convert network shuffle into a one-time data distribution. If the small side is 5MB, you replace a full shuffle of a 100GB table's join keys with a 5MB transfer per executor. If the "small" side is actually 2GB, you have moved the problem to executor memory and may crash the stage.
Option 1: Let the optimizer decide, but fix your statistics
The cheapest path is to keep the default threshold and make sure Catalyst's estimates are accurate. On Spark 3.x, run this in a Spark SQL session or notebook against the metastore:
ANALYZE TABLE dim_customers COMPUTE STATISTICS FOR ALL COLUMNS;
This requires write access to the table's metadata. Afterward, check DESCRIBE EXTENDED dim_customers for the sizeInBytes statistic. If the reported size is well under 10MB, Spark should choose a broadcast automatically. If stats are missing entirely, Catalyst falls back to conservative defaults and frequently picks sort-merge even for tiny tables.
Option 2: Raise the threshold deliberately
If your dimension table is, say, 200MB and your executors have several GB of free memory, raising the threshold is reasonable:
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", "209715200") -- 200MB in bytes
Set this at session level in your notebook or job, or at cluster level in spark-defaults.conf if it should apply to all jobs. Session-level is safer: a cluster-wide high threshold can surprise other workloads. Note that setting the threshold to -1 disables automatic broadcasting entirely, which some teams do to force explicit hints.
On Databricks, the same spark.sql.autoBroadcastJoinThreshold applies; there is no separate Databricks-specific broadcast threshold in current runtimes — older documentation references to Databricks-specific variants should be verified against your runtime version before relying on them.
Option 3: Force it with a hint
When you know better than the optimizer — for example, a heavily filtered side that stats misjudge — use an explicit hint:
SELECT /*+ BROADCAST(d) */ *
FROM fact_events f
JOIN dim_customers d ON f.customer_id = d.customer_id
WHERE d.region = 'EMEA';
Or in the DataFrame API: fact.join(broadcast(dim), "customer_id"). The hint overrides the size check, so the OOM risk is entirely yours. Use it when you have measured the filtered size, not as a guess.
Validating the choice
Do not trust the configuration; check the physical plan. Two ways:
1. From code (any Spark session, no special permissions):
df.join(small_df, "customer_id").explain("formatted")
Look for BroadcastHashJoin and a BroadcastExchange node wrapping the small side. If you see SortMergeJoin instead, the broadcast did not happen — revisit stats or the threshold.
2. In the Spark UI. Open the SQL tab, select your query, and inspect the DAG. A broadcast join shows a BroadcastExchange stage and, critically, the join stage should show minimal shuffle read/write compared to a sort-merge baseline. Run the same join with spark.sql.autoBroadcastJoinThreshold set to -1 to get that baseline, then compare stage durations and shuffle metrics.
Limitations to keep in mind
- Broadcast joins do nothing for large-to-large joins; both sides still need a shuffle-based strategy or a redesign (e.g., pre-aggregating one side).
- Size estimates on tables with rapidly changing partitions go stale quickly; re-run
ANALYZE TABLEon a schedule or after large loads. - Very large broadcasts (hundreds of MB) can saturate the network during distribution and increase GC pressure; measure rather than assuming bigger thresholds are better.
- The driver collects the broadcast side first — a driver with default 1–2GB memory can fail even when executors are fine.
The practical rule: keep the default, keep statistics fresh, and only raise the threshold or add hints after you have confirmed the real data size and verified the plan change in the UI.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.