How to Run Apache Spark on AWS EMR: Jobs, Serverless, S3, and Monitoring
This page explains how to run Apache Spark jobs on AWS using EMR on EC2 and EMR Serverless. It covers choosing the right option, submitting PySpark jobs, reading and writing S3 data, and finding logs when something goes wrong.
Simple explanation
Think of running Spark on AWS like managing a warehouse operation. Your PySpark script is the work order. S3 is the warehouse: raw materials arrive on one set of shelves, finished goods go out on another. Amazon EMR is the warehouse itself, and Apache Spark is how the work gets organised inside it.
When a job runs, Spark assigns a driver (the foreman) and a set of executors (the workers). The driver reads the work order and divides the data into chunks. Each executor picks up its chunk, processes it in parallel, and writes its results back to S3. The driver then consolidates everything into the final output.
There are two ways to run Spark on EMR: with a cluster you manage (EMR on EC2) or without a cluster at all (EMR Serverless). The rest of this page explains when to choose each and exactly how to run jobs with both.
How Spark runs on AWS
Here is the execution flow from script to output:
- Upload your script to S3 and submit a job. On EMR on EC2 this is a “step”; on EMR Serverless it is a “job run”.
- EMR provisions compute. On EMR on EC2, the cluster is already running. On EMR Serverless, AWS provisions workers for this specific job (typically 1–3 minutes).
- The Spark driver starts. In cluster mode it runs inside the cluster. It reads your script arguments, plans the execution graph, and assigns tasks to executor processes.
- Executors process the data in parallel. Each executor reads its partition from S3 via EMRFS, processes it, and writes intermediate results to memory or local disk.
- Output is written to S3 or Redshift. When all tasks finish, the driver consolidates results and writes the final output.
- Logs and metrics are emitted. Step logs go to your S3 log bucket, cluster metrics go to CloudWatch, and Spark UI shows the full execution timeline.
Reading and writing S3 works natively because EMRFS is pre-installed on every EMR cluster. Your script uses standard s3:// paths with no extra configuration beyond IAM permissions on the execution role.
When to use this
This page is the right fit if you need to:
- Process large datasets (tens of GB to terabytes) with Python or Scala
- Run ETL or ELT pipelines that need distributed compute
- Join, aggregate, or transform data in a data lake before loading it into a warehouse
- Move from local Spark development to production jobs on AWS
If your job is simple SQL-based transformation on catalogued data, AWS Glue or Amazon Athena may be simpler. If you are designing the broader pipeline architecture first, read the data pipeline design guide before coming back here.
EMR on EC2 vs EMR Serverless vs Glue
Running many jobs per hour or need Spot Instances? Use EMR on EC2. Running daily or weekly batch jobs and want zero cluster management? Use EMR Serverless. Doing simple SQL-style ETL without writing Spark code? Use AWS Glue.
| EMR on EC2 | EMR Serverless | AWS Glue | |
|---|---|---|---|
| Cluster management | You manage nodes | None | None |
| Startup time per job | Seconds (cluster already running) | 1–3 min provisioning | ~1–2 min |
| Best for | High-frequency jobs, Spot savings, custom tuning | Sporadic or daily batch jobs, no ops overhead | Simple ETL with Glue Data Catalog integration |
| Pricing model | Per-second while cluster runs | Per vCPU-second and GB-second used | Per DPU-hour |
| Spark customisation | Full control | Full control | Limited (managed runtime) |
If you run more than a few Spark jobs per hour, EMR on EC2 with a persistent cluster is usually more cost-effective. The per-job provisioning overhead of EMR Serverless adds up fast at high frequency. For daily or hourly batch jobs, EMR Serverless is simpler and removes cluster lifecycle management entirely.
Prerequisites before you run a job
Before submitting any Spark job on EMR, confirm you have the following in place.
S3 bucket for scripts. Upload your PySpark script here before submitting. Example:
s3://my-scripts/aggregate_orders.py.S3 bucket for data. Input data and output results both live in S3. Use separate prefixes:
raw/for input,aggregated/for output.S3 bucket for logs. Set the log URI when creating the cluster or submitting the Serverless job run. Without it, logs disappear when the cluster terminates.
IAM execution role. EMR on EC2 uses a service role and an EC2 instance profile. EMR Serverless uses an execution role. The role needs
s3:GetObject,s3:PutObject, ands3:ListBucketon your data and log buckets. See IAM roles explained if you are new to role-based permissions on AWS.A running cluster (EMR on EC2 only). You need a cluster ID (
j-XXXXXXXXXXXX) before adding steps. See the EMR overview for cluster creation.
If your job fails with AccessDeniedException on S3, the execution role is missing permissions. The S3 access denied troubleshooting guide lists exactly which permissions to check.
Submit a Spark job to EMR on EC2
The standard way to run Spark on EMR on EC2 is to add a step. Each step runs a spark-submit command pointing to a script on S3. Use cluster mode so the driver runs inside the cluster, not on the machine you submit from.
# Upload the script to S3 first
aws s3 cp aggregate_orders.py s3://my-scripts/aggregate_orders.py
# Submit a PySpark job as an EMR step
aws emr add-steps \
--cluster-id j-XXXXXXXXXXXX \
--steps '[{
"Name": "Daily Order Aggregation",
"ActionOnFailure": "CONTINUE",
"HadoopJarStep": {
"Jar": "command-runner.jar",
"Args": [
"spark-submit",
"--deploy-mode", "cluster",
"--executor-memory", "4g",
"--executor-cores", "2",
"--num-executors", "10",
"--conf", "spark.sql.shuffle.partitions=200",
"s3://my-scripts/aggregate_orders.py",
"--input", "s3://my-data-lake/raw/orders/",
"--output", "s3://my-data-lake/aggregated/orders/",
"--date", "2026-03-18"
]
}
}]'
# Check step status
aws emr list-steps --cluster-id j-XXXXXXXXXXXX
# Get details on a specific step
aws emr describe-step \
--cluster-id j-XXXXXXXXXXXX \
--step-id s-XXXXXXXXXXXXIf your script declares —input, —output, and —date, all three must appear in the step’s Args list. A missing argument causes an argparse error that only shows up in the stderr log in S3. The EMR console just shows FAILED with no explanation.
The ActionOnFailure setting controls what happens if a step fails:
CONTINUE: subsequent steps still runTERMINATE_CLUSTER: shuts the cluster down immediatelyCANCEL_AND_WAIT: skips remaining steps but keeps the cluster alive
In production, prefer CONTINUE or CANCEL_AND_WAIT. A terminated cluster removes your ability to inspect logs on the primary node if you have not configured an S3 log URI.
Sample PySpark job
This script reads raw order data from S3, filters and aggregates it, then writes results back to S3 as Parquet. The —input, —output, and —date arguments match the step definition above exactly.
import argparse
from pyspark.sql import SparkSession
from pyspark.sql.functions import col, sum as spark_sum, count, to_date
def main(input_path: str, output_path: str, date: str):
spark = SparkSession.builder \
.appName("DailyOrderAggregation") \
.getOrCreate()
# EMRFS handles s3:// paths natively — no extra config needed
df = spark.read.json(input_path)
daily = df.filter(to_date(col("placed_at")) == date) \
.filter(col("status") == "completed")
aggregated = daily.groupBy("product_category") \
.agg(
count("order_id").alias("order_count"),
spark_sum("amount").alias("total_revenue")
) \
.orderBy(col("total_revenue").desc())
# Partition by date so downstream Athena queries skip irrelevant partitions
aggregated.write \
.mode("overwrite") \
.parquet(f"{output_path}/date={date}/")
print(f"Wrote {aggregated.count()} rows to {output_path}/date={date}/")
spark.stop()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--input", required=True)
parser.add_argument("--output", required=True)
parser.add_argument("--date", required=True)
args = parser.parse_args()
main(args.input, args.output, args.date)Partitioning output by date (/date=2026-03-18/) is important for downstream query performance. When Athena or a later Spark job reads this data, it skips entire date partitions instead of scanning every file. See partitioned tables for how partition pruning works in practice.
Run Spark with EMR Serverless
EMR Serverless removes the need to create or maintain a cluster. You create one application (a reusable runtime environment), then submit individual job runs to it. AWS provisions compute per job and releases it when the job finishes. You pay only for what each run uses.
# Create a Spark application — do this once, reuse across many job runs
aws emr-serverless create-application \
--name daily-aggregation-app \
--type SPARK \
--release-label emr-7.0.0 \
--initial-capacity '{
"DRIVER": {
"workerCount": 1,
"workerConfiguration": {
"cpu": "2vCPU",
"memory": "4GB"
}
},
"EXECUTOR": {
"workerCount": 5,
"workerConfiguration": {
"cpu": "4vCPU",
"memory": "8GB"
}
}
}'
# Submit a job run — arguments match --input, --output, --date in the script
aws emr-serverless start-job-run \
--application-id 00fabcdefabcdef \
--execution-role-arn arn:aws:iam::123456789012:role/EMRServerlessRole \
--job-driver '{
"sparkSubmit": {
"entryPoint": "s3://my-scripts/aggregate_orders.py",
"entryPointArguments": [
"--input", "s3://my-data-lake/raw/orders/",
"--output", "s3://my-data-lake/aggregated/orders/",
"--date", "2026-03-18"
],
"sparkSubmitParameters": "--conf spark.executor.cores=4 --conf spark.executor.memory=8g"
}
}' \
--configuration-overrides '{
"monitoringConfiguration": {
"s3MonitoringConfiguration": {
"logUri": "s3://my-emr-logs/serverless/"
}
}
}'EMR Serverless provisioning takes 1–3 minutes per job run. If your pipeline submits many short Spark jobs per hour, that startup cost adds up. An EMR on EC2 cluster with pre-warmed capacity responds in seconds and is usually cheaper at that frequency.
Read from S3 and write results to S3 or Redshift
Most Spark jobs on EMR read from and write back to S3. EMRFS handles this natively: your code uses s3:// paths and the connector manages the API calls. The IAM role on your cluster or EMR Serverless execution role must have read and write access to the relevant buckets.
A common next step in production data pipelines is loading processed results from S3 into Amazon Redshift for BI queries. The Redshift Spark connector does this by staging data in S3 and then issuing a COPY command:
# Write a Spark DataFrame to Redshift via the Redshift Spark connector
aggregated.write \
.format("io.github.spark_redshift_utils.spark_redshift") \
.option("url", "jdbc:redshift://my-cluster.us-east-1.redshift.amazonaws.com:5439/mydb") \
.option("dbtable", "analytics.daily_order_summary") \
.option("tempdir", "s3://my-temp-bucket/redshift-temp/") \
.option("aws_iam_role", "arn:aws:iam::123456789012:role/RedshiftS3Access") \
.mode("overwrite") \
.save()Writing through S3 and COPY is orders of magnitude faster than row-by-row JDBC inserts. For all the ways to get data into Redshift, including COPY, Firehose, and Data Sharing, see the loading data into Redshift guide.
Monitor and troubleshoot Spark jobs
When a Spark job fails, treat it like a triage situation: do not start guessing from the outside. Go straight to the error log first, then use the other tools to understand performance once the job is actually running.
Step logs in S3 (check this first)
If you did not set an S3 log URI when creating the cluster, step logs only exist on the primary node while it is running. Once the cluster terminates, those logs are gone. Always set a log URI at cluster creation time.
EMR writes step-level logs to the S3 log URI you configured at cluster creation. Navigate to:
s3://your-log-bucket/{cluster-id}/steps/{step-id}/Open stderr first. This file contains the actual exception and stack trace. The stdout file contains any print() output from your script. For EMR Serverless, logs go to the logUri you set in the s3MonitoringConfiguration block of the job run submission.
Spark UI (use this for performance problems)
Spark UI runs on port 18080 of the primary node and shows job stages, task timelines, executor utilisation, and shuffle metrics. It is the right tool once the job is running but slower than expected. Use it to spot data skew (one task taking 10x longer than others) or excessive shuffle (tasks spilling gigabytes to disk).
# Create an SSH tunnel to access Spark UI at localhost:18080
ssh -i my-key.pem -N -L 18080:primary-node-dns:18080 hadoop@primary-node-dns
# Then open http://localhost:18080 in your browserCloudWatch metrics
EMR sends cluster metrics to CloudWatch automatically. The most useful ones to watch:
YARNMemoryAvailablePercentage: drops when executors are memory-constrainedHDFSUtilization: HDFS space usage on core nodesCoreNodesPending: indicates autoscaling is waiting for capacityStepsFailed: set an alarm on this to get notified when any step fails
See the CloudWatch overview for how to create alarms and configure notification channels for EMR health events.
Key Spark settings that matter first
Think of these four settings as the main controls on a forklift. Get them roughly right and the job runs well. Ignore them and the job either crawls or crashes with an out-of-memory error.
Pass Spark configuration as —conf key=value flags in the sparkSubmitParameters field (EMR Serverless) or in the step’s Args list (EMR on EC2). You can also set defaults at the cluster level using EMR application configurations.
spark.executor.memory: memory per executor. A safe starting point is 75% of the instance’s available RAM, leaving headroom for the OS and Spark overhead. If your job spills to disk, increase this first.spark.executor.cores: CPU cores per executor. 4–5 cores per executor is a common starting point for m5 and r5 instance types. Too many cores per executor increases memory contention between concurrent tasks.spark.sql.shuffle.partitions: partitions created after a join or aggregation. Default is 200. For small datasets that creates unnecessary overhead; for large datasets it causes memory pressure. Aim for 2–4x the total number of executor cores in your cluster.spark.dynamicAllocation.enabled: lets Spark request and release executors as workload changes across stages. Set totruefor jobs where different stages have very different parallelism requirements.
Common beginner mistakes
Missing a required script argument in the step definition. If your script takes
—input,—output, and—date, all three must appear in the step’sArgslist. A missing argument causes anargparseerror that only appears in thestderrlog in S3. The EMR console just shows FAILED with no detail.Using client mode in production. In client mode, the driver runs on the machine you submitted from. If that connection drops, the job dies. Always use
—deploy-mode clusterfor production steps.Not partitioning output. Writing all results to a flat directory means every downstream query scans every file. Partition by date or another filter key from the start. Retrofitting partitioning later requires rewriting all existing data.
Leaving
spark.sql.shuffle.partitionsat 200. For a small dataset with 4 executor cores, 200 shuffle partitions creates large overhead. For a multi-terabyte dataset, it causes out-of-memory errors. Tune this to match your cluster and data size.Not setting a log URI at cluster creation. Step logs only exist on the primary node while the cluster is running. Once it terminates, those logs are gone permanently.
Looking at the EMR console instead of stderr when a job fails. The console shows the step status but not the failure reason. The actual exception is always in the
stderrfile in the S3 log bucket under the step ID.
Summary
- Submit Spark jobs to EMR on EC2 as steps using
aws emr add-stepswithcommand-runner.jarandspark-submit - Every argument your script expects must appear in the step’s
Argslist — mismatches cause silent failures visible only in S3stderr - Use cluster mode in production so the driver runs inside the cluster and survives disconnection
- EMR Serverless removes cluster management: create one application, submit job runs, pay only for execution time
- Spark reads and writes S3 natively on EMR via EMRFS — use standard
s3://paths in your script - When a job fails, open the
stderrfile in your S3 log bucket under the step ID, not the console - Monitor with Spark UI for performance problems and CloudWatch for infrastructure health and failure alerts
Frequently asked questions
What is the difference between EMR on EC2 and EMR Serverless for Spark?
EMR on EC2 gives you a persistent cluster with fixed node sizes. You manage it, pay while it runs, and can run many jobs on it back to back. EMR Serverless has no cluster to manage — you submit a job run and AWS provisions compute automatically, then releases it when the job finishes. EMR Serverless is better for sporadic or scheduled jobs. EMR on EC2 is better when you run many jobs per hour or need fine-grained control over instance types and Spot usage.
How do I submit a Spark job to EMR?
You submit a Spark job to EMR as a "step" using aws emr add-steps. Each step specifies the spark-submit command, the S3 path to your script, and any arguments or Spark configuration flags. EMR queues and runs the step on the cluster. You can also submit steps from the EMR console or the AWS SDK.
How does Spark on EMR read from and write to S3?
EMR pre-installs EMRFS, an S3-compatible Hadoop filesystem. Your Spark code uses standard s3:// paths — for example spark.read.parquet("s3://my-bucket/data/") — and EMRFS handles the underlying API calls, retries, and consistency. No extra setup is needed beyond giving your EMR execution role permission to access the bucket.
Where do I find logs when a Spark job fails on EMR?
Check the S3 log bucket you configured at cluster creation. Navigate to s3://your-log-bucket/{cluster-id}/steps/{step-id}/ and open the stderr file — this contains the Spark exception and stack trace. For EMR Serverless, look in the s3MonitoringConfiguration log URI you set when submitting the job run. The EMR console shows the step status but not the failure reason.
Can I run PySpark on AWS without setting up a cluster?
Yes. Use EMR Serverless. Create a Spark application once with aws emr-serverless create-application, then submit job runs with aws emr-serverless start-job-run. AWS provisions compute per job and charges only for the vCPU-seconds and GB-seconds used during execution. There is no cluster to start, scale, or terminate.