# dihlabs.lol Source: https://dihlabs.lol/ > A dev lab where I build and break things for fun: diffusion models like SDXL, image and video generation, LLMs, and fast software on small machines. This site runs on its own framework, on a $1.50 server. # dihlabs.lol A dev lab by sxnistxr A dev lab where I build and break things for fun: diffusion models like SDXL, image and video generation, LLMs, and fast software on small machines. This site runs on its own framework, on a $1.50 server. [GitHub](https://github.com/sxnistxr)[Source](https://github.com/sxnistxr/OPTZ) Checking the server The server behind this page answered in **…**. It has been up for … on one CPU core. ## In the lab - Image generationExploring ### SDXL Stable Diffusion XL on my own hardware: samplers, schedulers, LoRAs and refiners, and what fits in how much VRAM. - Generative mediaExploring ### Image and video generation Diffusion and video models beyond SDXL: image-to-image, inpainting, upscaling and short clips, and the pipelines that glue them together. - Language modelsExploring ### LLMs Running and testing language models: local inference, quantization, prompting, tools and agents, and building small apps on top of them. - Web frameworkShipped ### [optz](/docs/introduction) The React framework for Bun this site runs on: HTML by default, islands for interactivity, Server Actions, streaming and ISR. About 40–60× the requests per second of Next.js on one core. - SystemsShipped ### [The $12 Server Challenge](/docs/engine) A social network backend in Rust on raw io_uring and SQLite, built for the $12 Server Challenge: one CPU, 2 GB of memory, as many users as it can hold. ## Writing - [The close that never closedio_uring direct descriptors on kernel 6.8, and why my server's refusals were silent.Oct 26, 2026](/blog/a-close-that-never-closed) - [SQLite was re-parsing my query on every requestA prepared statement that wasn't staying prepared, and the compile flag that fixed it.Oct 25, 2026](/blog/sqlite-was-reparsing-my-query) ## The code behind it This site runs on optz, a React framework for Bun built in this lab, on a $1.50 server. The docs cover how to build on it, how it's made, and how fast it is. [Read the docs](/docs)[Why optz](/docs/why-optz)[optz vs Next.js](/docs/vs-nextjs) --- # Writing Source: https://dihlabs.lol/blog > Writing by dihlabs.lol. # Writing Notes from building fast things on small machines. - [The close that never closedio_uring direct descriptors on kernel 6.8, and why my server's refusals were silent.Oct 26, 2026](/blog/a-close-that-never-closed) - [SQLite was re-parsing my query on every requestA prepared statement that wasn't staying prepared, and the compile flag that fixed it.Oct 25, 2026](/blog/sqlite-was-reparsing-my-query) --- # The close that never closed Source: https://dihlabs.lol/blog/a-close-that-never-closed > io_uring direct descriptors on kernel 6.8, and why my server's refusals were silent. My server keeps every connection as an io_uring direct descriptor: a slot in the ring's own file table instead of a normal file descriptor. That's how it gets past the 65,535 open-file limit. On my development machine (kernel 6.18) every test passed. On the real target, Ubuntu 24.04 with kernel 6.8, one test hung: when the server refused a connection because it was full, the client never found out. ## What was happening Before kernel 6.13, removing a direct descriptor doesn't take effect right away. The kernel waits until every older in-flight request on the ring has finished. An idle keep-alive connection always has one request in flight: the multishot receive waiting for its next request. With thousands of idle connections, that wait never ends, so the close never reached the client. Closes started by the client still worked, which is why every other test passed. ## The fix Shut the TCP connection down first, with an io_uring `shutdown` hard-linked to the close. Shutdown acts on the socket straight away, whatever the descriptor table is doing. The close still happens later, but the client sees the connection end immediately. The lesson I keep relearning: test on the kernel you'll actually run on. [All writing](/blog) --- # SQLite was re-parsing my query on every request Source: https://dihlabs.lol/blog/sqlite-was-reparsing-my-query > A prepared statement that wasn't staying prepared, and the compile flag that fixed it. The single-post endpoint was slower than it had any right to be: 18 µs per request for one primary-key lookup and a count. A profile showed the surprise right at the top. `yy_reduce` and `sqlite3RunParser` were running on every request. That's the SQL parser, and a prepared statement should never need it again. `sqlite3_stmt_status(stmt, SQLITE_STMTSTATUS_REPREPARE, 0)` confirmed it: 200 re-prepares in 200 calls. ## Why When SQLite plans a query, it can notice that the best plan might depend on the value bound to a parameter. If so, it marks the statement so that binding a different value expires it, and the next step re-prepares it from the SQL text. Each request bound a different post id, so each request paid for a full parse and plan. ## The fix Compile SQLite with `SQLITE_ENABLE_QPSG`, the query planner stability guarantee. Plans are fixed when the statement is prepared and never depend on bound values. For simple index lookups that's exactly what you want anyway. Single-post reads went from 47,800 to about 108,000 requests per second on one core. A test now asserts zero re-prepares, so it can't quietly come back. [All writing](/blog) --- # Docs Source: https://dihlabs.lol/docs > How to build on optz, how it's made and with what, how fast it is, and how the lab's server runs. # Docs Everything in this repository: the optz framework this site runs on, how it's built and with what, how fast it is and why, the Rust engine, and how it all deploys to a $1.50 server. Start with the [introduction](/docs/introduction). ## Start here - [Introductiondihlabs.lol is a dev lab by sxnistxr: a place to try things for fun, from diffusion models and LLMs to fast software on small machines, and write down what happens. This…](/docs/introduction) - [Getting startedInstall Bun, run this site, edit its content, and start a new optz app.](/docs/getting-started) - [Why optzPick a framework by what your site does most. For most sites that's serving pages people read, and that's what optz makes nearly free, without giving up the features an app needs.](/docs/why-optz) ## Building with optz - [Framework referenceEverything optz does, with examples: routes, render modes, islands, Server Actions, streaming, ISR, API routes, i18n, images, fonts and deploy targets.](/docs/framework) - [Every feature in one appEvery feature optz shares with Next.js, in one small app: streaming and _loading, Server Actions (also without JavaScript), ISR and on-demand revalidation, dynamicParams, draft…](/docs/features-app) - [Social demoThe social feed that used to be on dihlabs.lol, now its own app: a token panel, compose box, feed and post pages as optz islands, over the Rust engine in portfolio/engine (the…](/docs/social-demo) ## AI and agents - [optz with AIConnect AI clients to an optz site over MCP, give your app its own MCP tools, and set up coding agents to work on an optz project.](/docs/ai) ## How it's made - [Architecture and techHow optz and this site are built, from a source file to a byte on the wire, and the tools underneath.](/docs/architecture) - [ContributingHow to work on optz itself, the site and the engine: where things live, how to check a change, and how a commit reaches production.](/docs/contributing) ## Speed - [optz vs Next.jsThe same site on optz and on Next.js 16, measured on one core, and a feature-by-feature comparison.](/docs/vs-nextjs) - [Would C be faster?Somewhat, on raw throughput: nginx (C) serving the same built files beat optz by 20-30% on pages in the run below. Not by an order of magnitude, and not in bytes sent. The…](/docs/native) - [MeasurementsWhere each number came from. Everything here was measured; estimates are labelled as such.](/docs/measurements) ## The $12 Server Challenge - [The Rust engineA social network backend in Rust on raw io_uring and SQLite for the $12 Server Challenge: architecture, optimizations and tests.](/docs/engine) - [Engine design notesThe engine's design document: the feed path, connections and memory, durability, and what changed while building it.](/docs/engine-design) ## Operations - [Deploymentdihlabs.lol runs on one $1.50 VPS: 1 vCPU, 2 GB of memory, Ubuntu 24.04. nginx in front, the site on Bun, the Rust engine next to it, all under systemd, updated from this…](/docs/deployment) - [Production filesEvery file in ops/: systemd units, the generated nginx config, deploys with rollback, verified backups.](/docs/ops) - [Restoring a backupBackups are in /var/backups/optz/feed-.db.gz, each with a .sha256 of the uncompressed file. Every backup already passed PRAGMA integrity_check before it was kept.](/docs/restore) --- # optz with AI Source: https://dihlabs.lol/docs/ai > Connect AI clients to an optz site over MCP, give your app its own MCP tools, and set up coding agents to work on an optz project. On this page - Try it: connect this site - Make your app an MCP server - Coding agents on an optz project It covers Claude, ChatGPT, Codex, Cursor, VS Code, Windsurf, Gemini CLI and any other client that speaks the Model Context Protocol (MCP). There are two separate integrations: | what it is | who uses it | where | **Your app as an MCP server** | `/mcp` on the running site: tools, prompts and resources from `app/mcp`, plus the site's pages | your visitors' AI assistants (Claude, ChatGPT, ...) | production, over HTTPS | **optz for coding agents** | `optz mcp` (project tools over stdio), `AGENTS.md`, agent skills | the agent writing your code (Claude Code, Codex, Cursor, ...) | your machine | ## Try it: connect this site dihlabs.lol is an optz app with MCP turned on. Its endpoint is: ``` https://dihlabs.lol/mcp ``` It has `search_pages` and `read_page` for every docs page and post, `lab_experiments` for the lab, and a `build_with_optz` prompt. Add it to your client, then ask something like "How do Server Actions work in optz? Cite the docs." client | how to add `https://dihlabs.lol/mcp` | **Claude** (claude.ai, desktop, mobile) | Settings → Connectors → Add custom connector → paste the URL | **Claude Code** | `claude mcp add --transport http dihlabs https://dihlabs.lol/mcp` | **ChatGPT** | Settings → Apps → Advanced settings → turn on Developer mode, then Create app with the URL (no authentication) | **Codex** (CLI, IDE, app) | `codex mcp add dihlabs --url https://dihlabs.lol/mcp`, or in `~/.codex/config.toml`: `[mcp_servers.dihlabs]` + `url = "https://dihlabs.lol/mcp"` | **Cursor** | `.cursor/mcp.json`: `{ "mcpServers": { "dihlabs": { "url": "https://dihlabs.lol/mcp" } } }` | **VS Code** (Copilot agent mode) | `.vscode/mcp.json`: `{ "servers": { "dihlabs": { "type": "http", "url": "https://dihlabs.lol/mcp" } } }` | **Windsurf** | `~/.codeium/windsurf/mcp_config.json`: `{ "mcpServers": { "dihlabs": { "serverUrl": "https://dihlabs.lol/mcp" } } }` | **Gemini CLI** | `.gemini/settings.json`: `{ "mcpServers": { "dihlabs": { "httpUrl": "https://dihlabs.lol/mcp" } } }` | **Anything else** | Streamable HTTP transport, no authentication. Inspect it with `npx @modelcontextprotocol/inspector` | Client menus move around; if a step above doesn't match, look for "connectors", "MCP servers", "apps" or "tools" in the client's settings. Prefer plain text? The same content is at [/llms.txt](https://dihlabs.lol/llms.txt) (an index of every page) and [/llms-full.txt](https://dihlabs.lol/llms-full.txt) (every page in one file), which you can paste into any chat. ### From code: the Claude API and OpenAI API Both APIs can call a remote MCP server for you during a request. Claude (Messages API, MCP connector beta): name the server in `mcp_servers` and enable its tools with an `mcp_toolset`: ``` curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: mcp-client-2025-11-20" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5-5", "max_tokens": 16000, "mcp_servers": [{ "type": "url", "url": "https://dihlabs.lol/mcp", "name": "dihlabs" }], "tools": [{ "type": "mcp_toolset", "mcp_server_name": "dihlabs" }], "messages": [{ "role": "user", "content": "What render modes does optz have? Cite the docs." }] }' ``` OpenAI (Responses API): add an `mcp` tool: ``` { "model": "gpt-5", "tools": [{ "type": "mcp", "server_label": "dihlabs", "server_url": "https://dihlabs.lol/mcp", "require_approval": "never" }], "input": "What render modes does optz have? Cite the docs." } ``` ## Make your app an MCP server Every file in `app/mcp` is one tool, prompt or resource; the file name is its name. optz serves them at `/mcp` from the same process as the site, on every deploy target (Bun, the single executable, edge workers). ``` // app/mcp/search_products.ts import { defineTool, ToolError } from "optz/mcp"; import { products } from "$lib/catalog"; export default defineTool({ title: "Search products", description: "Search the catalog by name. Returns id, name, price and URL. Use it before answering any question about what's for sale.", input: { type: "object", properties: { q: { type: "string", minLength: 2, description: "Words in the product name" }, maxPrice: { type: "number", minimum: 0 }, limit: { type: "integer", minimum: 1, maximum: 20, default: 5 }, }, required: ["q"], }, annotations: { readOnlyHint: true }, // q: string, maxPrice?: number, limit?: number: typed from `input` run: ({ q, maxPrice, limit }) => { const hits = products.filter((p) => p.name.toLowerCase().includes(q.toLowerCase()) && (maxPrice === undefined || p.price <= maxPrice)); if (!hits.length) throw new ToolError(`No product matches "${q}".`); return { products: hits.slice(0, limit).map((p) => ({ id: p.id, name: p.name, price: p.price, url: `https://shop.example/p/${p.id}` })) }; }, }); ``` What optz does for you: - **Validation.** Arguments are checked against `input` (types, `required`, `enum`, lengths, ranges, `pattern`) and defaults are filled in before `run`. A bad call returns an error the model can read and fix. - **Results.** Return a string (text), an object (JSON text plus `structuredContent`), or a full `{ content: [...] }` result. Throw `ToolError` for messages the model should see; other errors are logged and reported as "tool X failed" in production, so internals don't leak. - **Context.** Requests go through `app/middleware.ts`. Tools get `{ request, url, locals }` and can call `cookies()`, `headers()` and `after()` from `optz/server`. - **Pages for free.** With `mcp.pages` (the default), the pre-rendered pages are MCP resources and `search_pages` / `read_page` tools read them, so a content site needs no code at all. Each page is checked against your middleware with the caller's cookies and `Authorization` header, so pages it protects stay protected. - **Checked at build.** `optz build` loads every `app/mcp` file, fails on a bad definition, and prints the counts: `MCP 1 tool, 1 prompt`. Prompts and resources work the same way: ``` // app/mcp/compare.ts: a prompt the user picks in their client import { definePrompt } from "optz/mcp"; export default definePrompt({ description: "Compare two products", arguments: [{ name: "a", required: true }, { name: "b", required: true }], run: ({ a, b }) => `Look up ${a} and ${b} with search_products and compare them in a table.`, }); // app/mcp/product.ts: a resource template, product://{id} import { defineResource } from "optz/mcp"; export default defineResource({ uri: "product://{id}", mimeType: "application/json", read: ({ id }) => products.find((p) => p.id === id) ?? {}, }); ``` ### Config ``` // optz.config.ts mcp: { path: "/mcp", // default name: "shop", // default: site.url's host name instructions: "Tools for the Example Shop catalog. Search before answering.", pages: true, // default allowedOrigins: [], // extra browser origins; site.url's is always allowed }, llms: { exclude: ["/account"] }, // /llms.txt and /llms-full.txt (false turns them off) ``` Setting `mcp` turns the endpoint on even without an `app/mcp` folder; that's how this site works. `features.mcp: false` turns it off. The MCP code isn't loaded until the first request to `/mcp`. ### Authentication Read-only public tools need nothing. For private data, require a token in middleware and pass the user to tools through `locals`: ``` // app/middleware.ts export default defineMiddleware(async (ctx, next) => { if (ctx.url.pathname === "/mcp") { const token = ctx.request.headers.get("authorization")?.replace(/^Bearer /, ""); const user = token ? await userForToken(token) : null; if (!user) return new Response("Unauthorized", { status: 401, headers: { "WWW-Authenticate": "Bearer" } }); ctx.locals.user = user; } return next(); }); ``` Clients that take a header (Claude Code `--header`, Codex `bearer_token_env_var`, Cursor and VS Code `headers`) can send the token. Browser-based clients that need OAuth sign-in (claude.ai, ChatGPT) need an OAuth authorization server in front. ### How it's served - Streamable HTTP, **stateless**: each `POST /mcp` carries one JSON-RPC message (or a batch) and gets JSON back; no sessions and no event stream. That's what lets the endpoint run in an edge worker or behind any load balancer. - Protocol versions `2025-11-25`, `2025-06-18`, `2025-03-26` and `2024-11-05`. - An `Origin` header must be `site.url`'s origin (or in `mcp.allowedOrigins`), which stops web pages from reaching a local server through DNS rebinding. Non-browser clients send no `Origin`. - `GET /mcp` answers 405, which tells clients there's no server-to-client stream. Check it by hand: ``` curl -s https://dihlabs.lol/mcp -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search_pages","arguments":{"query":"islands"}}}' ``` ## Coding agents on an optz project `optz create` sets this up for new apps; in an existing one run: ``` bunx optz agents # or --only claude,codex,cursor,vscode,gemini ``` It writes: file | read by | `AGENTS.md` (an optz section; your own text is kept) | Codex, Cursor, VS Code Copilot, Gemini CLI, Windsurf, Zed, Jules and most others | `CLAUDE.md` with `@AGENTS.md` | Claude Code | `.claude/skills/optz`, `.claude/skills/optz-mcp` | Claude Code skills | `.agents/skills/optz`, `.agents/skills/optz-mcp` | Codex skills (and other Agent Skills clients) | `.mcp.json` | Claude Code (project MCP servers) | `.codex/config.toml` | Codex (trusted projects) | `.cursor/mcp.json`, `.vscode/mcp.json`, `.gemini/settings.json` | Cursor, VS Code, Gemini CLI | Each MCP config starts `bunx optz mcp`, an MCP server over stdio with tools for the project: tool | what it does | `optz_project` | pages with render modes, loaders and ISR, API routes and methods, Server Actions, islands, MCP tools, middleware, config | `optz_match_url` | which file answers a URL, with params, layouts, error and loading files | `optz_docs` | the framework reference, by section | `optz_scaffold` | a new page, dynamic page, API route, island, action, MCP tool or prompt, layout or middleware (never overwrites) | `optz_check` | `optz check`: route types and the type-check | `optz_build` | `optz build` (pre-renders static pages, so loader errors show up) | `optz_fetch` | a request to the running dev server, HTML as readable text | It also has two prompts, `optz_add_feature` and `optz_expose_to_ai`, and the skills as resources. The skills teach the agent optz's conventions (it isn't Next.js: no `"use client"`, islands instead), the cheapest render mode for each page, and "done means `optz check` and `optz build` pass". Re-run `optz agents` after upgrading optz: it updates its own parts and leaves the rest of each file alone. To use the skills in every project, copy them into your user folder: ``` cp -r node_modules/optz/skills/* ~/.claude/skills/ # Claude Code cp -r node_modules/optz/skills/* ~/.agents/skills/ # Codex ``` --- # Architecture and tech Source: https://dihlabs.lol/docs/architecture > How optz and this site are built, from a source file to a byte on the wire, and the tools underneath. On this page - The stack - A request, from the wire to the response - The build - Islands in the browser - Rendering and streaming - Caching, four layers - Server Actions - The dev server - Deploy targets - The framework's source ## The stack layer | technology | why | Runtime | [Bun](https://bun.sh) 1.4 | Native HTTP server, bundler, TypeScript and JSX without a build step, fast startup | UI | React 19 (server), Preact 11 or React in the browser | React's server renderer and streaming; Preact keeps islands at ~4 KB | Language | TypeScript, checked with TypeScript 7 (`tsgo`) | Typed routes, params, API responses and request bodies | Styling | Tailwind CSS v4 | Compiled to one hashed stylesheet at build time | Content | Markdown via `marked` | Blog posts and these docs, rendered at build time | Images | sharp (libvips) | AVIF/WebP resizing behind `/_optz/image` | Open Graph images | satori + resvg | JSX to SVG to PNG at build time | Backend (engine) | Rust, raw io_uring, SQLite (WAL) | The $12 Server Challenge entry: one core, 2 GB, as many users as possible | Serving | nginx in front, systemd units | TLS and buffering; config generated from `optz.config.ts` | Tests | `bun test`, Playwright (Chromium), Cloudflare `workerd` | Unit, in-process app and browser checks on every deploy target | ## A request, from the wire to the response ``` nginx :80/:443 ├─ /assets/* from disk (immutable, pre-compressed) └─ everything else → Bun on 127.0.0.1:3001 (optz start) handler.fetch(request) ├─ /assets/* in-memory file, no middleware ├─ proxy prefixes forwarded (config.proxy) └─ middleware chain app/middleware.ts └─ route() ├─ static page → memory lookup (brotli/gzip/ETag precomputed) ├─ client shell → memory lookup (one HTML for every param value) ├─ ISR page → memory lookup; stale? one background re-render ├─ dynamic page → React render, streamed (or from the response cache) ├─ API route → your handler (GET responses cacheable) └─ /_optz/action → a Server Action ``` The handler (`framework/src/handler.ts`) is web-standard code: a `fetch(Request) => Response` function. The Bun server, the single executable and the edge worker all run the same function; only how files and modules are loaded differs. Static pages are the common case and the cheapest: the build writes HTML plus `.br` and `.gz` copies; the server loads them into a map at startup with precomputed ETags and headers. Serving one is a map lookup and building a `Response`, about 3 µs of JavaScript per request. React's server renderer isn't even loaded unless a page renders per request. ## The build `optz build` (`framework/src/build.ts`): - **Scan** `app/routes` and `app/api` into a route table: params, catch-alls, layouts, error and loading boundaries, `@slot` folders, intercepting folders, `*.og.tsx` images. - **Generate types** (`.optz/types.d.ts`): every route and API module as a type, so ``, `PageProps` and `api()` are checked against files that exist. - **Bundle the browser code** with `Bun.build`: a ~2 KB entry (navigation, island loader) and one chunk per island. `"use server"` modules become fetch stubs. With `runtime: "preact"`, React imports are aliased to `preact/compat` in browser bundles only. - **CSS**: Tailwind compiles `app/styles.css`; self-hosted fonts are downloaded and their `@font-face` rules (plus size-matched fallbacks read from the font files) are appended. - **Prerender** every static page, client shell, prerendered API route and Open Graph image, by sending requests through the same handler that serves production. - **Write** `sitemap.xml`, `robots.txt` and `manifest.webmanifest` from `app/*.ts`, and compress every text file with brotli (quality 11) and gzip (level 9). The result is `dist/static` (servable as-is) and `dist/manifest.json` (routes, chunks, render modes, actions, redirects). ## Islands in the browser A page is HTML. Each `*.island.tsx` renders on the server inside an `` element with its props as JSON. In the browser, `framework/src/islands.ts` (no React) finds those elements, loads each island's chunk when it should (`load`, `idle`, or `visible` via IntersectionObserver) and hydrates just that element. React (or Preact) arrives with the first island; a page without islands never downloads it. Navigation swaps HTML instead of hydrating whole pages: a click fetches the next page (often already prefetched on hover), replaces `#__optz`, and hydrates the new islands. `refresh()` (called after Server Actions revalidate) does the same for the current page but keeps every island whose id and props didn't change mounted, so their state survives. ## Rendering and streaming `framework/src/render.tsx` builds the document. Static pages use React's `prerender` (every Suspense boundary resolved, no inline scripts). Dynamic pages use `renderToReadableStream`: the head and shell go out at once, async components stream in, and React's small inline scripts get a per-response nonce that the handler adds to your Content-Security-Policy after middleware ran. ## Caching, four layers - **Built files** in memory, pre-compressed, with ETags; hashed assets are `immutable`. - **Incremental static regeneration**: a static page past its `revalidate` time (or named by `revalidatePath`/`revalidateTag`) is served stale while one background render replaces it. - **The response cache** (`cache.ts`) for dynamic pages and API GETs: LRU bounded by entries and bytes, stale-while-revalidate, concurrent misses share one render, tags. - **nginx**: `optz nginx` adds `s-maxage`-aware proxying and per-prefix micro-caching. ## Server Actions At build time, every module under `app/` that starts with `"use server"` gets an id. Browser bundles get stubs that POST JSON (or FormData) to `/_optz/action//`; on the server the real module runs inside the request context (`cookies()`, `headers()`, `revalidatePath()`). The server also tags each action with React's `$$FORM_ACTION`, so `
` rendered on the server is a real form post that works without JavaScript, bound arguments included. Plain form posts must come from the same origin. ## The dev server `optz dev` runs a child process that builds (unminified, with React in a stable vendor bundle and Fast Refresh transforms) and serves. A file change restarts the child (~0.3 s). Open pages keep a server-sent-events connection; when the new build says hello, the page swaps changed stylesheets, re-imports changed island chunks and lets React Fast Refresh swap their code (state kept), then updates the page's HTML in place around them. ## Deploy targets target | how | Bun server | `dist/` + `optz start`; production runs it under systemd behind nginx | Single executable | `optz build --compile`: the Bun runtime, your code and every built file in one binary | Edge worker | `optz build --target edge`: one ES module with `export default { fetch }`, files embedded | Static | `optz export`: `dist/static` only | ## The framework's source file | role | `cli.ts` | `optz dev/build/start/check/export/nginx/create` | `routes.ts`, `match.ts` | File-system routes, slots and intercepts; matching shared with the browser | `build.ts`, `codegen.ts`, `manifest.ts` | The build, generated types, the manifest format | `handler.ts` | The request handler (everything per request) | `server.ts`, `static.ts`, `cache.ts` | Bun adapter, in-memory files, response cache | `render.tsx`, `runtime.tsx` | Document rendering and streaming; router, layouts, `Link`, hooks | `island.tsx`, `plugin.ts`, `islands.ts`, `hydrate.tsx` | Islands: server marking, browser runtime | `actions*.ts`, `action-client.ts`, `context.ts`, `server-api.ts` | Server Actions and `optz/server` | `image*.ts(x)`, `fonts.ts`, `og*.ts`, `seo.ts`, `script*.ts(x)` | Images, fonts, OG images, sitemap/robots, scripts | `server-bundle.ts` | Single executable and edge worker | `dev.ts`, `dev-client.ts`, `dev-vendor.ts` | Dev server updates and Fast Refresh | `nginx.ts`, `create.ts`, `types.ts`, `index.ts` | nginx config, starter app, public types and API | --- # Contributing Source: https://dihlabs.lol/docs/contributing > How to work on optz itself, the site and the engine: where things live, how to check a change, and how a commit reaches production. On this page - Setup - Checks before a commit - Benchmarks - Conventions - Adding a framework feature - From commit to production ## Setup ``` git clone https://github.com/sxnistxr/OPTZ && cd OPTZ bun install # the workspace: framework/, portfolio/, apps/* ``` One `node_modules` at the root (`bunfig.toml` sets the hoisted linker), so every app resolves the framework's dependencies and the framework itself (`optz` is `framework/src/index.ts`, no build step: Bun runs the TypeScript directly). ## Checks before a commit check | command | covers | Framework tests | `cd framework && bun test` | routing, the cache, islands, actions, fonts, SEO files, and `apps/features` end to end in-process | Framework types | `bun node_modules/typescript/bin/tsc --noEmit -p framework/tsconfig.json` | the framework's own types | App types | `bun run check` in each app | generated route types + the app's code | Feature browser checks | `node apps/features/e2e.cjs http://127.0.0.1:3002` | 22 checks in Chromium against a running build | Dev server checks | `node apps/features/hmr.cjs` | Fast Refresh, CSS swap, in-place updates (needs `bun run dev`) | Site browser checks | `node portfolio/e2e.cjs http://127.0.0.1:3001` | this site: navigation, docs, search, lab, strict CSP | Social demo | `node apps/social/e2e.cjs` | the feed over the engine (needs the engine running) | Engine | `cd portfolio/engine && cargo test --release` | unit tests; `tests/*.py` for protocol, crash and memory checks | Browser checks use Playwright with the preinstalled Chromium (`NODE_PATH=$(npm root -g)`). `e2e.cjs` also runs against the single executable and the edge worker (`UNOPTIMIZED=1` where there's no image library). ## Benchmarks script | measures | `bash bench/compare.sh` | optz vs the Next.js port: req/s, p99, memory, cold start, build time | `bash bench/native.sh` | optz vs the single executable vs nginx (C) on the same files | Both pin the server to one core with `taskset` and run `wrk` on the others. Numbers vary by about 5–10% between runs on a shared machine; compare within one run. ## Conventions - **Web-standard code in the request path.** `handler.ts` and what it imports must run on Bun, in `workerd` and in Node: no `Bun.*` and no `node:fs` there. Bun-specific code goes in `server.ts` (the platform object) or build-time modules. - **Nothing per request that can happen at build time.** New features check: does a static page pay for this? (Static requests skip the request context, middleware-free paths and header lookups on purpose.) - **Off when unused.** Heavy features load lazily (`import()`), and get a `features.*` switch when they cost memory or time. - **Strict CSP stays possible.** No inline scripts or styles in framework output; streaming uses nonces. - **Types are the API.** Public options live in `types.ts` with doc comments (they show on hover); generated types in `codegen.ts` keep routes and APIs checked. - **Comments say why**, at the top of each module and where code isn't obvious. ## Adding a framework feature - Types and config in `types.ts` (documented), defaults in `resolveConfig`. - Build-time work in `build.ts`, request-time work in `handler.ts` (portable). - A page or API in `apps/features` that uses it, and a check in `framework/test/features.test.ts` and/or `apps/features/e2e.cjs`. - The reference in `framework/README.md` (it's the [framework reference](/docs/framework) page). - Run the checks above and `bench/compare.sh` to make sure static pages didn't get slower. ## From commit to production ``` git push ──> VPS polls GitHub every 30 s (deploy/sync.sh, systemd timer) └─ deploy/post-deploy.sh ─> ops/deploy.sh 1. build the engine (Rust, as the unprivileged optz user, cached) 2. bun install --frozen-lockfile, optz check, optz build (the site) 3. generate nginx config from optz.config.ts 4. switch /opt/optz/current to the new release, restart services 5. health-check the engine and the site for 30 s, else roll back ``` A commit that fails type-checking or building never goes live; one that starts but isn't healthy is rolled back to the previous release. See [Deployment](/docs/deployment). --- # Deployment Source: https://dihlabs.lol/docs/deployment > dihlabs.lol runs on one $1.50 VPS: 1 vCPU, 2 GB of memory, Ubuntu 24.04. nginx in front, the site on Bun, the Rust engine next to it, all under systemd, updated from this… On this page - Production layout - First install - Every deploy - The domain and TLS - nginx - Other ways to run an optz app - Operations dihlabs.lol runs on one $1.50 VPS: 1 vCPU, 2 GB of memory, Ubuntu 24.04. nginx in front, the site on Bun, the Rust engine next to it, all under systemd, updated from this repository automatically. ## Production layout piece | where | runs as | nginx | `:80` (and `:443` with TLS) | `www-data` | The site (`optz start`) | `127.0.0.1:3001`, from `/opt/optz/current/web/portfolio` | `optz-web.service` | The Rust engine | `127.0.0.1:3000` | `optz.service` | Releases | `/opt/optz/releases/`, `/opt/optz/current` → the live one | | Data and backups | `/var/lib/optz` (SQLite), hourly verified snapshots | `optz-backup.timer` | Secrets | `/etc/optz/env` | root only | ## First install ``` ssh root@your-server # clone into /srv/optz and pull the branch every 30 s (BRANCH defaults to main) curl -fsSL https://raw.githubusercontent.com/sxnistxr/OPTZ/main/deploy/setup-vps.sh | sudo BRANCH=main bash sudo bash /srv/optz/ops/install-prod.sh # users, toolchains (pinned, checksummed), units, first deploy ``` `install-prod.sh` installs Bun 1.4.4 and the Rust toolchain from pinned, checksummed archives, creates the `optz` user and directories, writes `/etc/optz/env` with a random secret, installs the systemd units and runs the first deploy. ## Every deploy `ops/deploy.sh` builds the commit, switches releases and health-checks them; it rolls back by itself if the new release isn't healthy within 30 seconds, and keeps the five newest releases. Follow it with `journalctl -u optz-sync.service -f`. The site build downloads its fonts from Google at build time and renders Open Graph images, so the server needs outbound HTTPS while building (not while serving). Downloads are cached in `~/.cache/optz`, so later builds only fetch what changed. ### Behind Cloudflare The site sends `Cache-Control: s-maxage=300, stale-while-revalidate=86400` on pages (`server.cdnMaxAge` in `optz.config.ts`), so a CDN in front serves them for five minutes and refreshes in the background. To clear Cloudflare's copy on every deploy, add a zone id and an API token with the "Cache Purge" permission to `/etc/optz/env`: ``` CF_ZONE_ID=... CF_API_TOKEN=... ``` `deploy.sh` purges the zone after the new release is live. Without them it skips the purge, and pages update at the CDN within five minutes. ## The domain and TLS Point `dihlabs.lol` (and `www`) at the server with A records, then get a certificate. Use `certonly`: the nginx config is generated on every deploy, so certbot shouldn't edit it. ``` sudo apt install certbot python3-certbot-nginx sudo certbot certonly --nginx -d dihlabs.lol -d www.dihlabs.lol --deploy-hook "systemctl reload nginx" sudo bash /srv/optz/ops/deploy.sh # regenerates nginx with HTTPS; "already live" is fine ``` Once `/etc/letsencrypt/live//` exists, every deploy generates the config with HTTPS (HTTP/2, HSTS) and a redirect from port 80. certbot renews the certificate by itself and the deploy hook reloads nginx. The site's absolute URLs (canonical links, Open Graph, sitemap) come from `url` in `portfolio/content/site.json`, or the `SITE_URL` environment variable. ## nginx The config is generated, not hand-written: `optz nginx` turns `optz.config.ts` into a server block (hashed assets from disk with `gzip_static`, everything else to Bun with keep-alive, and micro-caching for proxied APIs). The deploy regenerates and reloads it when it changes. ## Other ways to run an optz app | command | notes | Single executable | `optz build --compile` → `dist/server` | copy one file and run it; sharp is used if it can be found | Edge worker | `optz build --target edge` → `dist/edge/` | `npx wrangler deploy` from there (Cloudflare), or Deno Deploy / Vercel Edge | Static host | `optz export` → `dist/static` | no server features | ## Operations - Logs: `journalctl -u optz -u optz-web -f` - Restart: `sudo systemctl restart optz optz-web` - Memory per connection on the box: `bash /srv/optz/ops/vps-mem.sh` - Restore a backup: `ops/RESTORE.md` --- # The Rust engine Source: https://dihlabs.lol/docs/engine > A social network backend in Rust on raw io_uring and SQLite for the $12 Server Challenge: architecture, optimizations and tests. On this page - Architecture - Optimizations, and why - Rules - Tests Lives in `portfolio/engine/` in this repo (the social demo in `apps/social` is its frontend); `submissions/rust-iouring-sxnistxr` links here. Copy this folder as-is for a challenge PR. **Direct on `0.0.0.0:80`, no Nginx.** The server handles every keep-alive connection itself. | | Language | Rust 1.99.1 (stable, edition 2024), pinned in `rust-toolchain.toml`; production installs it from the official standalone tarball, SHA-256 pinned in `install.sh` (no rustup, nothing piped to a shell); elsewhere rustup picks it up from the toolchain file | Database | SQLite 3.54.1, official amalgamation built from source: `fetch-sqlite.sh` downloads it into `vendor/` (git-ignored), checked against `vendor.sha256` | I/O | `io-uring` 0.7.15 (raw io_uring: no async runtime, no HTTP framework) | Other crates | `libc` 0.2.191, `httparse` 1.10.1, `sha2` 0.11.1 + `hmac` 0.13.0. Dev-only: `serde_json` 1.0.154, for differential tests. Exact pins in `Cargo.toml`, checksums in `Cargo.lock` | Kernel | Linux ≥ 6.4 (multishot timeouts); built and tested on 6.18, and on Ubuntu 24.04's 6.8 via the VPS | ``` sudo bash install.sh # once, as root bash build.sh # as a normal user SQLITE_PATH=... JWT_SECRET=... HOST=0.0.0.0 PORT=80 bash start.sh # foreground ``` `start.sh` is a single `exec`. It never restarts the server itself, so systemd sees every crash. The server reads only the four variables in [SPEC.md](https://github.com/sxnistxr/OPTZ/blob/main/portfolio/engine/SPEC.md), which also lists every route, status and error. ## Architecture Two threads, sized for one vCPU: - **Network thread.** Accept, receive, HTTP parsing, JWT verification, every read query, response formatting and send, all driven by io_uring. - Sockets are io_uring direct descriptors in each ring's fixed-file table, not process fds, so `LimitNOFILE=65535` doesn't cap connections. A table can't exceed RLIMIT_NOFILE, so connections spill over up to 8 rings (524,280 slots), all driven by the same thread. Ring 0 holds a multishot poll on every other ring's fd. - Receives are multishot with a provided-buffer ring: an idle connection owns no buffer, just a 32-byte slot. - **Writer thread.** Owns the only read-write SQLite connection. Write jobs arrive over a lock-free SPSC queue. The writer drains everything pending into one `BEGIN IMMEDIATE` transaction and commits (`synchronous=FULL`: the WAL is fsynced). Only then are the results handed back (SPSC queue + eventfd watched by io_uring) and the 201/200 replies sent. Reads use a separate connection, so a read can never see an uncommitted write. Reads within one event-loop batch share one read transaction, and the loop ends it before waiting. A client only sends its next request after receiving its reply, and that reply was sent after commit, so it always sees its own writes. The pipelined create-then-feed test checks this. ## Optimizations, and why Every number below was measured. The full log, including rejected ideas, is in [BENCHMARKS.md](https://github.com/sxnistxr/OPTZ/blob/main/portfolio/engine/BENCHMARKS.md). Local numbers come from a 4 vCPU Xeon with the server pinned to one CPU, over loopback. They're for before/after comparisons, not capacity claims. - **`SQLITE_ENABLE_QPSG` (query planner stability).** perf found SQL being re-parsed on every single-post request: SQLite marked the statement for re-planning whenever `?1` changed. With plans fixed at prepare time: **47.8k → ~108k req/s** for single posts (18.4 → 9.1 µs), and the mixed workload 32.7k → ~45k. A test asserts zero re-prepares. - **Feed range path.** The newest 20 ids are almost always 20 consecutive ids. One rowid range scan fetches the rows, and one covering scan of `likes_post_id_idx` is counted in Rust. That costs 14.2–14.6 µs per feed vs 19.7–20.4 µs for the reference query. Counting in Rust beat `GROUP BY` (8.0 µs) and 20 point counts (10.2 µs). Sparse spans, or a row with a missing author, fall back to the exact query, and a test asserts both paths produce identical bytes. - **Responses written straight into the send buffer.** Header space is reserved first, the body written after, and `Content-Length` filled in at the end, so there's no second pass and no per-request allocation on the read path. - **Word-at-a-time JSON escaping.** It checks 8 bytes per step and only inspects bytes in a word that needs escaping (~3 % on feed). - **Single-shot accepts, counted against free slots.** With multishot accept, a full fixed-file table made the kernel accept and then drop a connection (measured: 4 lost out of 5,000 when spilling across shards). Keeping 16 single-shot accepts in flight, each with a slot reserved, means a table never overflows. - **Group commit + one statement per like.** `INSERT … SELECT … WHERE EXISTS … ON CONFLICT DO NOTHING` handles the common case in one statement; `changes()` tells 201 from 200/404. - **SQLite PGO on synthetic data.** `build.sh` profiles SQLite on a randomly generated database (never the seed) and rebuilds it: feed −7 %, mixed −5 to −12 % in every A/B pair. It falls back to the plain build if any step fails; `bash build.sh --no-pgo` skips it. - **Memory-budget admission control.** The connection ceiling comes from MemTotal and the measured cost per idle connection (~3.6–4.9 KB of kernel + app memory), plus a once-a-second MemAvailable floor read with `pread` on an fd opened at startup. Past either, new connections are closed at once. A refused connection costs one request; an OOM kill costs the run. Rejected after measurement (details in BENCHMARKS.md): adaptive completion batching, fixed `SO_RCVBUF`/`SO_SNDBUF`, fetching feed rows via the created_at index walk. ## Rules - SQLite only, in-process, on `$SQLITE_PATH`. Schema untouched. - No caching across requests: every request reads SQLite while it's served, and every JWT signature is verified. Only the HMAC key schedule is precomputed from the secret. - Writes are durable before responding: WAL + `synchronous=FULL`, group commit, no reply before `COMMIT` returns. A failed commit turns that batch's replies into 500s. A statement error that aborts the transaction fails only its own job: the batch is replayed without it. - Tokens without `exp`, `alg` other than HS256, bad base64url, or extra segments are rejected. - No kernel tuning, no CPU pinning. In-process only: the soft RLIMIT_NOFILE is raised to the hard limit, and options are set on the listening socket (inherited by accepted sockets). - Never crashes on EMFILE/ENFILE/ENOMEM/ENOBUFS: accept errors pause until a connection closes (or the 1 s tick), ENOBUFS re-arms the receive, a send that fails transiently is retried, and a ring that can't take more submissions (EAGAIN/EBUSY) is drained and retried without spinning. Error paths don't open files. - Writes queued behind a stalled writer are bounded (16,384 jobs or 32 MiB): past that, 503. - Memory admission follows the cgroup's limit (systemd `MemoryMax=`) when it's below the host's. - Foreign keys are enforced: a validly signed token for a user that doesn't exist writes nothing (401 `unknown user`). - Idle keep-alives stay open indefinitely (≥ 65 s); stalled partial requests or sends are closed after 120 s. SIGTERM: stop accepting, drain in-flight work (≤ 5 s), `wal_checkpoint(TRUNCATE)`. ## Tests From a clean clone (rustup installs the pinned toolchain on first use): ``` ./fetch-sqlite.sh # vendor/sqlite3.c, checksum-verified cargo fmt --check && cargo clippy --all-targets -- -D warnings cargo test --release # 27 unit tests (+2 needing a seed: TWELVE_SEED=... -- --ignored) python3 -c 'import sys; sys.path.insert(0, "bench"); import pgo_train; pgo_train.synth("/tmp/seed.db")' # a synthetic SEED (20k users, 100k posts) bash ../../test/run.sh submissions/rust-iouring-sxnistxr # official 42 checks python3 tests/protocol.py --seed SEED [--long] # 14 checks: pipelining, partial input, limits, 2,000 sockets, unknown users, 66 s idle python3 tests/crash.py --seed SEED # kill -9 during writes, then verify every acked write python3 tests/shards.py --seed SEED # spill-over across rings, admission ceiling python3 tests/connmem.py --seed SEED --conns N # kernel + app memory per idle connection ``` Results so far: 42/42; all unit tests, including a 300k-case differential test of the JSON reader against serde_json; `kill -9` ×10 under write load, with **0 of 110,768 acknowledged posts and 0 of 55,062 acknowledged likes lost**; official `load.js` at 12,000 users locally: p95 2.0 ms, p99 5.9 ms, 0 errors, 34 MB RSS. **Validated on an Ubuntu 24.04 VPS (1 vCPU Cascade Lake without SHA-NI, 2 GB, kernel 6.8, like the scoring box):** 42/42, all unit/protocol/shard tests, `kill -9` ×5 with 0 acknowledged writes lost, and **4.5–4.6 KB of kernel + app memory per idle connection**. That puts the memory ceiling near 320–330k connections on a 2 GB box; 500k would need ~2.3 GB for sockets alone. The VPS found two kernel-6.8 behaviours the 6.18 dev box hides, both fixed: completions on extra rings didn't wake a thread blocked on ring 0 (now a bounded 1 ms wait when several rings are live), and closing a direct descriptor is deferred while older requests are in flight (now a hard-linked `shutdown` precedes every close). **Not yet measured:** capacity under load.js with an external load generator. --- # Engine design notes Source: https://dihlabs.lol/docs/engine-design > The engine's design document: the feed path, connections and memory, durability, and what changed while building it. On this page - 0. What reading the repo changed - 4a. SQL and the feed path - 4b. Connections and memory - 4c. Durability and operations - 4d. Testing and verification - Implementation plan - Decisions I need from you - What changed during implementation Status: **implemented.** The decisions below were made without further sign-off, as instructed. See "What changed during implementation" at the end. Sections 1–3 of the prompt are approved and not repeated here, except where reading the repo changed a detail. ## 0. What reading the repo changed Facts from `twelve-dollar-server-challenge@1349c1c` that shape the design: Fact | Source | Consequence | Request mix per user iteration: feed 1, post 1, like 0.15, create 0.02. That is **46.1 % feed, 46.1 % single post, 6.9 % like, 0.9 % create**. | `bench/load.js` | Feed and single post are 92 % of requests. Auth is only 7.8 %, so HMAC speed barely matters (≈1 µs per verify without SHA-NI × 7.8 % ≈ 0.08 µs per average request). I'll use RustCrypto `sha2`+`hmac` unless a benchmark shows a big gap, and spend effort on the read path instead. | 500k users ≈ 24.4k iterations/s ≈ **53k req/s, ~4.1k writes/s, ~490 new posts/s**. | arithmetic on `load.js` | At that rate the 20-post feed turns over every ~40 ms, so the newest 20 ids are nearly always contiguous and the range path is the common case. Each recent post gets few likes. | Like requests send `Content-Type: application/json` with an **empty body**. | `load.js` | The parser must accept `Content-Length: 0` POSTs, and the like route must not try to parse JSON. | The seed feed body is **5,102 bytes**, and the newest 20 seed ids are contiguous (500000…499981). Bodies written under load are ~70 characters. | `test/golden/feed.json` | Most feed responses are 3–5 KB, so send buffers come from 8 KB slabs, with a rare oversized fallback. | The leader measured `GROUP BY` like counts **−16 % on feed** and kept a plain range scan counted in Rust. | leader `RESEARCH.md` | I'll benchmark three variants (§4a) rather than assume. | The leader inserts then runs `SELECT created_at`, rather than using `RETURNING`, which builds a temp result table. | leader `db.rs` | I'll measure both (§4a). | Flagged in other entries: tokens without `exp` accepted, reads that see uncommitted writes, restart loops in `start.sh`, **unchecksummed rustup**, and a crash on EMFILE because the error path opened stderr. | `LEADERBOARD.md` | Each is a named test or rule here. The toolchain comes from a pinned standalone Rust tarball checked against a SHA-256 written into `install.sh`, with no rustup at all. | The leader was OOM-killed at 360k/480k. Its cgroup peak was 450 MB at 100k users locally and 630 MB at 264k on the scoring box. | leaderboard, leader `IO_URING.md` | Memory, not CPU, is the first wall past ~300k (§4b). | **Measured here (not on the scoring box):** kernel 6.18, loopback, 18,000 idle established sockets after one request/response each: **slab grew 4.1 KB per socket** (`/proc/meminfo` Slab delta; script: `docs/tools/sockmem.py`). At that cost, 500k sockets need ~2.0 GB of kernel memory alone, which is more than the 1,967 MB box. Ubuntu 24.04 (kernel 6.8) may differ, so this must be re-measured on a 6.8 box before setting the admission ceiling. **Until then I treat 500k as probably memory-infeasible with one keep-alive socket per user, and the realistic ceiling is somewhere between ~330k (at 4.1 KB/socket) and ~500k (at ≤2.5 KB/socket).** ## 4a. SQL and the feed path All statements are prepared once at startup with `SQLITE_PREPARE_PERSISTENT`. Reads run on the read connection; writes run on the writer connection. ### Feed (46 % of requests) ``` -- F1: newest 20 ids, walking posts_created_at_id_idx (covering: rowid is in the index) SELECT id FROM posts ORDER BY created_at DESC, id DESC LIMIT 20; -- F2 (range path): rows for the id span, rowid range scan on posts + user lookup SELECT p.id, p.body, p.created_at, u.username FROM posts p JOIN users u ON u.id = p.user_id WHERE p.id BETWEEN ?1 AND ?2; -- F3 (range path): like counts, covering scan of likes_post_id_idx SELECT post_id FROM likes WHERE post_id BETWEEN ?1 AND ?2; -- F_exact (sparse fallback): the reference query SELECT p.id, p.body, p.created_at, u.username, (SELECT count(*) FROM likes l WHERE l.post_id = p.id) FROM posts p JOIN users u ON u.id = p.user_id ORDER BY p.created_at DESC, p.id DESC LIMIT 20; ``` - **Range path** when `max(id) − min(id) < 64`. Under load the span is 19, so this is the common case. F2 returns at most 64 rows, and rows whose id isn't in the F1 set are skipped. F3 counts into a 64-entry array indexed by `post_id − lo`. Output follows F1's order, so ties and any created_at/id disagreement come out exactly as the reference query would. - **Exact fallback** for sparse spans: still correct, only slower. - **Consistency:** F1–F3 run inside one read transaction, so they see one snapshot and the post set, rows and counts all agree. - **Variants to benchmark for F3** (seeded DB + a synthetic churn DB): (a) F3 counted in Rust, (b) F3 with `GROUP BY post_id`, (c) 20 point `count(*)`s on a prepared `SELECT count(*) FROM likes WHERE post_id=?`. The cheapest by measurement wins, and all results go in BENCHMARKS.md. - **Output without intermediate allocation:** `sqlite3_column_text` pointers die on the next `step`, and output order (F1) can differ from scan order (F2). So F2 rows are copied once into a per-thread scratch arena that is reused and never freed. The JSON is then written into the connection's send slab in F1 order, escaping in the same pass. If F2 rows arrive in exactly the reverse of F1 order (the normal case), I'll test writing straight from column pointers with no scratch copy at all, scanning `ORDER BY p.id DESC` and checking each id against F1. The scratch copy stays as the fallback. - Integer formatting is hand-written, or the `itoa` crate if it measures faster. `created_at` and `username` are checked once to be JSON-safe, which they always are, using the same escaper's fast path. ### Single post (46 %) ``` SELECT p.id, p.body, p.created_at, u.username, (SELECT count(*) FROM likes l WHERE l.post_id = p.id) FROM posts p JOIN users u ON u.id = p.user_id WHERE p.id = ?1; ``` This is one statement and one step, written directly from column pointers. The id parse is strict: ASCII digits only, no sign, no leading `+`, value 1..=2^63−1, and anything else gets `invalid post id`. Ids above 2^53 are out of scope, but they still parse correctly. ### Like (6.9 %): writer thread ``` -- L1: one statement on the hot path; inserts only if the post exists INSERT INTO likes(user_id, post_id) SELECT ?1, ?2 WHERE EXISTS (SELECT 1 FROM posts WHERE id = ?2) ON CONFLICT(user_id, post_id) DO NOTHING; -- L2: only when sqlite3_changes() == 0, to tell "already liked" (200) from "no such post" (404) SELECT 1 FROM posts WHERE id = ?1; ``` `changes == 1` gives 201 with `already_liked:false`. Otherwise L2 decides between 200 `already_liked:true` and 404. This is equivalent to the prompt's "check existence, then `ON CONFLICT DO NOTHING`", with one statement fewer on the common path. `user_id` comes from the token's `sub`, unchecked against `users`, as the spec says it isn't looked up. Whether to enable `foreign_keys` is an open choice. It's off by default, and then a `sub` with no user row still inserts a like. The leader turns it on, which makes that case a 500. All seeded tokens name existing users, and I'll test both settings. ### Create (0.9 %): writer thread Candidates, measured: (a) `INSERT INTO posts(user_id, body) VALUES (?1, ?2) RETURNING id, created_at` (b) the same INSERT, then `sqlite3_last_insert_rowid` and `SELECT created_at FROM posts WHERE id = ?`. `created_at` always comes from the column default, never from application time. Body validation (JSON parse, trim, 1..=500 characters counted as Unicode scalar values, matching SQLite `length()` on TEXT) happens on the network thread before the job is queued, so the writer only does SQL. ### Health `SELECT 1` on the read connection, run on every request. If it fails, return 503 with `sqlite3_errmsg` as the `error` string, JSON-escaped. ### SQLite build and pragmas - Built from the official amalgamation, version pinned and SHA-256 verified, with `SQLITE_THREADSAFE=2 SQLITE_DEFAULT_MEMSTATUS=0 SQLITE_OMIT_DEPRECATED SQLITE_OMIT_SHARED_CACHE SQLITE_LIKE_DOESNT_MATCH_BLOBS SQLITE_DQS=0 SQLITE_DEFAULT_WAL_SYNCHRONOUS=2 SQLITE_OMIT_LOAD_EXTENSION SQLITE_OMIT_PROGRESS_CALLBACK HAVE_FDATASYNC=1 SQLITE_USE_ALLOCA`. - Both connections: `journal_mode=WAL` (already set in the seed; asserted), `synchronous=FULL`, `mmap_size=1 GiB` (covers the ~250 MB seed plus growth; mmap pages are reclaimable page cache), `temp_store=MEMORY`, `cache_size=−8192` (8 MB, tuned by measurement). - Reader: `query_only=1`. Writer: `wal_autocheckpoint=0` (checkpoints are explicit, §4c), `journal_size_limit=64 MB`. - **PGO of SQLite:** a later, separately measured step, trained on a synthetic database generated at build time (no seed data). It only ships if BENCHMARKS.md shows a win. ## 4b. Connections and memory ### Rings and connection table - **8 shard rings + 1 master ring, all driven by the network thread.** Each shard registers a sparse fixed-file table of 65,535 slots (`IORING_REGISTER_FILES_SPARSE`; the table is capped at RLIMIT_NOFILE), giving 524,280 slots in all. The master ring holds a multishot `POLL_ADD` on each shard ring's fd, plus the writer's eventfd, a signalfd and a 1 s timer. When the master reports a shard ready, the thread reaps that shard's CQEs in a batch. - Accept: **one listening socket**, with multishot direct accept armed on exactly one shard at a time. When that shard's table fills, accept moves to the next shard with free slots. I'll prototype this against 8 × `SO_REUSEPORT` listeners and keep whichever costs less CPU per accepted connection. The single-armed design is the default because it never refuses a connection while another shard has room. - **The connection slot (≤ 64 bytes, idle connections hold nothing else):** generation u32, state u8, flags u8, deadline u16 (coarse seconds, wrapping), input handle u32 (0 = none), output handle u32, output offset/length u32, intrusive deadline-list links 2 × u32, user id u64 copy for in-flight writes. That is about 40 bytes, padded to 64 for cache alignment. 524,280 slots × 64 B = 33.5 MB, allocated lazily in 4,096-slot chunks so a lightly loaded server stays small. - `user_data` on every SQE = shard (3 bits) | slot (17 bits) | generation (32 bits) | op (4 bits). A completion whose generation doesn't match the slot's is a stale CQE for a reused slot. Its buffers are returned and the CQE is otherwise ignored. ### Buffers - **Receive:** one provided-buffer ring per shard, 1,024 × 2 KB (16 MB total). A k6 request is 150–450 bytes, so one buffer is almost always a whole request. It's parsed in place and handed back to the ring in the same batch. A request split across completions is copied into a pooled 4 KB "partial input" block, the only receive state an idle connection can ever hold, and freed when the request completes. Requests over 16 KB are out of scope: answer 413 or close. - **Send:** a slab pool of 8 KB blocks, bounded (default 8,192 blocks = 64 MB, sized from the memory budget below). A rare oversized response (20 maximal 500-character bodies with escapes) takes a heap allocation from a bounded counter. A slab returns to the pool on its final send completion. Idle connections hold no send buffer. - **ENOBUFS** on multishot recv (the provided ring ran dry): put the connection on a bounded re-arm queue that's drained after buffers are returned in the same loop iteration. Never spin, and never allocate. ### Per-socket kernel buffers - `SO_RCVBUF`: set small (start at 4 KB, then measure the smallest that doesn't add round trips). That turns off receive autotuning and bounds the worst-case receive memory per socket. - `SO_SNDBUF`: set to hold one maximal normal response (start at 16 KB; the kernel doubles it) so that a feed goes out in one `send`. - Neither reduces the **fixed** cost of an idle socket (`tcp_sock`, inode, file, dentry), which dominates at scale. They only cap bursts, so I'll measure them, not assume a gain. ### Memory-budget admission control Two layers, both computed without allocating or opening files on the hot path: - **Static ceiling** at startup: `max_conns = (MemTotal − reserve) / per_conn_cost`. `reserve` covers the OS (~200 MB), the app (rings, slabs, connection table) and a minimum page cache for SQLite's hot set. `per_conn_cost` is the **measured** kernel+app cost per idle connection on Ubuntu 24.04 (6.8), compiled in as a constant with a comment pointing to the measurement. It's a constant, not configuration, so the env-only rule holds. - **Dynamic floor**, every second on the 1 s timer: `pread` `/proc/meminfo` through an fd opened at startup. If `MemAvailable` drops below a floor (default 96 MB), stop arming accept and fast-close new connections (`SO_LINGER{1,0}` then close) until it recovers past floor + 32 MB. Existing connections are never killed. A refused connection is one k6 error; an OOM kill fails the whole run. ### Backpressure and timeouts - While a connection has unsent output, its recv is paused (the multishot recv is cancelled or not re-armed), so pipelined input can't pile up responses. - Deadlines exist only for connections that hold a partial request or a pending send (120 s). Those sit on an intrusive deadline list swept by the 1 s timer. Idle keep-alive connections are on no list and are never closed by the server (well above the 65 s minimum). - Errors: EMFILE/ENFILE/ENOMEM/ENOBUFS from accept, recv and send are counted in fixed atomic counters and handled in place: re-arm later, close the connection, or pause accept. There is no logging on hot error paths. Stderr is written only with a preformatted, rate-limited `write(2)` on fd 2, which is already open, so the error path never opens a file (the C# failure mode). ## 4c. Durability and operations ### Write path - The network thread validates the request and pushes `Job { slot ref, kind, user_id, post_id | body span }` into a fixed-capacity SPSC ring (lock-free, 4,096 entries). Bodies are copied into a job-owned 512 B block. If the queue is full, the connection's input is paused until there is room. The queue never drops a job. - **Writer loop:** - Block on a futex/eventfd until there are jobs. - Drain everything pending (cap 1,024 per batch). - `BEGIN IMMEDIATE`, then run each job's statement(s). - `COMMIT`. In WAL with `synchronous=FULL`, this fsyncs the WAL before it returns. - Push results to the reply SPSC ring and `write` the eventfd that the master ring watches. - The network thread then formats and sends the 201/200/404 replies. **No reply exists before step 4 returns success.** - **SAVEPOINT per job: proposed change.** Each job is a single write statement (L1, or the create INSERT), and SQLite already rolls back a single failed statement without aborting the transaction. So a savepoint adds two statements per job for no extra isolation. My proposal: no savepoints. If a statement failure does abort the transaction (`sqlite3_get_autocommit` turns true: IOERR, FULL, NOMEM), the batch's earlier jobs are re-run in a fresh transaction once, and if that fails too, all of them get 500. Savepoints come back if a job ever needs more than one write. **This deviates from §3 and needs your OK.** - A failed `COMMIT` → `ROLLBACK`, and every tentative reply in that batch becomes 500 `internal server error`. - **Read-your-writes:** a client only sends its next request after receiving the reply, and the reply is only sent after commit. The network thread opens a fresh read transaction per completion batch and always ends it at the end of the batch. So any read the client sends after a 201 starts a snapshot that includes the commit. A read never sees an uncommitted write, because it runs on the other connection. ### Checkpoints - With `wal_autocheckpoint=0`, after each commit the writer checks the WAL frame count. Past 4,096 frames (~16 MB), it runs `wal_checkpoint(PASSIVE)` on the writer thread. Reads continue on the network thread, and writes simply wait for the next batch. - Read transactions last one completion batch, so they never pin the WAL for long, and the WAL restarts from the beginning on the next write after a complete checkpoint. If the WAL still passes 64 MB, a `RESTART` checkpoint with a 50 ms busy timeout forces it. - The checkpoint threshold is tuned by measurement. A checkpoint stalls writes but not reads, which is the point of the two-thread split. ### Signals, startup, start.sh - SIGTERM/SIGINT go through a signalfd on the master ring. Shutdown: stop arming accept, finish the in-flight write batch and send its replies, stop taking new jobs (new writes get 503), close sockets, then the writer runs `wal_checkpoint(TRUNCATE)` and both connections close. The whole thing is bounded at 5 s, after which the process exits with unsent replies dropped. Acknowledged writes are already durable either way. - Startup: open the database, set pragmas, prepare statements, no table scans. WAL recovery happens on first access. The target is healthy in under 1 s on the seed. - `start.sh` is a single `exec` of the binary with nothing looping, so systemd sees every crash. - `/health` really runs `SELECT 1` on every request. ### Production extras in ops/ (outside the submission) - `optz.service`: `User=optz`, `Restart=on-failure`, `LimitNOFILE=65535`, `AmbientCapabilities=CAP_NET_BIND_SERVICE`, `MemoryMax=1800M` / `MemoryHigh=1700M`, `ProtectSystem=strict`, `ReadWritePaths=`, `NoNewPrivileges=yes`, `PrivateTmp=yes`, `ProtectHome=yes`, `RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX`, `SystemCallFilter=@system-service io_uring_setup io_uring_enter io_uring_register` (to be verified on the box). - `optz-backup.service` + `.timer` (hourly): the `sqlite3` CLI, built from the same amalgamation, runs `VACUUM INTO` (or `.backup`; whichever holds the WAL lock for less time) into a dated file. Then `PRAGMA integrity_check` on the copy, zstd compression, and keeping 24 hourly and 14 daily copies. The unit fails loudly if the check fails. - Off-box replication: I'll check whether Litestream is still maintained (late 2026) before recommending it. Otherwise `rclone` the verified backups to object storage. - `RESTORE.md`: stop the service, verify the backup's checksum and integrity, replace the database (and remove stale `-wal`/`-shm`), start, check `/health` and the newest post id. - journald: `SystemMaxUse=200M` drop-in. The server logs only startup, shutdown and rate-limited errors, never per-request lines. - The sync timer already on your VPS (`deploy/`) becomes the deploy path: `post-deploy.sh` builds as the service user and restarts the unit, but only if the build succeeds. ## 4d. Testing and verification Layer | What | Pass criterion | Official | `test/run.sh submissions/rust-iouring-sxnistxr` on a fresh seed | 42/42, output pasted into BENCHMARKS.md | Unit: HTTP | byte-at-a-time partial input, pipelining (3 requests in one buffer), oversized header/body, garbage, `Content-Length` vs missing, lowercase headers, `Connection: close` | exact parse/close decisions | Unit: JWT | valid; bad signature; expired; **missing exp**; `alg:none`; `alg:HS512`; header not JSON; 2 or 4 segments; bad base64url (padding, `+/`); `sub` "abc"/"0"/"-1"/number; `username` missing/number | each lands in the right one of the three 401 messages | Unit: JSON | escapes (`\" \\ \/ \b \f \n \r \t \uXXXX`, surrogate pairs, lone surrogates rejected), raw control characters rejected, trailing garbage, duplicate keys (last wins, as `JSON.parse` does), trim with Unicode whitespace vs ASCII (matching JS `String.trim`, as the reference uses), the 500-character boundary counted in scalar values | **differential against `serde_json`** (dev-dependency) on generated inputs | Unit: ids, writer | id parsing table; writer batch with one failing job; forced COMMIT failure → all 500 | exact | Crash / power loss | write load with 200 connections → `kill -9` the server mid-batch (at random times, ×20) → restart → every id that got a 201 exists with its body, and every acknowledged like row exists | 0 missing over all rounds. A separate VM-level test (hard reset of a VM, if available) is the only real power-loss check, and I'll label it as such | Load: official | `k6 run bench/load.js` (k6 pinned and checksum-verified) | p95/p99/error thresholds, with RSS and cgroup memory recorded | Load: connections | a small Rust client opening 100k/250k/500k idle keep-alive connections from many 127.x.y.z source addresses, one request each, then idle | server-side memory per connection (cgroup `memory.current` + slab delta), measured | Accounting | µs/request = server CPU time (`/proc/pid/stat` utime+stime, or cgroup `cpu.stat`) ÷ requests; RSS; cgroup memory | recorded for every change in BENCHMARKS.md, including rejected ones | **What this container can and can't measure** (checked): 4 vCPUs (Xeon 2.1 GHz **with** SHA-NI, unlike the scoring box), 16 GB, kernel 6.18, io_uring works, **fd hard limit 20,000**, no cgroup controllers. So here I can do correctness, unit, differential and crash tests, plus single-core CPU comparisons via `taskset` on the load-generator side only. **I can't do** 65,535+ connections, Ubuntu 6.8 kernel memory, cgroup memory accounting, or anything like the scoring numbers. Those need a 1 vCPU / 2 GB Ubuntu 24.04 box and a separate load-generator machine, and every number in BENCHMARKS.md will be labelled with where it was measured. ## Implementation plan Small commits, each one passing its tests. - **Skeleton** (`submissions/rust-iouring-sxnistxr/`): `install.sh` (pinned standalone Rust tarball, SHA-256 in the script, and apt packages), `build.sh` (SQLite amalgamation with SHA-256, `cargo build --release --locked`), `start.sh`, `.gitignore`, `Cargo.toml`/`Cargo.lock` with exact pins (versions looked up then, not recalled). - **Correct and simple:** one ring, no fixed files, the reference queries, a writer thread with group commit. **Goal: 42/42 on `test/run.sh`.** - **Tests:** HTTP, JWT, JSON (with serde_json differential), ids, writer failure, crash test. - **Network scale-out:** fixed-file shards, master ring, provided buffers, slabs, backpressure, timeouts, admission control, plus the many-connection test client and memory measurements. - **Read-path optimizations, one at a time, each measured:** feed range path and F3 variants, direct output, read transaction per batch, pragmas and cache size, create (a) vs (b), SQLite PGO. Each goes into BENCHMARKS.md with before/after numbers. - **`ops/` and docs:** systemd units, backup, restore runbook, journald, deploy hook, final README. - **Validation on a 1 vCPU / 2 GB box** (your VPS, if its specs fit) with an external load generator. ## Decisions I need from you - **Drop SAVEPOINT-per-job** in favour of statement atomicity plus a one-time batch retry (§4c)? - **Use your VPS as the test box?** What are its vCPU/RAM/disk specs? Real load numbers also need a second machine to run k6. A VPS can't load-test itself honestly. - **500k expectations:** on the kernel measured so far, 500k sockets don't fit in 2 GB. I'll design for it and set the admission ceiling from the 6.8 measurement, but the honest target may be ~330–450k, and I'll report what's actually measured. ## What changed during implementation Planned | Done | Why | SAVEPOINT per write job | No savepoints. A job that aborts the transaction is dropped and the batch replayed | Each job is one statement, which SQLite already makes atomic. The replay path is unit-tested with a test-only trigger | Multishot direct accept | 16 single-shot accepts in flight, each counted against a shard's free slots | With multishot, a full fixed-file table made the kernel accept and then drop a connection (4 of 5,000 lost) | Feed F3 variants to benchmark | Range scan counted in Rust (5.7 µs) beat `GROUP BY` (8.0) and point counts (10.2) | `feed_variants` test, BENCHMARKS.md §4 | Create: RETURNING vs separate SELECT | RETURNING kept | Creates are 0.9 % of traffic; not worth a second statement | HMAC library comparison | RustCrypto `sha2`/`hmac` without comparison | Auth is 7.8 % of requests (~0.08 µs of the average request) | SO_RCVBUF/SO_SNDBUF tuning | Kernel defaults | No measurable CPU difference; idle sockets hold no buffers either way | Completion batching | Not shipped | No gain under saturation, and it can't be exercised in its target regime locally | SQLite PGO | Done, trained on a synthetic DB generated at build time | feed −7 %, mixed −5 to −12 % (BENCHMARKS.md §8) | (unplanned) `SQLITE_ENABLE_QPSG` | Added | The single-post statement was re-prepared on every request; 2.3× faster single-post reads | (unplanned) single-step backups | Backup API in one step | A batched backup restarts on every write and never finished under steady writes | --- # Every feature in one app Source: https://dihlabs.lol/docs/features-app > Every feature optz shares with Next.js, in one small app: streaming and _loading, Server Actions (also without JavaScript), ISR and on-demand revalidation, dynamicParams, draft… Every feature optz shares with Next.js, in one small app: streaming and `_loading`, Server Actions (also without JavaScript), ISR and on-demand revalidation, `dynamicParams`, draft mode, optional catch-alls, parallel and intercepting routes, i18n (en/fr), Open Graph images, sitemap, robots and manifest, ``, self-hosted fonts, `