The thesis
This blog rides on something I call BloodIOCTLV9. It's the engine underneath. Not a framework, not a CMS, not a static site generator. An engine — the kind of thing you'd build to run a real-time multiplayer game server, except the surface I bolted on top of it is a blog.
The litmus test is one enum:
public enum Transport : byte
{
Unknown = 0,
TCP_MessagePack = 1,
TCP_JSON_Encrypted = 2,
TCP_SimpleEncryption = 3,
UDP_MessagePack = 4,
UDP_JSON = 5,
HTTP_MessagePack = 6,
HTTP_JSON = 7,
WebSocket = 8
}
Nine wires. One command handler answers over all of them without knowing which one carried the request. You write the function once. The chassis picks the transport at runtime — TCP/MessagePack for a thick desktop client, UDP/MessagePack for telemetry that doesn't care about loss, WebSocket for the browser, HTTP/JSON for an external script poking the admin API. Same code path. No if (transport == X) branch anywhere. No separate "REST controller." No separate "WebSocket hub." One function, nine possible mouths.
The post you're reading right now was rendered by the HTTP/JSON face of the same machinery. The comment you might leave below will travel WebSocket and land in HolyLain — the live in-memory DB. The agent that wrote this post talked to it over plain HTTPS plus a bearer token. All the same engine. No glue layers. No "but for that case we use…"
This is the opposite of vibe-coded. Nobody npm-installed their way here. Nobody asked an AI to "scaffold a blog." There are no node_modules at all on the server. There's a single self-contained ~80MB Linux binary that boots in under 200ms and serves every request — HTML, JSON, MessagePack, UDP datagrams, the lot. The source is mine. Every line of it. The cave-man comments at the top of every file are mine. The operatic naming (BrutalDB, HolyLain, LammerPwner, OPENFIRE) is mine.
What follows is how this happened, what's actually in the box, and why a chassis built for real-time game traffic turns out to be the right shape for a personal blog — not despite the overkill, but because of it.
Origin story: the protocol IS the architecture
I learned how to build servers by tearing apart how other people's servers got beaten.
The formative texts in my life are not Designing Data-Intensive Applications or Clean Code. They're the leaked source trees of Silkroad Online, the reversed packet structures of Mu Online, and the absolutely incomprehensible Korean code that somehow held tens of thousands of concurrent players together with two grams of duct tape and a prayer to whatever god looks after CCU.
If you've never spent a weekend in a hex editor watching a client encode a movement packet, here's what you learn:
The protocol IS the architecture. Everything else is wrapping paper.
A movement packet in Silkroad is on the order of 18 bytes. Opcode (2), char ID (4), flag byte (1), region ID (2), x/z floats (8), heading byte (1). It fires 10–15 times a second per player. Multiply that by 5,000 concurrent players — that's 50,000–75,000 movement packets per second, each one needing to be parsed, validated, sanity-checked against the world map, and broadcast to every other player within view distance, all before the next packet from the same player arrives ~70ms later.
That constraint forces every other decision you make. It forces UDP for movement (you cannot afford TCP head-of-line blocking). It forces wire formats designed with byte-level care (every byte saved is 75,000 bytes/s saved). It forces you to think about your DB as a hot cache, not a source of truth (you can't round-trip Postgres for every step a player takes). It forces lock-free atomics for counters because a hundred threads are touching the same player record. It forces you to measure everything, because the difference between "smooth" and "rubberbanding" is one slow handler buried in the chain.
Most of those constraints don't apply to a blog. Nobody is WASD-ing through /papers. But the taste you develop from operating under them changes how you write everything else.
You stop seeing "HTTP+JSON for the API" as a default. It's one transport choice among many — convenient for external integrators, wildly wasteful for an internal client your own software wrote. You stop seeing your ORM as the storage layer. It's a translation layer; the storage layer is your indexed lookup. You stop seeing your framework as your architecture. It's a default someone else picked.
After fifteen years of that, the only thing that feels honest is to build your own.
So I did.
What's actually in the box
A tour. The names are deliberately operatic — software that takes itself too seriously to be "agile."
- BloodIOCTLV9 — the chassis name.
v9because v8 was bad. v7 was worse. v1 was a chat protocol I wrote in college because I couldn't get IRC's source to compile. The rig as a whole. - BrutalDB — the storage layer. In-memory entity store with sorted indexes.
[BrutalDBIndex]and[BrutalDBUnique]attributes on entity properties, a source generator builds the index registry at compile time, and queries look likeDB.HolyLain.Index<BlogPost>("Slug", "not-vibe-coded").FirstOrDefault(). That's the entire query language. No SQL, no ORM, no migrations. Persistence is MessagePack snapshots to disk. - HolyLain — the live DB instance, named after Lain Iwakura from Serial Experiments Lain — the one who eventually realizes she IS the network. All blog content lives here.
- WiredStream — the per-request object passed to every command handler. Holds the deserialized payload, the auth context (user, access level, divine flag), the active transport, and a writer that knows how to fire the response back to the originating client.
- IResponseWriter — the abstraction every transport implements. One method to send a success, one to send an error. Command handlers receive a
WiredStreamwhose writer might be TCP, UDP, WebSocket, or HTTP. They don't know and don't care. - TRANSPORTS/ — the directory with one file per wire format.
TCPMessagePack.cs,UDPMessagePack.cs,UDPJson.cs,TCPJsonEncrypted.cs(ECDH + AES-GCM),TCPSimpleEncryption.cs(XOR+HMAC for low-cost paths),TCPHeartbeat.cs. Each one accepts incoming bytes, deserializes into aCommandRequest, dispatches to a handler, sends the answer back. - RESPONDERS/ — the parallel directory of
IResponseWriterimplementations, one per transport. Wire-specific framing lives here. - PulseJsonContext — the source-gen
JsonSerializerContext. Every type the wire can carry is registered as[JsonSerializable]. Zero reflection at runtime. - AotMessagePack — the equivalent for binary.
[MessagePackObject]+ source-gen formatters. Same zero-reflection guarantee. - LammerPwner — security primitives. The name is from "lamer-pwner," the spirit of someone who's been on the receiving end of enough script-kiddie probes to decide to write the layer that throws them out. IP rate-limiting, hash-equality timing safety, the failure-counter brain behind login throttling.
Every file in the tree opens with a header in deliberate cave-man voice. They're not jokes. They're how I think about the code: as instruments, not as art.
* Description: Cave man stab stick in dirt at start. Stick fall over at end.
* `using` block do magic. Even if rock fall on code, stick still fall.
* Cave man no forget log. Cave man no forget Prometheus grunt.
* Five line of boilerplate become one. Cave man happy.
That's the actual top of ScopedTimer.cs. It tells you, in four lines, what RAII does, why it matters, and that the author has thought about it long enough to find it funny. When you've read enough corporate JavaDoc, this is the only honest register left.
Protocol abstraction: write once, run over anything
The heart of the chassis is one interface and one attribute.
The interface:
public interface IResponseWriter
{
Task WriteResponseAsync(WiredAnswer answer);
Task WriteErrorAsync(string errorMessage);
string ConnectionId { get; }
Transport TransportType { get; }
bool IsConnected { get; }
}
That's the entire contract a transport has to satisfy. Four methods, two properties. There's an implementation for every wire — TCP/MessagePack, TCP/JSON+ECDH, TCP/SimpleEncryption, UDP/MessagePack, UDP/JSON, WebSocket. They're files in RESPONDERS/, a few hundred lines each, and they're the only place wire-specific framing lives in the codebase.
The attribute:
[WiredCommand<EmptyRequest, AdminBotKeyMainGetResponse>(
"ADMIN_BOTKEY_MAIN_GET",
MinAccessLevel = AccessLevels.SysAdminGate,
RequiresDivine = true,
Description = "Gets masked status for ADMIN_BOTKEY_MAIN. Owner-only.")]
public static void ADMIN_BOTKEY_MAIN_GET(WiredStream ws)
{
var row = AdminSecretSettingsCore.Current(AdminSecretSettingsCore.AdminBotKeyMainName);
ws.OPENFIRE(Project(row)).Wait();
}
Read it again. There is no transport-aware code anywhere in that function. No if (ws.Transport == UDP). No separate Express controller and separate WebSocket handler and separate gRPC service that all do "the same thing, slightly differently." One function. One signature. One body.
The [WiredCommand<TReq, TResp>] attribute is consumed by a source generator at compile time. It walks every public static method in the assembly, finds the ones tagged with the attribute, and emits a routing table — a Dictionary<string, CommandHandler> keyed by command name, with each entry knowing its access level, its expected request type, and its response type. Zero reflection at runtime. Zero attribute-scanning startup tax. The routing table is already built before main() runs.
At request time, a transport receives bytes, deserializes them into a CommandRequest, looks up the handler in that pre-built table, constructs a WiredStream with a transport-appropriate IResponseWriter attached, and calls the function. When the handler calls ws.OPENFIRE(response), the writer serializes the response with the right format (MessagePack for binary wires, JSON for JSON wires), frames it with the right framing (length-prefixed for TCP, raw datagram for UDP, WebSocket frame for SignalR), and sends it.
The handler doesn't know it's running over UDP. It doesn't know its bytes will travel an encrypted TCP stream. It doesn't know the response will land in a browser via SignalR. It doesn't need to know. That's the entire point of the abstraction.
There's a second-order consequence here that took me years to internalize: when the transport is interchangeable, you stop designing handlers around the transport. You design them around the operation. "Set the admin bot key" is the operation. How the byte tube delivered the request is somebody else's problem. Testability falls out for free — you can call the handler with a stub WiredStream in a unit test. New transports come online without touching any of the hundreds of commands already in the system.
That's the engineering win. But it's also the philosophical win, because it forces you to think about what your software does instead of what protocol some framework picked for you in 2014.
UDP as the litmus test
An abstraction is only as good as the most hostile transport you can shove through it. For me, that transport is UDP.
Here's the actual top of UDPMessagePack.cs:
* Description: Cave man open ear at port 1340. Listen for binary pebble
* fall from sky. Each pebble a whole MessagePack thought,
* no glue, no length prefix, datagram is the box. Cave man
* eat pebble, run command, throw answer back. No handshake.
* No memory of who threw. Game telemetry love this. Loss ok.
UDP is the hardest possible test for a transport-agnostic command handler. Why:
- No connection — every packet is an island. No session, no handshake, no "open" or "close." The sender's IP can change between packets. The responder has nothing to bind state to except what the request payload carries.
- No ordering — packet B can arrive before packet A even though A was sent first. You cannot assume sequential delivery within a "logical conversation."
- No retries — a dropped packet is dropped. If your handler relies on the client retrying, your handler is broken on UDP.
- No flow control — a fast sender can drown a slow receiver. The OS will silently drop datagrams once buffers fill. No backpressure signal.
- Size cap — a UDP datagram is bounded at ~64KB by the IP stack. If your handler produces a 2MB response, UDP can't carry it. Period.
Most "framework" RPC layers don't survive contact with these constraints. They were built assuming TCP semantics — a connection, ordering, retries — and bake those assumptions into every layer above the socket. When you bolt on "UDP support" later, you discover all the implicit invariants you didn't know you had.
In BloodIOCTLV9, UDP works because the entire chassis was designed assuming it might be UDP. Handlers can't reach into "the connection" because there isn't one to reach into. Responses are constructed and handed to the writer; the writer decides whether to length-prefix them or fire them as a single datagram. Handlers that would generate a >64KB response are caught at design time — the response struct is known via source-gen, you'd notice the size budget by inspection.
What's UDP actually used for here? Game-style telemetry. Fire-and-forget metric pushes. Anything where occasional packet loss is preferable to TCP's head-of-line blocking. A home-network telemetry probe will fire UDP/MessagePack datagrams at port 1340 every second, and the same command handler that answers HTTP/JSON over 443 will absorb them.
If your abstraction survives UDP, your abstraction is real. If it doesn't, you have a TCP framework wearing a transport-agnostic t-shirt.
Observability to the line
If you can't see it, you can't make it faster. A chassis built for game-server latency budgets had to come with instrumentation that costs nearly nothing on the hot path. Two primitives carry most of the weight.
ScopedTimer — RAII auto-instrumentation. One line at the top of any method or using block and you get the duration logged AND emitted to Prometheus when the scope exits, even if the code threw an exception on the way out.
using ScopedTimer timer = new ScopedTimer("UPLOAD", "file_upload");
await ProcessFileAsync();
// Dispose() fires automatically:
// - logs "file_upload completed in 234ms"
// - metric PrometheusCollector.RecordCommand("file_upload", 234000, true)
The cave-man header:
* Description: Cave man stab stick in dirt at start. Stick fall over at end.
* `using` block do magic. Even if rock fall on code, stick still fall.
* Cave man no forget log. Cave man no forget Prometheus grunt.
* Five line of boilerplate become one. Cave man happy.
* Tick in, tick out, ms or s out the other end. Done. No tears.
It's a readonly struct. The constructor reads Stopwatch.GetTimestamp() into a single long. Dispose() reads it again, computes the delta, writes the log line and the metric. That's the whole API. You can sprinkle them through code as freely as you want — the overhead is two timestamp reads and a couple of function calls. Tens of nanoseconds, not microseconds.
PrometheusCollector — the metric sink. Every command handler is auto-wrapped in a ScopedTimer by the dispatcher, so every command name has a histogram of durations and a counter of failures, with no per-handler boilerplate. The collector itself is lock-free:
private static readonly ConcurrentDictionary<string, MetricCounters> _commandMetrics = new();
private static readonly ConcurrentDictionary<string, HistogramData> _histograms = new();
// Hot path: three atomic ops, ~5ns
public static void RecordCommand(string cmd, long ticks, bool ok)
{
var c = _commandMetrics.GetOrAdd(cmd, _ => new MetricCounters());
Interlocked.Increment(ref c.TotalCalls);
Interlocked.Add(ref c.TotalDurationTicks, ticks);
if (!ok) Interlocked.Increment(ref c.TotalErrors);
}
No mutex. No lock block. No async overhead. Just three Interlocked ops on the hot path. A background timer wakes every 10 seconds, snapshots the counters into a fresh MetricsSnapshot, and the /metrics endpoint serves that snapshot in Prometheus-text format on demand. Scrape it from whatever dashboard you like.
What falls out: every command in the system has p50/p95/p99 latency, error rate, and call count, always on, with no annotations to write, no decorators to remember, no AOP weaving. It's not a feature you opt into. It's the default state of being a command in this chassis.
The HTTP router does the same. Every SSR render emits a Server-Timing: render;dur=1.4 header on the response — open DevTools → Network → Timing on this very post and you'll see it. Measured at the line, exposed at the wire, free. The combination of protocol abstraction and observability is the actual magic; either one alone is just a tool. Together they're a feedback loop that lets you reason about a multi-protocol server the way an embedded engineer reasons about a serial bus: every signal is on the scope.
AOT: the performance subsection
The binary that serves this blog is NativeAOT-compiled. Compile-to-native ahead of time. No JIT. No IL on disk. No .NET runtime preinstalled on the VPS. One ~80MB self-contained Linux executable that contains the entire .NET base library it actually uses, statically linked, plus my code, plus every transitive dependency, all in one ELF.
What that gets you:
- Cold start under 200ms. The VPS reboots, the binary launches, the systemd unit reports
READY=1, traffic flows. You can deploy ten times an hour without users noticing. - Zero JIT warmup. The first request hits the same machine code as the millionth. There is no "the first few requests are slow" effect. There is no tiered compilation kicking in halfway through a load test. It's the fast path from request one.
- Zero reflection. The trimmer eats any code reachable only via reflection, so the codebase is structured around source generators. Every JSON type is registered in
PulseJsonContextvia[JsonSerializable]. Every MessagePack type is[MessagePackObject]with a source-gen formatter. The[WiredCommand]routing table is built at compile time. The BrutalDB index registry is built at compile time. Nothing scans assemblies at boot. - No .NET runtime on the VPS. You don't
apt install dotnet-runtime-10. Youscpa tarball, untar intoreleases/<commit>/, swing a symlink, restart the service. Operators love this. Small VPS providers love this. Nothing-but-the-binary deploys love this.
What it costs:
- Build time. The AOT compiler takes about 2–3 minutes to do whole-program optimization, trimming, and native code generation for this codebase. On the local Linux build box (i9, 64GB) it's brisk. On a 2-core / 2GB VPS, the same build would OOM in thirty seconds — which is why the build runs on the local box, a Forgejo workflow does the publish, and only the resulting tarball ships to the VPS via
scp. - Strictness. Trimming will silently break anything reachable only via reflection. You learn this the first time. You stop reaching for reflection. Eventually you stop wanting to reach for reflection, because forcing yourself to make your data flow explicit turns out to make the code easier to reason about regardless.
The tradeoff math is trivial. I deploy on the order of ten times a day on a busy day. Every user request hits the warmed binary millions of times. Spending three build-minutes once to save tens of milliseconds per request, every request, forever, is so lopsided it's barely a tradeoff.
The trimmer-strictness cost is one-time. The runtime savings are perpetual. That's the right shape of bet.
The chassis used to run on JIT-mode .NET. Switching to NativeAOT was the single biggest "this thing feels different now" moment of the project. The cold-start improvement alone made deploying psychologically free — and once deploys are psychologically free, you deploy more, you iterate faster, you ship the post you're reading right now via an agent on a Sunday afternoon because the friction is gone.
Why this blog is SSR (mechanics, not aesthetics)
The blog you're reading right now is server-side rendered HTML, augmented with htmx for SPA-feel navigation. Zero React, zero Vue, zero Solid, zero hydration. One server response per nav; the browser just swaps a <main> chunk in place.
I am sympathetic to people who believe you can ship a beautiful experience with client-side rendering. I am also the same person who has watched Google Search Console show six months of "discovered but not indexed" on JS-rendered routes. The truth is uncomfortable:
Google bots are meh at JavaScript.
The Googlebot's rendering pipeline is technically a real browser, but it's queued, rate-limited, and unpredictable. Fast JS sites still get indexed reliably. Complex SPA-style apps with client-side routing and async data fetching often get partial indexing, or get indexed with a multi-week delay, or get the wrong title because the JS hadn't run yet when the snapshot was taken. You can argue with this. The argument doesn't change what happens to your traffic.
The cheap fix is server-side rendering. You don't need a SPA. You need first-paint HTML that says what the page says. Once that's a hard requirement, you might as well make it the only requirement and skip the whole rehydration tax.
htmx gives you the rest of the user experience for ~14KB. Clicks become fetch() calls that swap a piece of the DOM. No virtual DOM diffing. No JS-framework lifecycle to learn. No useEffect mental model to internalize. Just HTML over the wire, in and out. The chassis already speaks HTML — adding htmx-style fragment endpoints was a few extra lines per page.
The service worker (separate post coming) handles the "feels instant on repeat visit" piece for free. The icon font got subsetted from 1.9MB to 13KB because real cold-load metrics actually matter (also a separate post coming). Together: a page that ships <50KB cold, renders before paint, indexes cleanly, navigates without a full reload, and survives a 2G connection.
The framework you didn't pick is the dependency you don't have to update for the next decade.
Closing: what this is for
The cost is years.
I've been at this for fifteen of them. Reversing other people's protocols, watching commercial servers fail in interesting ways, getting frustrated by every layer of every framework I tried to fit my problems into, eventually realizing the friction wasn't accidental — it was the framework imposing its model on my work, and my work didn't want that model.
The reward is a stack that bends to my shape.
If I want to add a new wire format tomorrow, I write one IResponseWriter implementation and the hundreds of commands already in the system pick it up for free. If I want a new admin operation, I write one function with a [WiredCommand] attribute and it's instantly available over every transport, with auth, with audit, with metrics, with the bearer-token API I demoed in the previous post. If I want to change how the blog renders, I edit one C# file and dotnet publish ships a new binary in two minutes. No node_modules, no package-lock.json, no "let me check what version of React we're on this quarter."
This isn't for everyone. It's specifically not for someone whose career rewards them for picking the "obvious" stack. It's not for someone who wants a working blog in an hour and doesn't care what's inside. It's not for someone who measures their productivity in npm installs saved.
It's for the people who've been around long enough to develop opinions. Who'd rather spend a year understanding their stack than five years debugging someone else's. Who feel a small but real revulsion every time a framework asks them to do something stupid because "that's the framework way."
It's for the people whose GitHub handle is /layer07 because they think of themselves as the application layer — the layer where intent meets implementation, and where the choice of how matters as much as what.
If that's you, build your own. Start with the protocol. The rest follows.
This post was authored by an agent, via the admin API described in the previous post, patched section by section live, in front of you. No node_modules were harmed.

comments (0)
Markdown supported, fenced code encouraged.