Abhishek.
All posts
3 min read

JAVA_GATEWAY_EXITED: one error, two root causes

sparkpysparkwindowsdebugging

If you run PySpark on Windows for long enough, you will meet this:

PySparkRuntimeError: [JAVA_GATEWAY_EXITED] Java gateway process exited
before sending its port number

It is one of the least helpful errors I know. It means the driver JVM died before it could hand Python its gateway port, and that is all it means. It never tells you why the JVM died. In a single week I hit it twice, for two causes that had nothing to do with each other.

What the error actually says

PySpark starts the Spark driver as a child JVM process, then talks to it over a Py4J gateway. If that JVM exits before publishing its port, Py4J gives up and raises JAVA_GATEWAY_EXITED. So read it literally: "the JVM did not come up." The real cause is always somewhere upstream of Spark itself.

Cause 1: the VS Code kernel had no JAVA_HOME

The exact same builder code worked every time from a plain CLI python, and failed every time inside the VS Code notebook. That contrast was the whole clue.

The VS Code kernel is spawned by the editor, and its environment does not inherit my shell profile. No JAVA_HOME, no java on PATH. Spark's launcher could not find Java, so the driver JVM never started. CLI runs worked only because they picked up JAVA_HOME from my user profile.

The fix is to force a known-good environment in the notebook before importing or using pyspark:

import os, pyspark
os.environ["JAVA_HOME"]  = r"C:\Program Files\Java\jdk-17"
os.environ["SPARK_HOME"] = os.path.dirname(pyspark.__file__)
os.environ["PATH"] = r"C:\Program Files\Java\jdk-17\bin;" + os.environ.get("PATH", "")

Lesson: do not assume a GUI-launched kernel inherits your shell environment. Set JAVA_HOME and SPARK_HOME explicitly. And when you want to reproduce an environment-specific failure, truly unset the variable (env -u JAVA_HOME) rather than just overriding PATH, or you will leave other inherited values in place and chase a ghost.

Cause 2: a SAS token with an ampersand broke spark-submit.cmd

Java was sorted, and it still failed. This time java -version and spark-submit --version both returned cleanly, so the environment was genuinely fine. The JVM was dying for a different reason.

The culprit was how I passed my ADLS SAS token. I had set it as a builder .config() value. On Windows, PySpark forwards every .config() entry to spark-submit.cmd as --conf key=value, and spark-submit.cmd is a batch file running under cmd.exe. A SAS token looks like se=...&sp=rl&sig=..., and in cmd.exe the ampersand is a command separator. The launch command got split apart, spark-submit received garbage, and the JVM never started.

I reproduced it exactly: a config value of se=2026&sp=rl&sig=ab%2Bcd produced 'sp' is not recognized as an internal or external command, followed by JAVA_GATEWAY_EXITED.

The fix is to keep the token off the command line entirely. Create the session first, then set the fs.azure.* values directly on the Hadoop configuration:

spark = SparkSession.builder.config("spark.jars.packages", "...").getOrCreate()
hconf = spark._jsc.hadoopConfiguration()
hconf.set(f"fs.azure.account.auth.type.{ACCOUNT}.dfs.core.windows.net", "SAS")
hconf.set(f"fs.azure.sas.fixed.token.{ACCOUNT}.dfs.core.windows.net", SAS_TOKEN)

Lesson: on Windows, builder configs travel through cmd.exe, so any value holding a shell-special character (& | < > ^ %) can corrupt the launch and surface as the same generic gateway error. Keep secrets, tokens, and URLs out of .config(). Set them on spark._jsc.hadoopConfiguration() after the session is already running.

The takeaway

When you see JAVA_GATEWAY_EXITED, stop treating it as a Spark problem. Treat it as "the driver JVM did not launch," and work backwards. Check the environment first (JAVA_HOME above all), then check whether any config value you are passing contains characters the shell will mangle. The message is identical in both cases, but the causes could not be further apart.