Alvaro Guzman Résumé ↓
← Work / Systems
Personal project · the server behind this site

A web framework in C, from the socket up.

The page you're reading was routed, rendered, and sent by ~3,500 lines of C99 with zero framework dependencies; libcurl handles outbound email and zlib compresses responses. Not because C is the right tool for a portfolio — because owning every layer from accept() to template interpolation is the most honest way to show what "full stack" means.

ROLE
Author — every layer
STACK
C99, pthreads, Lua → WASM
PERIOD
2026 — present
DEPLOY
Cloud Run, multi-stage Docker
STATUS
Live — serving this page
The problem

Every framework hides the interesting parts.

After years of shipping on .NET and Angular, the question was: can I build the whole request lifecycle myself — socket loop, thread dispatch, routing, middleware, templating, static files — small enough to hold in one head, robust enough to run in production on Cloud Run?

Constraints: C99 + pthreads. No libevent, no framework dependencies; libcurl is linked only to send the contact form's email, and zlib only to gzip responses. Every byte of the HTTP response is assembled by code in this repo.

Architecture

One thread per request, one linked list per concern.

The socket loop accepts and dispatches to a pthread. The request walks a middleware chain — logging, static files, rate limiting, CSRF — then the router, then a controller ACTION, then a two-pass template render into the layout's body slot.

c-copper request lifecycleA request is accepted, handed to a pthread, parsed, passed through the logging, static, rate-limit and CSRF middleware, routed to a controller, rendered through the view engine into the layout, and written back to the socket.

/static/app.css

middleware chain (linked list)

logging

static/*

rate-limit

csrf

client

accept()

pthread dispatch

request parser

public/ file

router — strcmp / {param} bind

controller ACTION

view engine — 2-pass render

layout.html body slot

response_flush() → write()

ACCEPT
server.c
socket loop, pthread dispatch, request parser.
CHAIN
middleware.c
Linked list; each layer can short-circuit.
MATCH
router.c
strtok_r pattern match, {param} bind, first-match-wins.
RENDER
view.c
Six directives, two-pass layout render.

Decisions

D1

Thread-per-request over an event loop

Simpler invariants, no callback state machines. The throughput ceiling was accepted consciously and measured below — it is nowhere near this site's bottleneck.

D2

A template engine with six directives

Escaped {{key}}, raw {{{key}}}, #if/else, #each, @layout and @csrf, plus a two-pass layout render. No partials, no filters, no expression language — enough for real pages, nothing speculative.

D3

Linked-list middleware chain

Logging → static → rate-limit → CSRF → router. Each layer can short-circuit, and the order is readable at the call site in main.c.

D4

Build-time everything

Tailwind compiled to a static file; articles are markdown compiled to HTML; diagrams are mermaid rendered to SVG. Browser interactivity is Lua compiled to WASM, loaded per route. The server's job stays brutally simple.

Numbers

~1,900/s
Dynamic renders per second, full template.
~3,700/s
Static file responses per second.
23 / 65ms
p50 / p99 dynamic latency under load.
0
Framework dependencies; libcurl for email, zlib for gzip.
~3,500
lines of C, src/ (.c + .h)
21
http routes registered
~76KB
stripped server binary
~9KB
CSS over the wire, gzipped
Live, from this instance

This page was rendered in 0.88 ms. It is request #51 since this instance woke up 3h 23m ago. Cloud Run scales to zero, so the count resets when the instance sleeps.

/api/status →

Benchmark: hey, 50 concurrent connections, 10s runs (median of 3), Intel N100 (4 cores) under WSL2 — laptop-class hardware, not the Cloud Run deployment. Numbers are for honesty, not bragging: a thread-per-request C server on a small box outruns what this site will ever need.