返回资讯中心
外部精选
软件工程
#开发者实时流

Scaling self-hosted Convex: compilation of TypeScript Convex functions into WASM

View more blogs with the tag self hosted, View more blogs with the tag internals, View more blogs with the tag operations, View more blogs with the tag wasm

Hacker News 最新leventov2026/10/26 · 今天30 分钟阅读

以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。

View more blogs with the tag self hosted, View more blogs with the tag internals, View more blogs with the tag operations, View more blogs with the tag wasm

Table of Contents#Overview

#Why compiling TypeScript Convex functions into Wasm

#The design of convex-wasm-compiler and convex-backend Wasm runtime

#Wasm vs. V8: performance results

#Operational considerations of Wasm AOT compilation

Overview

I’ve developed convex-wasm-compiler that compiles Convex queries and mutations into WebAssembly and the accompanying change to convex-backend to execute Wasm-compiled Convex functions using Wasmtime fibers right on convex-backend’s Tokio runtime.

TLDR results: queries and mutations are 2 times faster on average, tails 3 times smaller. Memory retained on the server for resident execution environments (V8 contexts vs. Wasm environments) is down 80%.

Before this change, fast execution of Convex functions required extensive V8 context caching and reuse. Without V8 context reuse, each Convex function starts with a fresh context without any modules loaded or JIT warmup done. It starts loading application modules necessary for executing the function and interpreting the function’s JS code. Even relatively simple Convex functions average 100-150ms of execution time.

The problem gets worse as the application grows and the modules get larger. Convex components remedy the uncontrolled module growth, but themselves add sufficient overhead and ceremony that I think make them a net negative for a lot of apps, definitely apps that are developed by a single developer or a couple of developers where there are no concerns of subsystem ownership, API stability, etc. I discussed my stance on the “majestic monolith” style of Convex development vs. component-heavy development in the post about my application.

Executing a Convex function with a reused, warmed-up V8 context that already loaded all modules and has the function’s code compiled into native code via TurboFan often drops Convex function execution time dramatically, as much as 10 times, e.g., from 100-150ms down to 10-15ms (provided that database rows that this function reads are already cached in memory).

So, V8 context reuse is/was essential for snappy and efficient Convex apps. However, there are two issues with it.

First, to ensure that > 99% of executions reuse V8 contexts, each V8 isolate in the convex-backend runtime needs to maintain more than one context: it needs a separate context for each of the modules (technically, module initialization groups) that get frequently assigned for execution on each specific V8 isolate by convex-backend. In my app’s configuration of self-hosted convex-backend, I’m currently using TinyLFU caches with up to 6+1 contexts (up from 5+1 since I posted about my self-hosted Convex config), and up to 400 total cached contexts, despite the fact that the peak load of my application is much lower than 400 simultaneous Convex function executions. The main reason for this relative inefficiency is that V8 contexts are pinned to V8 isolates and can’t be shared between isolates.

Each V8 context takes about 35MB of memory, and this gets larger itself as the application grows: two months ago, I reported that the average V8 context retained memory was 30MB in my application.

So, these contexts add up to about 14GB of memory on my convex-backend server.

The second issue with V8 context reuse is that some cached contexts are invalidated when the Convex app is deployed. This can trigger a big wave of module loading and compilation activity across the server that could last for about a minute, as all the hundreds of V8 contexts are re-created and re-warmed. V8 isolates don’t share optimized machine code with each other; even when serialized bytecode is cached, module initialization still happens for each V8 context in isolation.

Whether substantial V8 context invalidation happens depends on which modules were updated in the app deploy. But if the deploy updates exactly the key modules containing “hot” Convex functions that constitute a huge fraction of the total server load (and therefore each of these modules has many V8 contexts associated with it within TinyLFU caches of many different V8 isolates), a large portion of all V8 contexts might be scrapped and require re-initialization.

This wave of V8 context re-warming coincides with another wave that a Convex app deployment induces: cached query results for queries in the updated modules are invalidated and the reactive subscriptions from clients to the queries from these updated modules re-execute upon app deployment even if the data in the read set didn’t change. So, the server not only needs to compile stuff and load modules, but also execute more queries than in the steady state.

In practice, these post-deploy waves of work can lead to queries and mutations working slower for some time and even shedding of some executions if the overload is severe.

This is a more general issue of running Convex functions in V8 isolates, not an issue specific to V8 context reuse. convex-backend limits the number of V8 isolate workers that can actively execute (that is, not blocked by runtime dependencies like database reads or writes, or other I/O) via the FUNRUN_ISOLATE_ACTIVE_THREADS variable. This variable should be set to the CPU limit allotted to the convex-backend container, or a value a little lower than that.

Yet these threads mostly can’t be isolated (without restricting the maximum sustained throughput of the server a lot) from convex-backend’s own Tokio runtime whose OS thread count is set via the RUNTIME_WORKER_THREADS variable, or the V8 engine’s own background threads that are responsible for compilation and garbage collection in V8 contexts (whose count is set via the V8_THREADS variable).

In my server configuration, convex-backend’s CPU limit is 28.5 CPUs, FUNRUN_ISOLATE_ACTIVE_THREADS=28, RUNTIME_WORKER_THREADS=28, and V8_THREADS=16. Additionally, I have several Node execution pools with 1-2 active threads each. This means that in theory, there could be at least 75 active OS threads across the processes in the container that is limited to 28.5 CPUs.

This level of oversubscription is not terrible, but it is far from optimal. Over the last week when V8 Convex query and mutation execution was active on my server, the 5-minute period with the most contention reached 29% of CPU pressure. Of course, this happened around the time of an application deploy.

The tangible outcome of this CPU pressure is that when the server is busy (whether due to an app deployment, or just naturally increased load), Convex function executions get slower than they are under moderate load, even separately from the effects of slower database reads/writes (the database underneath Convex such as MySQL or PostgreSQL might itself exhibit higher CPU pressure, or its container might be throttled, during the periods of higher transaction load), higher query results cache churn, index cache churn, and V8 context cache churn.

Moving hot function execution from V8 to a WebAssembly runtime such as Wasmtime embedded in Convex’s Tokio runtime is an obvious response to the issues described above. The Convex team announced that a Wasm runtime is on the agenda early in 2026. In August, a Wasm runtime was added to the convex/convex-backend source tree. However, it was removed the next day and hasn’t reappeared since. That implementation used the QuickJS interpreter compiled into Wasm, so the Convex functions are interpreted on the Wasm substrate, not compiled themselves. Therefore, that implementation was very slow and wasn’t meant for production.

The conventional idea (also conveyed by Convex leadership during various Q&A sessions since August) is that developers are going to write Convex functions in a compiled language that has a Wasm target, primarily Rust, to benefit from the WebAssembly runtime’s performance.

I started developing a compiler for ordinary Convex functions (written in TypeScript) into WebAssembly bytecode using Static Hermes (a JavaScript AOT compiler that supports a Wasm target) in late July, before the Convex team’s plan to accelerate the Wasm runtime agenda became known: before August, Jamie Turner only said that a Wasm runtime would probably only arrive in 2027.

From July until now, in the whole development campaign that took almost three months, embedding the Wasmtime runtime into convex-backend was a relatively minor slice of work, totalling maybe about 10% of the total effort. In other words, just adding a Wasm runtime to convex-backend is comparatively easy, and it was relatively clear to me already from the beginning (even though I dramatically underestimated some other slices of the problem, and perhaps thought that a Wasm runtime in convex-backend would be 50% of the total effort, when in reality it ended up being only 10%, and the whole project took five times longer than I initially expected).

So, actually rewriting parts of my Convex application into Rust was already on the table back then. I didn’t go that route for a combination of reasons:

Rewriting an application in the ballpark of 350k LOC of production Convex code alone, with substantial sharing of logic with other (non-Convex) parts of the system that were running in Node.js (together with these parts, production code size was more than 0.5M LOC), seemed about as daunting as making a compiler of the existing Convex functions into WebAssembly.

I was anxious about Rust compilation times. And the development loop slowdown that might have caused. I feel partially vindicated in that because since then, I’ve written a piece of the application fully in Rust via the native resident functionality that I’ve added to convex-backend. This part is not a Convex action or a Convex query/mutation: this is a background Rust process that is deployed together with the Convex app and is calling Convex queries and mutations itself over loopback. This part of the application is only 30k lines of Rust, but whenever its code is changed, I feel the pain of slow Rust compilation times.

I didn’t want to invent Convex programming APIs in Rust (the equivalents of ctx.db.query(), insert(), defineSchema(), etc.) that would later be superseded by the Convex team’s official APIs. More generally, I wanted to do something that had the potential to be complementary, rather than divergent from or competing with the future Convex developments.

Finally, making a compiler just seemed more interesting to me than doing a Rust rewrite!

The Convex application code is unchanged: it is still TypeScript that uses the convex-js SDK.

convex-wasm-compiler orchestrates source analysis, JavaScript bundling, and native compilation. It uses tsrs to identify Convex function registrations and connect exported routes to their source, and esbuild to resolve dependencies and translate the application’s TypeScript into bundled JavaScript. Its Rust analyzer uses Oxc to inspect the source and dependency graph for unsafe retained state, establishing whether initialized modules can be reused across requests. These analysis results guide admission and packaging; esbuild’s emitted JavaScript supplies the executable application code.

The compiler then prepares that JavaScript as application modules, adds a bridge to Convex’s host operations (here and below, “host” means convex-backend’s runtime operations like database reads and writes, “guest” means application function execution), and invokes (Static) Hermes. Its shermes compiler emits C, which Emscripten’s Clang/LLVM toolchain compiles alongside the Hermes runtime and Convex’s native adapter. wasm-ld links these components, and Binaryen optimizes the resulting Core WebAssembly. The linked Hermes runtime supplies JavaScript objects, strings, promises, exceptions, and garbage collection inside the Wasm guest.

Static Hermes compiles JavaScript async functions into resumable state machines backed by prom

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →