Skip to content
PHP2Shell: The PHP Bug
Akshay Thakare

Spark Execution Model: Jobs, Stages, Tasks and Shuffles

What actually happens between calling an action and getting a result — and why the shuffle boundary is where most performance problems live.

Apache SparkIntermediate3 min readUpdated 11 Oct 2026

From code to cluster

Spark builds a lazy logical plan from your transformations. Nothing runs until you call an action (count, collect, write, show). At that point the driver:

  1. Optimises the logical plan (Catalyst) into a physical plan
  2. Splits it into a job per action
  3. Cuts each job into stages at every shuffle boundary
  4. Runs each stage as a set of tasks — one per partition — on executors
FlowchartSwipe to explore →

Rendering flowchart

Narrow vs wide transformations

Narrow (no shuffle)Wide (shuffle)
select, filter, withColumn, mapgroupBy, join (non-broadcast), distinct, repartition, orderBy
Each output partition depends on one input partitionOutput partitions depend on many input partitions
Pipelined inside one stageEnds a stage; data is written to disk and fetched over the network

Watch what a groupBy does to the data — every record travels to the partition that owns its key:

spark shuffle · groupBy(key)
key A key B key C

input partition 0

ACBA

input partition 1

BBCA

input partition 2

CABC

Stage 1 · narrow transformations run in place

Before: each input partition holds a mix of keys. After the shuffle, all records with the same key live in one partition — that data movement over the network is what makes wide transformations expensive.

A shuffle is expensive because it serialises, writes to local disk, transfers over the network and deserialises every row it moves.

Reading the plan

Python.py
df = (spark.read.parquet("s3://bucket/orders/")
        .filter("status = 'PAID'")
        .groupBy("customer_id")
        .agg(F.sum("amount").alias("spend")))
 
df.explain(mode="formatted")

Look for Exchange nodes — each one is a shuffle. Exchange hashpartitioning(customer_id, 200) means rows are redistributed into 200 partitions by customer_id.

Practical rules

  • Filter and project early so less data reaches the shuffle
  • Tune spark.sql.shuffle.partitions (default 200) to data size; with AQE enabled, Spark can coalesce small shuffle partitions automatically
  • Prefer broadcast joins when one side is small
  • Watch the Spark UI's stage tab: a few tasks taking far longer than the median usually means skew

Finished this note?

Share