[HN Gopher] Python Workers redux: fast cold starts, packages, an...
___________________________________________________________________
Python Workers redux: fast cold starts, packages, and a uv-first
workflow
Author : dom96
Score : 116 points
Date : 2025-12-08 14:42 UTC (3 days ago)
(HTM) web link (blog.cloudflare.com)
(TXT) w3m dump (blog.cloudflare.com)
| jtbaker wrote:
| ``` BREAKING CHANGE The following packages are removed from the
| Pyodide distribution because of the build issues. We will try to
| fix them in the future: arro3-compute arro3-core arro3-io Cartopy
| duckdb geopandas ... polars pyarrow pygame-ce pyproj zarr ```
|
| https://pyodide.org/en/stable/project/changelog.html#version...
|
| Bummer, looks like a lot of useful geo/data tools got removed
| from the Pyodide distribution recently. Being able to use some of
| these tools in a Worker in combination with R2 would unlock some
| powerful server-side workflows. I hope they can get added back.
| I'd love to adopt CF more widely for some of my projects, and
| seems like support for some of this stuff would make adoption by
| startups easier.
| jeff17robbins wrote:
| The comparison with AWS Lambda seems to ignore the AWS memory
| snapshot option called "SnapStart for Python". I'd be interested
| in seeing the timing comparison extended to include SnapStart.
| killingtime74 wrote:
| "SnapStart for Python" costs extra though. If we are paying
| then you can even have prewarmed Python lambdas with no cold
| start on AWS (Provisioned Concurrency).
| Yacoby wrote:
| Unless I misunderstand, AWS SnapStart and their memory
| snapshots are the same feature (taking memory snapshots to
| speed up cold start). It doesn't seem a fair comparison to
| ignore this and my assumption is because AWS Lambda SnapStart
| is faster.
| dom96 wrote:
| It wasn't an intentional omission, we weren't aware of this
| feature in AWS Lambda. The blog post has been updated to
| reflect that the numbers are for Lambda without SnapStart
| enabled.
|
| Python Workers use snapshots by default and unlike
| SnapStart we don't charge extra for it. For many use cases,
| you can run Python Workers completely for free on our
| platform and benefit from the faster cold starts.
| killingtime74 wrote:
| I think it's fair because AWS charges extra for it.
|
| They are comparing the baseline product of all three
| platforms. Why should we take paid add ons into account for
| 1 platform.
|
| As I mentioned, if you are ok with paying, then you should
| also compare Provisioned concurrency on AWSbas well, which
| has 0 cold start (they keep a prewarmed lambda for you).
|
| Product comparisons are not purely technical in nature. As
| a user, if im paying extra, I would much rather the 0 cold
| start than just a reduced cold start especially with all
| these additional complexities.
| saikiran-a1 wrote:
| nice
| BiteCode_dev wrote:
| Anybody using it for something serious ? I can't see a use case
| beyond I need a quick script running that is not worth setting up
| a vps.
| pedrozieg wrote:
| The most interesting bit here is not the "2.4x faster than
| Lambda" part, it is the constraints they quietly codify to make
| snapshots safe. The post describes how they run your top-level
| Python code once at deploy, snapshot the entire Pyodide heap,
| then effectively forbid PRNG use during that phase and reseed
| after restore. That means a bunch of familiar CPython patterns at
| import time (reading entropy, doing I/O, starting background
| threads, even some "random"-driven config) are now treated as
| bugs and turned into deployment failures rather than "it works on
| my laptop."
|
| In practice, Workers + Pyodide is forcing a much sharper line
| between init-time and request-time state than most Python
| codebases have today. If you lean into that model, you get very
| cheap isolates and global deploys with fast cold starts. If your
| app depends on the broader CPython/C-extension ecosystem behaving
| like a mutable Unix process, you are still in container land for
| now. My hunch is the long-term story here will be less about the
| benchmark numbers and more about how much of "normal" Python can
| be nudged into these snapshot-friendly constraints.
| sandruso wrote:
| I'm betting against wasm and going with containers instead.
|
| I have warm pool of lightweight containers that can be reused
| between runs. And that's the crucial detail that makes or
| breaks it. The good news is that you can lock it down with
| seccomp while still allowing normal execution. This will give
| you 10-30ms starts with pre-compiled python packages inside
| container. Cold start is as fast as spinning new container
| 200-ish ms. If you run this setup close to your data, you can
| get fast access to your files which is huge for data related
| tasks.
|
| But this is not suitable for type of deployment Cloudflare is
| doing. The question is whether you even want that global
| availability because you will trade it for performance. At the
| end of the day, they are trying to reuse their isolates infra
| which is very smart and opens doors to other wasm-based
| deployments.
| resiros wrote:
| Very interesting but the limitation on the libraries you can use
| is very strong.
|
| I wonder if they plan to invest seriously into this?
| wg0 wrote:
| If anyone from cloudflare comes here - it's not possible to
| create D1 databases on the fly and interact them because
| databases must be mentioned in the worker bindings.
|
| This hampers the per user databases workflow.
|
| Would be awesome if a fix lands.
| ashwindharne wrote:
| I'm always a little hesitant to use D1 due to some of these
| constraints. I know I may not ever hit 10GB for some of my side
| projects so I just neglect sharding, but also it unsettles me
| that it's a hard cap.
| dom96 wrote:
| (I work at Cloudflare, but not on D1)
|
| I believe this is possible, you can create D1 databases[1]
| using Cloudflare's APIs and then deploy a worker using the API
| as well[2].
|
| 1 -
| https://developers.cloudflare.com/api/resources/d1/subresour...
|
| 2 -
| https://developers.cloudflare.com/api/resources/workers/subr...
| wg0 wrote:
| Thank you! That's great and it is possible but... With some
| limitations.
|
| The idea is from sign up form to a D1 Database that can be
| accessed from the worker itself.
|
| That's not possible without updating worker bindings like you
| showed and further - there is an upper limit of 5000 bindings
| per worker and just 5000 users then becomes the upper limit
| although D1 allows 50,000 databases easily with further
| possible by requesting a limit increase.
|
| edit: Missed opening.
| ewuhic wrote:
| Hey, would you happen to know if/when D1 can get support for
| ICU (https://sqlite.org/src/dir/ext/icu) and transactions?
| kentonv wrote:
| Transactions are supported in Durable Objects. In fact,
| with DO you are interacting with the SQLite database
| locally and synchronously, so transactions are essentially
| free with no possibility of conflicts and no worry about
| blocking other queries.
|
| Extensions are easy to enable, file a bug on
| https://github.com/cloudflare/workerd . (Though this one
| might be trickier than most as we might have to do some
| build engineering.)
| ewuhic wrote:
| Re DO - I am definitely not rewriting my web wasm rust-
| sqlx app to use DO.
|
| Re filing an issue - sounds straightforward, will do!
| educhana wrote:
| Why not durable objects? I think it's the recommended pattern
| for having a db per user
| kentonv wrote:
| Try Durable Objects. D1 is actually just a thin layer over
| Durable Objects. In the past D1 provided a lot of DX benefits
| like better observability, but those are increasingly being
| merged back into DO directly.
|
| What is a Durable Object? It's just a Worker that has a name,
| so you can route messages specifically to it from other
| Workers. Each one also has its own SQLite database attached. In
| fact, the SQLite database is local, so you can query it
| synchronously (no awaits), which makes a lot of stuff faster
| and easier. You can easily create millions of Durable Objects.
|
| (I am the lead engineer for Workers.)
| silverwind wrote:
| I wish they would contribute stuff like this memory snappshotting
| to CPython.
| jitl wrote:
| It relies entirely on the WebAssembly runtime, see the
| discussion of how ASLR problems don't occur with WASM. Doing
| this with WASM is pretty easy, doing it with system memory is
| quite tricky.
| scottydelta wrote:
| It's 2025 and choosing a region for your resources is still an
| enterprise feature on cloudflare.
|
| In contrast, AWS provides this as the base thing, you choose
| where your services run. In a world where you can't do anything
| without 100s of compliance and a lot of compliances require
| geolocation based access control or data retention, this is
| absurd.
| NicoJuicy wrote:
| That's basically not how Cloudflare works.
|
| Your app works distributed/globally on the go.
|
| Additionally, every Enterprise feature will become available in
| time ( discussed during their previous quarter earnings). It
| will be bound to regions ( eg. Eu)
| baq wrote:
| it's only absurd if you don't want to pay cloudflare money
| cloudflare728 wrote:
| I hope Cloudflare improve Next.js support on Workers.
|
| Currently pagespeed.web.dev score drops by around 20 than self
| hosted version. One of the best features of Next.js, Image
| optimization doesn't have out of the box support. You need
| separate image optimization service that also did not work for me
| for local images (images in the bundle).
| randomtoast wrote:
| One of my biggest points of criticism of Python is its slow cold
| start time. I especially notice this when I use it as a scripting
| language for CLIs. The startup time of a simple .py script can
| easily be in the 100 to 300 ms range, whereas a C, Rust, or Go
| program with the same functionality can start in under 10 ms.
| This becomes even more frustrating when piping several scripts
| together, because the accumulated startup latency adds up
| quickly.
| nickjj wrote:
| > The startup time of a simple .py script can easily be in the
| 100 to 300 ms range
|
| I can't say I've ever experienced this. Are you sure it's not
| related to other things in the script?
|
| I wrote a single file Python script, it's a few thousand lines
| long. It can process a 10,000 line CSV file and do a lot of
| calculations to the point where I wrote an entire CLI income /
| expense tracker with it[0].
|
| The end to end time of the command takes 100ms to process those
| 10k lines, that's using `time` to measure it. That's on
| hardware from 2014 using Python 3.13 too. It takes ~550ms to
| fully process 100k lines as well. I spent zero time optimizing
| the script but did try to avoid common pitfalls (drastically
| nested loops, etc.).
|
| [0]: https://github.com/nickjj/plutus
| tlyleung wrote:
| Just a guess - but perhaps the startup time is before `time`
| is even imported?
| williadc wrote:
| `time` is a shell command that you can use to invoke other
| commands and track their runtime.
| zahlman wrote:
| > I can't say I've ever experienced this. Are you sure it's
| not related to other things in the script? I wrote a single
| file Python script, it's a few thousand lines long.
|
| It's because of module imports, primarily and generally. It's
| worse with many small files than a few large ones (Python 3
| adds a little additional overhead because of needing extra
| system calls and complexity in the import process, to handle
| `__pycache__` folders. A great way to demonstrate it is to
| ask pip to do something trivial (like `pip --version`, or
| `pip install` with no packages specified), or compare the
| performance of pip installed in a venv to pip used cross-
| environment (with `--python`). Pip imports literally hundreds
| of modules at startup, and hundreds more the first time it
| hits the network.
| fwip wrote:
| And it's worse if your python libraries might be on network
| storage - like in a user's homedir in a shared compute
| environment.
| dekhn wrote:
| Exactly this. The time to start python is roughly a
| function of timeof(stat) * numberof(stat calls) and on a
| network system that can often be magnitudes larger than a
| local filesystem.
| zahlman wrote:
| I do wonder, on a local filesystem, how much of the time
| is statting paths vs. reading the file contents vs.
| unmarshaling code objects. (Top-level code also runs when
| a module is imported, but the cost of that is of course
| highly module-dependent.)
| dekhn wrote:
| Maybe you could take the stat timings, the read timings
| (both from strace) and somehow instrument Python to
| output timing for unmarshalling code (or just instrument
| everything in python).
|
| Either way, at least on my system with cached file
| attributes, python can startup in 10ms, so it's not clear
| whether you truly need to optimize much more than that
| (by identifying remaining bits to optimize), versus
| solving the problem another way (not statting 500 files,
| most of which don't exist, every time you start up).
| nickjj wrote:
| Makes sense, most of my scripts are standalone zero
| dependency scripts that import a few things from the
| standard library.
|
| `time pip3 --version` takes 230ms on my machine.
| maccard wrote:
| That proves the point, right?
|
| `time pip3 --version` takes ~200ms on my machine. `time
| go help` takes 25, and prints out 30x more lines than
| pip3 --version.
| nickjj wrote:
| Yep, running time on my tool's --version takes 50ms and
| funny enough processing 10k CSV lines with ~2k lines of
| Python code takes 100ms, so 50ms of that is just Python
| preparing things to run by importing 20 or so standard
| library modules.
| randomtoast wrote:
| Here is a benchmark https://github.com/bdrung/startup-time
|
| This benchmark is a little bit outdated but the problem
| remains the same.
|
| Interpreter initialization: Python builds and initializes its
| entire virtual machine and built-in object structures at
| startup. Native programs already have their machine code
| ready and need very little runtime scaffolding.
|
| Dynamic import system: Python's module import machinery
| dynamically locates, loads, parses, compiles, and executes
| modules at runtime. A compiled binary has already linked its
| dependencies.
|
| Heavy standard library usage: Many Python programs import
| large parts of the standard library or third-party packages
| at startup, each of which runs top-level initialization code.
|
| This is especially noticeable if you do not run on an M1
| Ultra, but on some slower hardware. From the results on
| Rasperberry PI 3:
|
| C: 2.19 ms
|
| Go: 4.10 ms
|
| Python3: 197.79 ms
|
| This is about 200ms startup latency for a print("Hello
| World!") in Python3.
| zahlman wrote:
| Interesting. The tests use Python 3.6, which on my system
| replicates the huge difference shown in startup time using
| and not using `-S`. From 3.7 onwards, it makes a much
| smaller percentage change. There's also a noticeable
| difference the first time; I guess because of Linux caching
| various things. (That effect is much bigger with Rust
| executables, such as uv, in my testing.)
|
| Anyway, your analysis of causes reads like something AI
| generated and pasted in. It's awkward in the context of the
| rest of your post, and 2 of the 3 points are clearly
| irrelevant to a "hello world" benchmark.
| maccard wrote:
| A python file with import requests
|
| Takes 250ms on my i9 on python 3.13
|
| A go program with package main
| import ( _ "net/http" ) func
| main() { }
|
| takes < 10ms.
| dotdi wrote:
| This is not an apples-to-apples comparison. Python needs to
| load and interpret the whole requests module when you run
| the above program. The golang linker does dead code
| elimination, so it probably doesn't run anything and
| doesn't actually do the import when you launch it.
| maccard wrote:
| Sure it's not an apples to apples comparison - python is
| interpreted and go is statically compiled. But that
| doesn't change the fact that in practice running a
| "simple" python program/script can take longer to startup
| than go can to run your entire program.
| dotdi wrote:
| Still, you are comparing a non-empty program to an empty
| program.
| tuhgdetzhh wrote:
| Even if you actually use the network module in Go, just
| so that the compiler wouldn't strip it away, you would
| still have a startup latency in Go way below 25 ms from
| my experience with writing CLI tools.
|
| Whereas with Python, even in the latest version, you're
| already looking at atleast 10x the amount of startup
| latency in practice.
|
| Note: This is excluding the actual time that is made for
| the network call, which can of course also add quiete
| some milliseconds, depending on how far on planet earth
| your destination is.
| maccard wrote:
| You're missing the point. The point is that python is
| slow to start up _because_ it's not the same.
|
| Compare: import requests
| print(requests.get("http://localhost:3000").text)
|
| to package main import (
| "fmt" "io" "net/http" )
| func main() { resp, _ :=
| http.Get("http://localhost:3000") defer
| resp.Body.Close() body, _ :=
| io.ReadAll(resp.Body)
| fmt.Println(string(body)) }
|
| I get: python3: 0.08s user 0.02s
| system 91% cpu 0.113 total go 0.00s user 0.01s
| system 72% cpu 0.015 total
|
| (different hardware as I'm at home).
|
| I wrote another that counts the lines in a file, and
| tested it against
| https://www.gutenberg.org/cache/epub/2600/pg2600.txt
|
| I get: python 0.03s user 0.01s system
| 83% cpu 0.059 total go 0.00s user 0.00s system
| 80% cpu 0.010 total
|
| These are toy programs, but IME that these gaps stay as
| your programs get bigger
| dekhn wrote:
| It's not interpreting- Python is loading the already byte
| compiled version. But it's also statting several files
| (various extensions).
|
| I believe in the past people have looked at putting the
| standard library in a zip file instead of splatted out
| into a bunch of files in a dirtree. In that case, I think
| python would just do a few stats, find the zipfile,
| loaded the whole thing into RAM, and then index into the
| file.
| maccard wrote:
| > In that case, I think python would just do a few stats,
| find the zipfile, loaded the whole thing into RAM, and
| then index into the file.
|
| "If python was implemented totally different it might be
| fast" - sure, but it's not!
| dekhn wrote:
| No, this feature already exists.
| maccard wrote:
| Great - how do I use it?
| baq wrote:
| it depends somewhat on what you import, too. some people would
| sell their grandmothers to get below 1s when you start
| importing numpys and scikits.
| zbentley wrote:
| The upcoming lazy import system may help with startup
| time...but if the underlying issue wasn't "Python startup is
| slow" but rather "a specific program imports modules that
| take a long time to low", it'll only shift the time
| consumption to runtime.
| TudorAndrei wrote:
| Are you comparing the startup time of an interpreted language
| with the startup time of a compiled language? or you mean that
| `time python hello.py` > `( time gcc -O2 -o hello hello.c ) &&
| ( time ./hello )` ?
| randomtoast wrote:
| I'm referring to the startup time as benchmarked in the
| following manner: https://github.com/bdrung/startup-time
| maccard wrote:
| Here's the thing - I don't really care if its' because the
| interpreter has to start up, or there's a remote http call,
| or we scan the disks for integrity - the end user experience
| on every run is slower.
| smartmic wrote:
| Yes, that is also my feeling. But comparing an interpreted
| language with a compiled one is not really fair.
|
| Here is my quick benchmark. I refrain from using Python for
| most scripting/prototyping task but really like Janet [0] -
| here is a comparison for printing the current time in Unix
| epoch: $ hyperfine --shell=none --warmup 2
| "python3 -c 'import time;print(time.time())'" "janet -e '(print
| (os/time))'" Benchmark 1: python3 -c 'import
| time;print(time.time())' Time (mean +- s): 22.3 ms
| +- 0.9 ms [User: 12.1 ms, System: 4.2 ms] Range (min
| ... max): 20.8 ms ... 25.6 ms 126 runs
| Benchmark 2: janet -e '(print (os/time))' Time (mean +-
| s): 3.9 ms +- 0.2 ms [User: 1.2 ms, System: 0.5 ms]
| Range (min ... max): 3.6 ms ... 5.1 ms 699 runs
| Summary 'janet -e '(print (os/time))'' ran 5.75
| +- 0.39 times faster than 'python3 -c 'import
| time;print(time.time())''
|
| [0]: https://janet-lang.org/
| curiousgal wrote:
| Well python is also compiled technically.
| syrusakbary wrote:
| Completely agree on this.
|
| Regarding cold-starts, I strongly believe V8 snapshots are
| perhaps not the best way to achieve fast cold starts with
| Python (they may be if you are tied to using V8, though!), and
| will have wide side effects if you go out of the standards
| packages included on the Pyodide bundle.
|
| To put some perspective: V8 snapshots are storing the whole
| state of an application (including it's compiled modules). This
| means that for a Python package that is using Python (one wasm
| module) + Pydantic-core (one wasm module) + FastAPI... all of
| those will be included in one snapshot (as well as the
| application state). This makes sense for browsers, where you
| want to be able to inspect/recover everything at once.
|
| The issue about this design is that the compiled artifacts and
| the application state are bundled into one piece artifact (this
| is not great for AOT designed runtimes, but might be the
| optimal design for JITs though).
|
| Ideally, you would separate each of the compiled modules from
| the state of the application. When you do this, you have some
| advantages: you can deserialize the compiled modules in
| parallel, and untie the "deserialization" from recovering the
| state of the application. This design doesn't adapt that well
| into the V8 architecture (and how it compiles stuff) when
| JavaScript is the main driver of the execution, however it's
| ideal when you just use WebAssembly.
|
| This is what we have done at Wasmer, which allows for much
| faster cold starts than 1 second. Because we cache each of the
| compiled modules separately, and recover the state of the
| application later, we can achieve cold-starts that are a
| magnitude faster than Cloudflare's state of the art (when using
| pydantic, fastapi and httpx).
|
| If anyone is curious, here is a blogpost where we presented
| fast-cold starts for the application state (note that the
| deserialization technique for Wasm modules is applied
| automatically in Wasmer, and we don't showcase it on the
| blogpost): https://wasmer.io/posts/announcing-instaboot-
| instant-cold-st...
|
| Note aside: congrats to the Cloudflare team on their work on
| Python on Workers, it's inspiring to all providers on the
| space... keep it up and let's keep challenging the status quo!
| dilawar wrote:
| Reminds me of mercurial cvs!!
| yegle wrote:
| Yes it's bad enough that there's a chg to (barely) improve
| the command laten y.
|
| (Side note this is why jj is awesome. A `jj log` is almost as
| fast as `ls`).
| mixmastamyk wrote:
| Big packages shouldn't be imported until the cli has been
| parsed, and handed off to main. There's been work to do this
| automatically, but it's good hygiene to avoid it anyway.
|
| A modern machine shouldn't take this long, so likely something
| big is being imported unnecessarily at startup. If the big
| package itself is the issue, file it on their tracker.
| dekhn wrote:
| Run strace on Python starting up- you will see it statting
| hundreds if not thousands of files. That gets much worse the
| slower your filesystem is.
|
| On my linux system where all the file attributes are cached, it
| takes about 12ms to completely start, run a pass statement, and
| exit.
| rcarmo wrote:
| You can run .pyc stuff "directly" with some creativity, and
| there are some tools to pack "executables" that are just
| chunked blobs of bytecode.
| rcarmo wrote:
| Pyodide is a great enabler for this kind of thing, but most of
| the libraries I want to use tend to be native or just weird.
| Still, I wonder how fast things like Pillow, Pandas and the like
| are these days---benchmarks would be nice.
| ianberdin wrote:
| I still don't get, what is the use case for cloudflare workers or
| lambda?
|
| I used both for years. Nothing beats VPS/bare metal. Alright,
| they give lower latency, and maybe cheaper and big nightmare for
| managing at the same time. Hello to micro services architecture.
| maccard wrote:
| Scale down to actual 0, really easy edge distribution, stupidly
| simple to deploy to. That's really it.
| orliesaurus wrote:
| Checked out the Cloudflare post... they now support Pyodide-
| compatible packages through uv... so you can pull in whatever
| Python libs you need, not just a curated list.
|
| ALSO the benchmarks show about a one second cold start when
| importing httpx, fastapi and pydantic... that's faster than
| Lambda and Cloud Run, thanks to memory snapshots and isolate-
| based infra.
|
| BUT the default global deployment model raises questions about
| compliance when you need specific regions... and I'd love to know
| how well packages with native extensions are supported.
___________________________________________________________________
(page generated 2025-12-11 23:01 UTC)