End of life: sunsetting Ferron 1.x (and story about it)

Published on: Author: Dorian Niemiec

Today, Ferron 1.x officially reaches End of Life (EOL). It will no longer receive security patches, dependency updates, or bug fixes.

This isn’t a failure story. It’s a story about learning what actually matters when you’re building infrastructure that runs in production.

The journey: from JavaScript to Rust

The Ferron project (originally code-named “Project Karpacz”) started because I wanted better performance. Before Rust, I had built an asynchronous web server in Node.js called SVR.JS. Writing a server in JavaScript taught me how HTTP parsing works, how to route requests, and how proxies behave.

But eventually, you hit a wall. Single-threaded event loops, garbage collection pauses, and high memory overhead are fine for business applications, but they don’t work for infrastructure.

To build a real web server, I needed a systems language. I turned to Rust, and that became Ferron 1.x.

What Ferron 1.x accomplished (and its fatal flaw)

As my first serious project in Rust, Ferron 1.x was very, very ambitious. I tried to build a monolithic “Swiss Army knife” that did everything. It had custom FastCGI decoders, process pre-forking pools for WSGI/ASGI (which I added later), SCGI, CGI, caching, and custom web serving all packed into the binary with feature flags.

It was memory-safe, and it was fast. But it had a major problem: it was fast, but it was blind.

When something broke, I was flying in the dark. There were no trace IDs, no way to correlate requests across services, no structured logs. I had access logs, timestamps, and prayer.

The good parts

Even in that monolithic 1.x codebase, some of the ideas that define Ferron 3 today were already there:

  • Separate runtimes - I split execution into a server-pool and a log-pool so that disk I/O from logging wouldn’t slow down network tasks.
  • Fast memory allocation - I used mimalloc as the global allocator from day one.
  • Graceful reloads - I wrote a reload loop triggered by Unix SIGHUP signals.

The crucible: failing forward to vibeio

Every big step forward came from an earlier mistake.

Early on, I tried to build an HTTP server library completely from scratch. I used Mutex<T> everywhere, ran into lock contention, watched it underperform compared to Hyper, and abandoned it.

But that failure taught me what I needed to know to build vibeio and vibeio-http later.

[Node.js SVR.JS] ──► [Ferron 1.x monolith] ──► [mutex-heavy HTTP failure] 

[Ferron 3 edge proxy] ◄── [vibeio-http core engine] ◄───┘

When I built vibeio, I wanted a high-performance, cross-platform runtime that pins execution to specific CPU cores, maximizes cache locality, and avoids context-switching overhead. I wanted to replace the monoio async runtime that I used for Ferron 2, which wasn’t being actively maintained.

I built vibeio with AI coding agents early on, but it wasn’t “vibe coding.” It required a lot of manual work—fixing io_uring use-after-free bugs that caused silent memory corruption, tracking down macOS zombie process hangs in tests, and debugging Windows IOCP ConnectEx issues with unbound UDP sockets.

The evolution: 1.x vs. 2.x vs. 3.x

The project descriptions show how my thinking changed:

  • Ferron 1.x: “A fast, memory-safe web server written in Rust.” (focus: learning Rust)
  • Ferron 2.x (current stable): “A fast, modern, and easily configurable web server with automatic TLS.” (focus: features)
  • Ferron 3.x (beta): “A fast, modern web server built for production debugging.” (focus: observability)

Ferron 2

Ferron 2 is our current stable version. It completely changed how the engine handles modularity. It moved away from the dense 1.x YAML format (people complained about it; there’s even a website: http://noyaml.com/) to the cleaner KDL configuration syntax, and switched to the monoio async runtime—which I forked and modified to add Windows and FreeBSD support.

Ferron 3

Ferron 3 is a complete shift in philosophy. Where 1.x was about speed, 3 is about seeing what’s happening.

We took the 1,500+ line monolithic server.rs event loop and split it into separate components using vibeio and vibeio-http, with zero-copy network frames.

WhatFerron 1.x (old monolith)Ferron 3.x (modern edge proxy)
PhilosophyMonolithic (“Make it fast”)Observable (“Make it debuggable”)
RoutingFlat scans / SNI matchingTlsResolverRadixTree
LoggingText dumps to diskNative OTLP, trace IDs, grep-able spans
ProtocolsBuilt-in FastCGI, WSGI, ASGI, CGIProxy to external runtimes
PerformanceMulti-threadedCore-pinned workers with zero-copy sendfile
ConfigYAMLferron.conf

What this means for users

If you’re still running Ferron 1.x in production:

  • No more support - the 1.x branches won’t be monitored or updated anymore.
  • What to do - for stable deployments, migrate to Ferron 2. If you want modern observability and tracing, try the Ferron 3 beta.

Ferron 3 is growing; it’s being packaged for Ravenports, patched for DragonFlyBSD, and used in production by homelabbers and small operators.

Thank you

To everyone who deployed, tested, and broke Ferron 1.x: thank you. You helped me realize that as systems get bigger, developers don’t just want a faster black box. They want to see what’s actually happening with their requests.

Ferron 1.x was the chrysalis. Let it rest. Time to move forward.