When to Use Spark Broadcast Joins: A Practical Guide to Avoiding Shuffle Overhead
Learn how Spark’s broadcast join can eliminate costly shuffles, when it applies automatically, and how to force it or avoid pitfalls with a concrete example.
27 Aug 2026, 10:08 UTC

Problem: Shuffling is Expensive
In distributed SQL engines like Apache Spark, a join that requires data to be moved across the network is called a shuffle. Shuffle operations write intermediate data to disk, read it back, and incur network traffic. When one side of a join is tiny compared to the other, shuffling that small side is wasteful. The question is how to avoid it without changing the data layout or writing custom code.
Thesis: Use Broadcast Joins When One Dataset is Small
A broadcast join (also called a map‑side join) replicates the small dataset to every executor. Each executor then performs the join locally, eliminating the shuffle. Spark can do this automatically, but you need to understand the thresholds, how to force it, and what happens if the broadcast fails.
How Spark Decides to Broadcast
- Threshold key:
spark.sql.autoBroadcastJoinThreshold(default 10 MB in Spark 3.x). - When the serialized size of the smaller DataFrame is below this value, Spark inserts a
BroadcastExchangein the physical plan. - Adaptive Query Execution (AQE) can adjust the threshold at runtime based on actual shuffle statistics, potentially broadcasting even larger tables if the plan is cheaper.
Inspecting the Plan
# In a PySpark shell or notebook
join_df = small_df.join(large_df, "id", "inner")
print(join_df._jdf.queryExecution().executedPlan().toString())
# Look for "BroadcastHashJoin" or "BroadcastExchange"
Alternatively, use the UI: under the Physical Plan tab you will see a BroadcastExchange node if the join is broadcasted.
Forcing a Broadcast Join
Sometimes Spark chooses a shuffle join even though the small side is below the threshold (e.g., because of a complex predicate). You can explicitly hint Spark to broadcast:
# PySpark
from pyspark.sql import functions as F
broadcast_small = F.broadcast(small_df)
joined = broadcast_small.join(large_df, "id", "inner")
Or by setting the config to zero to disable auto broadcasting and then using the hint:
# Disable automatic broadcast
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", 0)
# Force broadcast with hint
joined = small_df.join(large_df, "id", "inner", hint="broadcast")
These commands run on the driver. The hint is propagated to the executors where the join is executed.
What Happens When the Broadcast Fails?
- If the serialized size exceeds the threshold, Spark falls back to a shuffle join. The physical plan will show a
ShuffleExchangeinstead of aBroadcastExchange. - Large broadcasts can exhaust executor memory, leading to
OutOfMemoryErroror spilling to disk. - Non‑serializable objects in the DataFrame schema can trigger
ClassNotFoundExceptionduring broadcast.
Concrete Example: Joining a 5 MB Customer Table with a 1 GB Orders Table
Assume we have two Parquet tables in S3: customers (≈5 MB) and orders (≈1 GB). We want an inner join on customer_id.
# PySpark 3.5+ example
spark = SparkSession.builder.appName("BroadcastJoinDemo").getOrCreate()
# Load datasets
customers = spark.read.parquet("s3://bucket/customers/")
orders = spark.read.parquet("s3://bucket/orders/")
# Verify size (optional, for debugging)
print("customers size:", customers.count())
# Join with broadcast hint
result = customers.join(orders, customers.customer_id == orders.customer_id, "inner")
# Show a few rows
result.select("customer_id", "order_id", "amount").show(5)
Because customers is only 5 MB, Spark will automatically broadcast it. You can confirm by checking the UI or printing the plan as shown earlier.
Trade‑offs and Limitations
- Memory pressure: Broadcasting replicates the data on every executor. If you have many executors or the broadcasted table is near the threshold, you may run out of memory.
- Serialization cost: The broadcasted data is serialized once on the driver and deserialized on each executor. For complex schemas, this can add CPU overhead.
- Dynamic thresholds: AQE can change the threshold at runtime. If the broadcasted table grows slightly during a job, the plan may switch to a shuffle join mid‑execution, causing a minor performance hit.
- Driver memory: The driver must hold the entire broadcasted dataset in memory. With very large small tables (e.g., 200 MB), the driver may need to spill to disk.
Practical Checklist Before You Broadcast
- Calculate the serialized size of the small DataFrame:
df.rdd.mapPartitions(lambda p: [pickle.dumps(list(p))]).map(len).sum()(adjust for your serialization library). - Set
spark.sql.autoBroadcastJoinThresholdto a value that comfortably fits your executor memory (e.g., 50 MB for 8 GB executors). - Use
spark.conf.set("spark.sql.adaptive.enabled", true)to let AQE decide dynamically. - Monitor the UI: look for
BroadcastExchangenodes and ensure noOutOfMemoryErrorlogs appear. - If the broadcast consistently fails, consider partitioning the small table or using a shuffle join with
repartitionByRangeto reduce data skew.
Conclusion
Broadcast joins are a powerful, low‑overhead way to join a tiny table with a large one in Spark. By understanding the threshold, using hints when necessary, and monitoring executor memory, you can keep joins fast and avoid the pitfalls of shuffling. Remember that the key to success is balancing broadcast size against available memory and letting Adaptive Query Execution do its job when you enable it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.