Senior Software Engineer / Tech Lead | Paris, France
I spend my working hours on production TypeScript and Go, and my remaining
hours writing a programming language in Rust. Both make me better at the other.
- Ryo, a web-first language in Rust
- Production work
- Performance engineering
- Open source
- Homelab
- How I work
- Stack
PHP won the early web for one reason that had nothing to do with language design: it knew it was serving HTTP. Everything since has been a runtime bolted onto a general-purpose language. Ryo asks what you get if the compiler itself understands routes, shared state, background work and the database.
It started as a tree-walking AST interpreter that ran fib(40) in 81 seconds.
That version is gone. So is the bytecode VM that replaced it. The current
runtime compiles to bytecode and then to native code through Cranelift.
Measured on the TechEmpower Framework Benchmarks harness.
| Path | Ryo | Reference |
|---|---|---|
| Store query (JIT hot path) | ~675k req/s | - |
| Shared counter, parallel endpoint | 189k req/s | Bun: 152k req/s |
/db single query | 93k req/s | uWebSockets.js: 89k req/s |
| Peak memory, large payload | 15 MB | Down from 650 MB |
The Bun comparison is the one I find most interesting, because Ryo does not win by a large margin on median throughput. It wins at p95 and above, which is what you actually feel in production.
Runtime architecture
- Compilation. Source to bytecode, then Cranelift IR, then native code. Both JIT and AOT backends. Binary inspection on Mach-O arm64 showed
__textdominated bycranelift-codegenat around 5 MB, with a transitivewasmtime-internal-coredependency I am still working to drop. - HTTP. Hyper on Tokio, with
SO_REUSEPORTper core so the kernel distributes accepts rather than a single acceptor thread fanning out. Thread-per-core, moving towardio_uringvia Monoio after 1.0. - Optimisation passes. Scalar Replacement of Aggregates at the Cranelift IR level removed roughly 13 million runtime method calls from a single
bytes.athot loop. Arena allocators and prebuilt response caches are in; kernel-level profiling with flamegraphs drives the rest. - Memory.
Heap::Bytes(Arc<[u8]>, Range<usize>)gives zero-copy slicing. The 650 MB to 15 MB reduction came from fixing aWeakpointer cycle that was keeping whole buffers alive. TheBytesAPI is built aroundnext_lineandnext_fieldreturning[start, end, next], backed bymemchr.
Language design
The primitives are chosen around what web applications actually need, not around what is easy to compile.
store {}is an actor with aquery/actionsplit. It solves mutable shared state without locks in user code, and it turns out to be structurally the same thing as an ErlangGenServer, which I only realised after building it.parallel {}has static and dynamic forms, so the scheduler can plan the static case at compile time.schedule {}puts cron-shaped work in the language rather than in a sidecar process.model {}is the ORM, with the schema in the type system.html {}is a first-class type that is XSS-safe by construction. You cannot concatenate a string into it.memo fnandstream fnwithemit fromcover caching and SSE.- WebSocket routes are declared like HTTP routes, alongside SSE routes with
heartbeatand typed events.
Tooling ships with the language: a full LSP (diagnostics, completion, hover,
go-to-definition, references, symbols, signature help, formatting), a VS Code
extension, test blocks and fixture declarations inside .ryo files, and a
fuzzing harness.
The grammar is formally specified in EBNF at spec/grammar/ryo.ebnf.
Individual subsystems have their own written specs before implementation:
lifecycle events, config and services (config desugars to resource, with
a Secret<str> type), the mail service (send versus enqueue, memory
transport for tests), security defaults (CSRF on by default, rate limiting
with trusted_proxies) and a cache layer with a flat normalised store and
O(1) read and edit guarantees behind sharded read-write locks.
What is not done
The core language is feature-complete and core HTTP hardening is in place: body limits, per-request timeouts, graceful shutdown, CORS, CSRF, rate limiting, connection caps, security headers. What is still missing before I would put it in front of real traffic: database pool backpressure, full observability defaults, supply-chain signing, documentation, and any stability guarantee at all.
The open items are tracked in roadmap/before_prod.md and summarised in
spec/caution_prod.md, both in the repository. Ryo is not ready for your
production traffic and I will keep saying so until it is.
Technical lead on a multi-tenant SaaS platform for sports event management. I am the principal engineer across every repository in the organisation.
| Payments processed | 3.8M EUR / year, 9M EUR since 2022 |
| Registrations | 190 000 over 12 months |
| Share of all-time volume in those 12 months | 74% |
| Organisers on the platform | 600+ |
| PostgreSQL models under Prisma | 84 |
That 74% number is the one that matters. Three quarters of everything the platform has ever processed arrived in the last year, and it was absorbed without a rewrite.
Change data capture: LISTEN/NOTIFY to Debezium and Kafka
Postgres LISTEN/NOTIFY is a lovely primitive right up to the point where you
need durability, replay, or more than one consumer group. Migrated to Debezium
reading the write-ahead log into Kafka, with Go consumers fanning out to
ClickHouse for analytics and Meilisearch for search. Search latency on
participant lookup dropped 40%, partly from the index being fed properly and
partly from query rewriting on the read side.
A WebSocket gateway for identity verification over 3G
Race sites are fields. The network is one bar of 3G and a lot of hope. Built a Go WebSocket gateway that streams identity documents and face captures to OCR and facial recognition, chunked and backpressured for links that drop constantly. The naive HTTP upload version failed often enough to be unusable.
Payments across three gateways
Monetico, Fintecture and PayZen, each with its own opinion about HMAC signatures and webhook semantics. The interesting work is not the happy path, it is reconciliation: what you do when the webhook arrives twice, arrives late, or never arrives and the money moved anyway. Plus refunds, which every gateway models differently.
Caching in three tiers
Redis for shared hot state, Badger and Pogreb for embedded local tiers where a network hop was not worth paying for. Chosen per access pattern rather than by putting Redis in front of everything and hoping.
The habit that connects everything above: measure at the level below the one you think the problem is on.
1 Billion Row Challenge, Rust and Java
Implemented independently in both, because the two languages fail in different places.
Java. SWAR temperature parsing, branchless throughout, flat open-addressed hash table, GraalVM native image to skip warmup.
Rust.read_unaligned for the parsing hot path, name addresses stored as
offsets into the mapped file rather than as pointers or owned strings, and a
triple-cursor hot loop to give the CPU three independent dependency chains to
work on at once.
Also profiled MmapSlice against a straight Vec copy on a 13 GB input,
which is where the zero-copy Bytes design in Ryo came from.
Tooling I built to do this
flamegraph_breakdown.py, a per-worker function-level breakdown of flamegraph output, because aggregate flamegraphs lie when your workers are not symmetric.- A Streamlit and Plotly benchmarking dashboard that ingests parquet and CSV so framework comparisons are reproducible rather than remembered.
| Project | Contribution |
|---|---|
| fullcalendar-vue | Ported the component from Vue 2 to Vue 3. Merged to the vue3 branch and released as the @fullcalendar/vue3 package. |
| oven-sh/bun | Implemented createReadStream on Bun's native Readable API, benchmarked ahead of the Node.js path. Not merged, the feature landed independently before review. |
| ryo-js | Filesystem-routed TypeScript fullstack framework. APIs, WebSockets, GraphQL, SSE, Preact components, SPA routing. The ancestor of Ryo. |
| postgrest-java | Java client for PostgREST. |
Built from parts: MACHINIST PR9 X99 board, Xeon E5-2699 v4, Ubuntu Server, Docker, Nextcloud, Jellyfin. It has taught me more about operations than any managed platform has.
The best lesson came from a thermal emergency. The NVMe drive hit 100 C and I
could not find a workload to blame. The cause was the nouveau GPU driver
flooding the kernel log at 49 000 messages per second, and the disk was cooking
itself writing them down. Nothing in the application layer would ever have
shown me that.
- Read the layer below. Most performance mysteries resolve one abstraction lower than where they appear. Flamegraphs,
perf, disassembly, kernel logs. - Throw away the first version. Ryo's interpreter, its bytecode VM, and its early
BytesAPI all got deleted. Keeping a bad design because it took a long time to write is the expensive mistake. - Benchmarks or it did not happen. A claimed speedup without a reproducible harness is a guess with good posture.
- Write the spec first for anything with a surface area. Every Ryo subsystem has a written specification before it has an implementation. It is faster, not slower.
- Say what is broken. See the "what is not done" section above.


