Free-Threaded Python in Timeplus: Scaling Across Cores
- Timeplus Team

- 20 hours ago
- 3 min read
Timeplus 3.3.1 embeds free-threaded CPython 3.14, allowing UDFs, external streams, and table functions to run concurrently instead of queueing behind a single interpreter lock. In our benchmark, eight concurrent Python UDF queries completed in 1.18 seconds, down from 10.09 seconds.
It’s the same query, same host, and same Python function, with more clients running it at once:

With Timeplus 3.2.14, going from one client to four costs you 3.7 extra seconds. With Timeplus 3.3.1, it costs 69 milliseconds. That flat line is the release: the old runtime turned every additional Python query into another entry in a queue, and the new one lets them run at the same time on different cores.
More Than Faster UDFs
That lock covered all Python in the server, not just UDFs. Scalar UDFs, aggregate UDFs, Python external stream sources and sinks, and Python table functions all enter the same embedded runtime — so they queued behind each other, not only behind themselves. A Python external stream ingesting in the background was contending with a Python UDF in an unrelated query.
You would have seen it as materialized views slowing each other down, or a scoring UDF that benchmarks fine on its own and degrades the moment real traffic arrives. CPU-bound per-row Python gains the most: feature engineering, custom parsing, geo and string transforms, scoring, model inference.
Under the Hood: Free-Threaded CPython 3.14
Timeplus 3.3.1 embeds CPython 3.14 built with the free-threaded ABI:
Under a GIL build, any query thread entering Python must acquire one process-wide lock before it can run bytecode — many DBMS threads, one executing Python at a time. Under the free-threaded build, entering Python attaches a thread state without that global serialization point.

You don't have to take the wall-clock numbers on faith. Timeplus exposes PythonGILWaitMicroseconds, and across the 8-client batch it falls from 1.04 seconds of pure lock wait to 17 microseconds — with both versions entering Python the same number of times, returning the same checksum. Same work, no longer standing in line.
A Benchmark Built to Isolate Python Execution
A deliberately CPU-bound scalar UDF — 600 pure-Python loop iterations per row, no IO and no packages — so the measurement lands on Python execution rather than data conversion or transfer:
Two shapes, three runs each, medians reported:
Pinning the first to max_threads = 1 means its parallelism can only come from separate sessions. The second uses numbers_mt — the multithreaded generator — because plain numbers() emits a single stream, leaving max_threads nothing to split.
One Query, Many Threads
Most users don't run eight concurrent clients. They run one query over a lot of data and let max_threads do the work — and the lock applied there too.

On 3.2.14, max_threads is inert for Python: the query holds a single core and takes ~82 seconds whether you give it one thread or sixteen. It actually gets slightly worse, because the extra threads add contention without adding throughput. On 3.3.1 the same query rides 16 cores and finishes in 7.24s — 12x faster at the same setting.
Before You Upgrade
Free-threaded CPython is a different ABI. C-extension packages need a cp314t wheel; a cp312 or cp314 (GIL) wheel will not load. Pure-Python packages are unaffected, and your Python source does not change.
The major numeric and ML stack already ships cp314t wheels, and our smoke suites exercise them on the free-threaded build: NumPy, pandas, PyArrow, SciPy, scikit-learn, PyTorch, Prophet, stumpy, openai.
Not yet published upstream, as of this release: statsmodels, river, PyCaret, qlib, neuralforecast, statsforecast, and the zstandard codecs. These are packaging gaps that resolve as maintainers ship cp314t builds.
Two behavior notes. An ABI mismatch fails with an explicit message naming the expected interpreter rather than segfaulting at import. And cancelling a Python external stream or table function is now cooperative — checked at batch boundaries instead of interrupting mid-call — so Python that blocks a long time inside one call stops at the next boundary. UDFs are unaffected.
Upgrade to Timeplus 3.3.1 to get the free-threaded runtime. If any of your Python — UDFs, external streams, or table functions — depends on C-extension packages, check for cp314t wheels before you roll forward.
Try Timeplus, free for 30 days: https://www.timeplus.com/download

