Skip to content

Build Not yet confirmed elsewhere1 publisher 3 min read Published

A Spark benchmark's Arrow UDF row toggled a setting that never touches UDFs

A Spark benchmark's "Arrow" UDF row ran the pickle path, so its 48.9x against 48.5x tie says nothing about Arrow transport. The same benchmark's useArrow=True UDFs ran about 2x faster than the plain ones.

The Engineer · Build desk

How we use AI Send a correction

Illustration accompanying A Spark benchmark's Arrow UDF row toggled a setting that never touches UDFs
Generated illustration
Plain and 'Arrow on' @udf rows cost near the same The benchmark's plain @udf and the same @udf 'with Arrow turned on', each measured against the built-in function at 50M and 100M rows.

Comparison of cost against the equivalent built-in function. At 50M rows, plain @udf ran at 48.5x and the @udf with Arrow turned on at 48.9x. At 100M rows, plain @udf ran at 69.2x and the Arrow-on @udf at 69.0x.

Plain and 'Arrow on' @udf rows cost near the same (Cost as a multiple of the equivalent built-in function, per post author)
ItemValueClaim
Plain @udf, 50M rows48.5 x built-in cost2
@udf, Arrow on, 50M rows48.9 x built-in cost2
Plain @udf, 100M rows69.2 x built-in cost2
@udf, Arrow on, 100M rows69 x built-in cost2

What happened

  • The benchmark's Arrow case set spark.sql.execution.arrow.pyspark.enabled, which governs only DataFrame.toPandas() and createDataFrame(pandas_df).
  • At 100M rows the same pair came out at 69.2x and 69.0x of the built-in function's time.
  • The author puts a ceiling of roughly 8 to 10 million rows per second on row-at-a-time Python UDFs, near the rate CPython can be entered and exited.

Why it matters

  • exposure A job or benchmark that sets only arrow.pyspark.enabled is still running pickled Python UDFs, so an Arrow comparison built on it compares one path with itself.
  • contradiction The write-up said Arrow transport buys nothing for per-row UDFs, yet the benchmark's own useArrow=True rows show roughly a 2x gain.
  • cost The 2x costs a config change. The next step, about 3x on arithmetic, means rewriting the function to take a pandas Series or Arrow array.

The failure is a naming problem. Every Python UDF call makes three separate choices: how data crosses from the JVM to the Python worker, which physical operator Spark plans (BatchEvalPython or ArrowEvalPython), and what the function receives once the data lands [8]. One config key sounds like it covers all three. According to the post's author, that key never touches Python UDFs [4], and the PySpark documentation says it controls pandas conversions [6].

That makes the original table an A/A test. The two rows were the same pickled UDF measured twice [7]. The gap was 0.8% at 50M rows and 0.3% at 100M [14]. The author's own phrase for it: "I had built an A/A test by accident" [13]. The post documents the author's write-up leaning on this pair as its key insight [3].

With the right switch, the benchmark's own numbers move. useArrow=True took arithmetic from 5.20s to 2.23s and the CDF workload from 6.18s to 2.94s [9]. That is 2.3x and 2.1x [15]. The original line, that Arrow buys nothing when the function runs one row at a time [3], is wrong as written.

The author's replacement thesis is that a UDF's cost depends far more on what the function receives than on whether Arrow carries the bytes [12]. The data fits that ordering. Vectorized UDFs cross into Python once per batch, and on arithmetic they ran about 3x faster again than useArrow=True [11]. Transport is worth about 2x. Execution model is worth roughly another 3x.

One figure in the post does not sit well with its own table. The author gives a ceiling of roughly 8 to 10 million rows per second for row-at-a-time UDFs, citing 50M rows in 5.2s and 100M in 10.1s [10]. Those work out to 9.6 and 9.9 million rows per second [16]. That fits the pickle UDF. If the 2.23s Arrow time comes from the same 50M-row run as the 5.20s baseline, useArrow=True ran at about 22 million rows per second [17]. Until the post gives the scale of the 2.23s figure, we would not quote the ceiling for Arrow-backed per-row UDFs.

Transfer is the other limit. These are one author's runs of x * 2 + 1, a string transform and a normal CDF [1]. The excerpt does not include the Spark version or the hardware. The 2x is serialization cost saved per row. In our view it carries to your job only if the function body is cheap enough that transport is a large share of the runtime. A UDF that spends its time in a model call will see less.

The check is short. Search job configs and benchmark harnesses for spark.sql.execution.arrow.pyspark.enabled. The settings that move a regular UDF are spark.sql.execution.pythonUDF.arrow.enabled for the session and useArrow=True per UDF [5]. Then look at the physical plan, since the two transports plan different operators [8].

What to watch

  • Whether the author publishes the Spark version and hardware behind the runs.
  • Whether the 2.23s useArrow=True figure comes from the 50M-row table, which decides if it breaks the stated 8 to 10 million rows per second ceiling.
  • Whether reruns on UDFs with heavier function bodies show the same 2x from useArrow=True.

Clarity's read

What the record supports and how the coverage leans. The claims behind it follow.

Reality

Evidence58
Adoption
Insufficient
Hype gap+8
Incentives
Insufficient
Confidence62
Why these scores

Claim ledger

Ranked by verification strength, evidence, and original report placement.

  1. [1]

    The benchmark compared seven options across three workloads (trivial arithmetic, a string transform, and a normal CDF) at scales from 100K to 100M rows.

    Reported Supported Source: Post author, dev.toView cited source
  2. [2]

    A plain @udf ran at 48.5x the cost of the equivalent built-in function at 50M rows, and the same @udf 'with Arrow turned on' ran at 48.9x; at 100M rows the pair was 69.2x against 69.0x.

    Reported Supported Source: Post author, dev.toView cited source
  3. [3]

    The author's write-up called the pair the key insight: switching transport from pickle to Arrow buys nothing when the function still executes one row at a time.

    Reported Supported Source: Post author, dev.toView cited source

Get this ledger inside ChatGPT. Ask about any of our stories and the answer brings its figure, each claim’s verdict and the sources. Free, no account.

Add to ChatGPTAdd to Claude

Sources

1 independent publisher whose own reporting we read for this story.

  1. dev.to

    1 article · October 13, 2026

    Stop Blaming Serialization for Slow Python UDFs

Share your take

Let Clarity write the post for you.

Signed-in readers get a short post drafted on this story in the register they choose — narrative, analytical, or a direct position — editable to the last word before it goes anywhere. The share buttons at the top of this story work without an account.

Topics and entities

Follow any of these and your For You feed starts watching them — no settings page required.

Topics

Loading related stories