Symptoms
One of these, usually after data growth or a code change:
org.apache.spark.SparkException: Could not execute broadcast in 300 secs.
You can increase the timeout for broadcasts via spark.sql.broadcastTimeout or disable broadcast join by setting spark.sql.autoBroadcastJoinThreshold to -1
java.lang.OutOfMemoryError: Java heap space (on the driver)
SparkException: Total size of serialized results ... is bigger than spark.driver.maxResultSize
SparkException: Cannot broadcast the table that is larger than 8GB
How broadcast joins work
For a broadcast hash join, Spark:
- Runs the query for the small side and collects it to the driver.
- Builds a hash table and sends it to every executor.
- Joins each partition of the big side locally, with no shuffle.
So the "small" side must fit in driver memory, serialize within limits, and reach executors before spark.sql.broadcastTimeout (default 300 s). The timeout clock also includes the time to compute the small side, which surprises many people.
Step 1: confirm the plan
df = orders.join(customers, "customer_id")
df.explain("formatted")
Look for BroadcastHashJoin and BroadcastExchange. In the Spark UI, the SQL tab shows the size of the broadcast relation (data size) and which side was broadcast.
Step 2: find why Spark thought it was small
Spark broadcasts when the estimated size is below spark.sql.autoBroadcastJoinThreshold (default 10 MB). With AQE, it can also switch to broadcast at runtime when the actual shuffle output is below spark.sql.adaptive.autoBroadcastJoinThreshold.
Common reasons for a wrong decision:
| Cause | Example |
|---|---|
| Stale or missing statistics | Table grew from 5 MB to 4 GB; catalog still says 5 MB |
| Filter estimate too optimistic | A filter the optimizer thinks removes 99% of rows removes 10% |
Explicit broadcast() hint in code | Written when the table was small |
| Threshold raised globally | Someone set the threshold to 1 GB to "speed things up" |
| Exploding join on the small side | Small side is itself a join that multiplies rows |
Refresh statistics:
ANALYZE TABLE sales.customers COMPUTE STATISTICS;
-- column stats help join and filter estimates
ANALYZE TABLE sales.customers COMPUTE STATISTICS FOR COLUMNS customer_id, region;
Step 3: choose the right fix
The small side really is small but slow to compute. Raise the timeout, or materialize it first so the broadcast itself is quick:
spark.conf.set("spark.sql.broadcastTimeout", "900")
The "small" side isn't small anymore. Stop broadcasting it. Use a hint just for this join instead of turning broadcast off everywhere:
SELECT /*+ SHUFFLE_MERGE(c) */ o.*, c.region
FROM sales.orders o JOIN sales.customers c ON o.customer_id = c.customer_id;
Or in PySpark: orders.join(customers.hint("shuffle_merge"), "customer_id"). Remove any broadcast() call that forces the old behaviour.
Global threshold set too high. Put it back near the default and broadcast specific tables with hints where you've measured the benefit:
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", str(50 * 1024 * 1024)) # 50 MB
Broadcast is right, but the driver is too small. A 1-2 GB broadcast needs a driver with plenty of heap. Increase driver memory or node size, and raise spark.driver.maxResultSize only after confirming the table should be broadcast.
Warning
Setting
spark.sql.autoBroadcastJoinThreshold = -1for the whole cluster fixes the failure but can make many other queries slower (more shuffles). Prefer per-join hints.
Step 4: AQE-specific cases
With AQE on (default in Spark 3.2+), a join planned as sort-merge can be converted to broadcast once Spark sees the actual size. That's usually good. Problems occur when:
spark.sql.adaptive.autoBroadcastJoinThresholdis set high, so a large runtime side gets broadcast.- Many concurrent queries on a shared cluster broadcast at once and exhaust driver memory.
Set the AQE threshold explicitly if you've raised the static one, so they don't silently follow each other.
Quick reference
| Error | Most likely fix |
|---|---|
| Could not execute broadcast in 300 secs | Small side slow to compute: cache/materialize it or raise broadcastTimeout |
| Driver OOM during join | Stop broadcasting a large side; refresh stats; bigger driver |
| Larger than 8GB | Use shuffle hash or sort-merge join; 8 GB is a hard limit |
| maxResultSize exceeded | Same as OOM; raise it only if broadcast is intended |
| Regression after data growth | Stale statistics or an old broadcast() hint |
Prevent it next time
- Run
ANALYZE TABLEas part of table maintenance for dimension tables. - Review explicit
broadcast()hints when tables grow; prefer letting AQE decide. - Track driver memory usage in your job monitoring; a creeping driver heap is an early warning.
- Related: skew joins, AQE and salting, Databricks driver not responding.