Spark executor memory determines the JVM heap available to tasks after a cluster manager starts each executor. Sizing it per application helps joins, aggregations, caching, and Python workers run with enough room while keeping cluster capacity from being reserved by jobs that do not need it.
The main setting is spark.executor.memory, the heap for each executor JVM. Non-heap overhead stays separate through spark.executor.memoryOverhead, and PySpark jobs can also use spark.executor.pyspark.memory when Python worker memory needs its own limit.
Memory values must be in place before the SparkContext is created, usually through spark-submit or spark-defaults.conf. Command-line configuration overrides shared defaults, and the running application should show the active values in Spark logs, Spark configuration, or the cluster UI.
Related: How to submit an Apache Spark job
Related: How to configure Spark defaults
Related: How to troubleshoot Spark out-of-memory errors
Steps to configure Spark executor memory:
- Choose the per-executor memory values for the application.
Executor heap: 768m Memory overhead: 384m PySpark worker memory: 256m Local proof workers: 2 workers, 1 core each, 2048 MiB each
spark.executor.memory sets executor JVM heap. Spark calculates spark.executor.memoryOverhead from an overhead factor when it is not set directly, with a 384 MiB minimum in current Spark 4 releases. Kubernetes non-JVM jobs use a higher default overhead factor than JVM jobs. Set spark.executor.pyspark.memory only when PySpark worker memory should have a separate limit.
- Save a temporary Spark application that prints the active memory settings.
$ vi executor_memory_check.py
- Add the memory check code.
- executor_memory_check.py
from pyspark.sql import SparkSession spark = SparkSession.builder.appName("sg-executor-memory-check").getOrCreate() spark.sparkContext.setLogLevel("ERROR") conf = spark.sparkContext.getConf() for key in ( "spark.executor.memory", "spark.executor.memoryOverhead", "spark.executor.pyspark.memory", ): print(f"{key} = {conf.get(key, '<not set>')}") print(f"defaultParallelism = {spark.sparkContext.defaultParallelism}") print(f"row_count = {spark.range(0, 1000, 1, 2).count()}") spark.stop()
The script reads the configuration after SparkContext starts and runs a small DataFrame action so executor scheduling is exercised.
- Submit the application with executor memory settings.
$ spark-submit \ --master 'local-cluster[2,1,2048]' \ --conf spark.executor.memory=768m \ --conf spark.executor.memoryOverhead=384m \ --conf spark.executor.pyspark.memory=256m \ executor_memory_check.py INFO ResourceProfile: Default ResourceProfile created, executor resources: Map(memoryOverhead -> name: memoryOverhead, amount: 384, memory -> name: memory, amount: 768, pyspark.memory -> name: pyspark.memory, amount: 256, offHeap -> name: offHeap, amount: 0) ##### snipped ##### INFO StandaloneSchedulerBackend: Granted executor ID app-20260706212802-0000/0 with 1 core(s), 768.0 MiB RAM ##### snipped ##### spark.executor.memory = 768m spark.executor.memoryOverhead = 384m spark.executor.pyspark.memory = 256m defaultParallelism = 2 row_count = 1000
local-cluster[2,1,2048] is a disposable local proof target. For production, replace it with the real --master value such as yarn, a spark:// master URL, or a k8s:// API URL, and use workload-sized memory values.
- Confirm the Spark logs and application output show the intended values.
The ResourceProfile line shows Spark built the executor resource profile with 768 MiB heap, 384 MiB overhead, and 256 MiB PySpark memory. The Granted executor line shows a worker accepted an executor with 768 MiB RAM, and row_count = 1000 confirms the application started and ran a Spark action.
- Remove the temporary memory check application.
$ rm executor_memory_check.py
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.