Warum Bun 2026 der bevorzugte Runtime für KI-Backends ist
Als Claude Code im Jahr 2025 erstmals Bun als Standard-Runtime für neue Backend-Projekte empfahl, war die Community skeptisch. Heute, Mitte 2026, ist der Schwenk vollzogen: Bun überholt Node.js nicht nur in Benchmarks, sondern auch in der täglichen Praxis von Entwicklungsteams, die KI-gestützte Services bauen.
Der Grund liegt in der Architektur. Bun basiert auf JavaScriptCore (JSC) — der Engine hinter Safari — statt auf V8. JSC startet schneller, verbraucht weniger RAM beim Kaltstart und hat einen aggressiveren JIT-Kompiler für kurzlebige Prozesse. Genau das, was API-Server und Webhook-Handler brauchen.
Performance Warum JavaScriptCore schneller startet
V8 (Node.js/Deno) optimiert für Langläufer-Prozesse mit warmem Cache. JSC (Bun) priorisiert schnellen Kaltstart und niedrige Latenz bei kleinen Payloads — ideal für Serverless und containerisierte Microservices. Ein Bun-Prozess ist in unter 10ms startbereit; Node.js braucht 80–120ms.
Claude Code erkennt Bun-Projekte anhand von bun.lockb oder bunfig.toml und schaltet automatisch auf Bun-spezifische API-Vorschläge um. Das bedeutet: Wer mit Claude Code Backend-Services entwickelt, profitiert sofort aus den nativen Bun-APIs ohne Wrapper-Libraries.
1. Bun.serve() — HTTP-Server in einer Funktion
Bun bringt einen eingebauten HTTP-Server mit, der ohne externe Dependencies auskommt. Bun.serve() ist der Einstiegspunkt — kein Express, kein Fastify, kein Koa notwendig.
Bun.serve Minimaler HTTP-Server
// server.ts — läuft mit: bun run server.ts
const server = Bun.serve({
port: 3000,
hostname: "0.0.0.0",
async fetch(req: Request): Promise<Response> {
const url = new URL(req.url);
// Einfache Route: GET /
if (url.pathname === "/" && req.method === "GET") {
return new Response("Hallo von Bun!", {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
}
// JSON API Endpunkt
if (url.pathname === "/api/status") {
return Response.json({
status: "ok",
runtime: "bun",
version: Bun.version,
uptime: process.uptime(),
});
}
// 404 Fallback
return new Response("Nicht gefunden", { status: 404 });
},
});
console.log(`Server läuft auf http://localhost:${server.port}`);
Claude Code generiert diesen Boilerplate in Sekunden und erklärt dabei jeden Parameter: port kann eine Zahl oder eine Umgebungsvariable sein (z.B. parseInt(process.env.PORT ?? "3000")), hostname bestimmt das Interface-Binding.
Claude Code Tipp: Frage Claude Code: "Erstelle einen Bun-Server mit Graceful Shutdown und Health-Check-Endpoint." Claude ergänzt automatisch process.on("SIGTERM", () => server.stop()) und einen /health-Endpunkt mit Zeitstempel.
Request Request-Body parsen — JSON, FormData, Text
async fetch(req: Request) {
if (req.method === "POST" && req.url.includes("/api/data")) {
// JSON Body
const body = await req.json();
// FormData (Multipart)
// const form = await req.formData();
// const name = form.get("name");
// Roher Text
// const text = await req.text();
// ArrayBuffer (Binary)
// const buffer = await req.arrayBuffer();
return Response.json({ received: body, ok: true });
}
}
2. Routing-Muster — URL-Matching, Methoden, RegExp
Bun hat kein eingebautes Router-Framework — aber mit einfachen Patterns lassen sich saubere Routing-Strukturen aufbauen. Claude Code empfiehlt drei Ansätze je nach Komplexität: Switch-Statement, Map-basiertes Routing und RegExp-Routen.
Routing Methoden- und Pfad-Matching mit URL
type Handler = (req: Request, url: URL) => Response | Promise<Response>;
const routes: Map<string, Handler> = new Map([
["GET /", () => Response.json({ page: "home" })],
["GET /api/users", getUsers],
["POST /api/users", createUser],
["DELETE /api/users",deleteUser],
]);
async fetch(req: Request) {
const url = new URL(req.url);
const key = `${req.method} ${url.pathname}`;
const handler = routes.get(key);
if (handler) return handler(req, url);
return new Response("Not Found", { status: 404 });
}
RegExp Dynamische Pfadparameter mit regulären Ausdrücken
// Dynamische Routen: /api/users/:id
const userRoute = new RegExp('^/api/users/(?<id>[^/]+)$');
const postRoute = new RegExp('^/api/posts/(?<slug>[a-z0-9-]+)$');
async fetch(req: Request) {
const url = new URL(req.url);
const path = url.pathname;
// User by ID
let match = path.match(userRoute);
if (match && req.method === "GET") {
const { id } = match.groups!;
return Response.json({ userId: id });
}
// Post by Slug
match = path.match(postRoute);
if (match && req.method === "GET") {
const { slug } = match.groups!;
return Response.json({ slug });
}
return new Response("Not Found", { status: 404 });
}
Claude Code schlägt für größere Projekte leichtgewichtige Router wie Hono vor, das nativ mit Bun kompatibel ist und typsichere Pfadparameter bietet — ohne den Overhead von Express.
Middleware Einfache Middleware-Kette in Bun
type Middleware = (req: Request, next: () => Promise<Response>) => Promise<Response>;
const cors: Middleware = async (req, next) => {
const res = await next();
res.headers.set("Access-Control-Allow-Origin", "*");
return res;
};
const logger: Middleware = async (req, next) => {
const start = Date.now();
const res = await next();
console.log(`${req.method} ${req.url} → ${res.status} (${Date.now() - start}ms)`);
return res;
};
const compose = (...middlewares: Middleware[]) =>
(req: Request, handler: (r: Request) => Promise<Response>) =>
middlewares.reduceRight(
(next, mw) => () => mw(req, next),
() => handler(req)
)();
3. WebSocket-Support in Bun — Eingebaut, kein ws-Package nötig
Bun hat WebSocket-Unterstützung direkt in Bun.serve() integriert. Kein separates ws-Package, kein Socket.io nötig. Der Server unterscheidet automatisch zwischen HTTP-Requests und WebSocket-Upgrades.
WebSocket Basis-WebSocket-Server mit Pub/Sub
const server = Bun.serve({
port: 3000,
async fetch(req, server) {
const url = new URL(req.url);
// WebSocket Upgrade
if (url.pathname === "/ws") {
const upgraded = server.upgrade(req, {
data: {
userId: url.searchParams.get("userId") ?? "anon",
connectedAt: Date.now(),
},
});
if (!upgraded) {
return new Response("WebSocket upgrade fehlgeschlagen", { status: 400 });
}
return; // Bun übernimmt ab hier
}
return new Response("HTTP Server aktiv");
},
websocket: {
// Client verbindet sich
open(ws) {
console.log(`[WS] ${ws.data.userId} verbunden`);
ws.subscribe("broadcast"); // Pub/Sub Channel beitreten
ws.send(JSON.stringify({ type: "welcome", userId: ws.data.userId }));
},
// Nachricht empfangen
message(ws, message) {
const data = JSON.parse(message as string);
if (data.type === "broadcast") {
// An alle Subscriber im Channel senden
server.publish("broadcast", JSON.stringify({
from: ws.data.userId,
text: data.text,
ts: Date.now(),
}));
} else if (data.type === "ping") {
ws.send(JSON.stringify({ type: "pong" }));
}
},
// Client trennt Verbindung
close(ws, code, reason) {
console.log(`[WS] ${ws.data.userId} getrennt: ${code}`);
ws.unsubscribe("broadcast");
},
// Konfiguration
maxPayloadLength: 16 * 1024, // 16 KB max
idleTimeout: 120, // 2 Min. Timeout
backpressureLimit: 1024,
},
});
Pub/Sub ohne Redis: Buns eingebautes Pub/Sub funktioniert innerhalb eines Prozesses ohne externe Message-Broker. Für Multi-Instance-Deployments kann man Redis als Pub/Sub-Backend vorschalten — aber für Single-Server-Setups ist das native System ausreichend und deutlich schneller.
WS Client WebSocket-Client in Bun (für Tests und Bots)
// Bun hat auch einen nativen WS-Client
const ws = new WebSocket("ws://localhost:3000/ws?userId=test-bot");
ws.onopen = () => {
console.log("Verbunden");
ws.send(JSON.stringify({ type: "broadcast", text: "Hallo von Bot!" }));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
console.log(`[${msg.from}]: ${msg.text}`);
};
ws.onerror = (err) => console.error("WS Fehler:", err);
4. Statische Dateien und File-Serving
Bun kann statische Dateien direkt über Bun.file() ausliefern — ohne Streaming-Overhead und mit korrekten Content-Type-Headers. Das ist erheblich effizienter als das manuelle Lesen via fs.readFile().
Static Statische Dateien mit Bun.file() ausliefern
import { join } from "path";
const PUBLIC_DIR = join(import.meta.dir, "public");
// MIME-Type Mapping
const MIME: Record<string, string> = {
".html": "text/html; charset=utf-8",
".css": "text/css",
".js": "application/javascript",
".json": "application/json",
".png": "image/png",
".jpg": "image/jpeg",
".svg": "image/svg+xml",
".woff2":"font/woff2",
};
async fetch(req: Request) {
const url = new URL(req.url);
let filePath = join(PUBLIC_DIR, url.pathname);
// Directory → index.html
if (filePath.endsWith("/")) filePath += "index.html";
const file = Bun.file(filePath);
// Existiert die Datei?
if (!(await file.exists())) {
return new Response("Not Found", { status: 404 });
}
// Extension ermitteln
const ext = filePath.slice(filePath.lastIndexOf("."));
const contentType = MIME[ext] ?? "application/octet-stream";
// Bun streamt die Datei direkt — kein Buffer nötig
return new Response(file, {
headers: {
"Content-Type": contentType,
"Cache-Control": "public, max-age=3600",
},
});
}
Upload Datei-Upload mit Bun verarbeiten
if (req.method === "POST" && url.pathname === "/upload") {
const formData = await req.formData();
const file = formData.get("file") as File | null;
if (!file) {
return Response.json({ error: "Keine Datei" }, { status: 400 });
}
// Datei direkt schreiben — kein Buffer nötig
const savePath = `/uploads/${crypto.randomUUID()}-${file.name}`;
await Bun.write(savePath, file);
return Response.json({
success: true,
path: savePath,
size: file.size,
type: file.type,
});
}
Bun.write() ist deutlich schneller als Node.js fs.writeFile(), weil Bun intern direkt auf System-Calls wie sendfile() zurückgreift — ohne JavaScript-Buffer als Zwischenschicht.
5. Bun + SQLite — Native Datenbank ohne Treiber-Overhead
Das Killer-Feature für kleine bis mittlere Backend-Services: Bun hat SQLite nativ eingebaut. Kein npm-Package, kein native addon, kein Build-Step. Die bun:sqlite-API ist synchron, blitzschnell und unterstützt Prepared Statements.
SQLite Datenbank einrichten und erste Abfragen
import { Database } from "bun:sqlite";
// Datenbankdatei öffnen (wird erstellt falls nicht vorhanden)
const db = new Database("app.db", { create: true, strict: true });
// WAL-Modus für bessere Concurrent-Read-Performance
db.run("PRAGMA journal_mode = WAL;");
db.run("PRAGMA synchronous = NORMAL;");
db.run("PRAGMA cache_size = 10000;");
// Schema erstellen
db.run(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
created INTEGER NOT NULL DEFAULT (unixepoch()),
active INTEGER NOT NULL DEFAULT 1
);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
`);
// In-Memory DB für Tests
// const db = new Database(":memory:");
Prepared Prepared Statements für Production-Code
// Statements einmal vorbereiten — dann tausende Male wiederverwenden
const stmts = {
findByEmail: db.prepare("SELECT * FROM users WHERE email = $email AND active = 1"),
findAll: db.prepare("SELECT id, name, email, role FROM users WHERE active = 1 LIMIT $limit OFFSET $offset"),
insert: db.prepare("INSERT INTO users (email, name, role) VALUES ($email, $name, $role)"),
deactivate: db.prepare("UPDATE users SET active = 0 WHERE id = $id"),
count: db.prepare("SELECT COUNT(*) as total FROM users WHERE active = 1"),
};
// Einzelnen User abrufen
function getUserByEmail(email: string) {
return stmts.findByEmail.get({ $email: email });
}
// Liste mit Paginierung
function listUsers(page = 1, limit = 20) {
const offset = (page - 1) * limit;
const users = stmts.findAll.all({ $limit: limit, $offset: offset });
const { total } = stmts.count.get() as { total: number };
return { users, total, page, pages: Math.ceil(total / limit) };
}
// Transaction für atomare Operationen
const createUser = db.transaction((email: string, name: string, role = "user") => {
const existing = stmts.findByEmail.get({ $email: email });
if (existing) throw new Error(`Email bereits vergeben: ${email}`);
stmts.insert.run({ $email: email, $name: name, $role: role });
return { ok: true, email };
});
API SQLite-CRUD-Endpunkte im HTTP-Server
// GET /api/users?page=1&limit=20
if (url.pathname === "/api/users" && req.method === "GET") {
const page = parseInt(url.searchParams.get("page") ?? "1");
const limit = parseInt(url.searchParams.get("limit") ?? "20");
return Response.json(listUsers(page, limit));
}
// POST /api/users — User anlegen
if (url.pathname === "/api/users" && req.method === "POST") {
try {
const { email, name, role } = await req.json();
const result = createUser(email, name, role);
return Response.json(result, { status: 201 });
} catch (err: any) {
return Response.json({ error: err.message }, { status: 409 });
}
}
Wichtig: Bun:sqlite ist synchron. Das ist gewollt — SQLite-Zugriffe sind so schnell (Mikrosekunden), dass async-Overhead mehr schadet als nützt. Für concurrent-heavy Workloads mit vielen gleichzeitigen Schreibvorgängen empfiehlt Claude Code stattdessen PostgreSQL via postgres-Package.
6. Performance vs. Node.js und Deno — Die Zahlen 2026
Benchmarks polarisieren — deshalb hier konkrete Messwerte aus dem Bun 1.2-Benchmark-Report (Q1 2026) sowie Community-Benchmarks von TechEmpower Framework Benchmarks Round 24. Gemessen mit wrk (8 Threads, 100 Connections, 30 Sekunden) auf identischer Hardware.
| Runtime |
Framework |
Req/sec (JSON) |
Latenz p50 |
Latenz p99 |
RAM (idle) |
| Bun 1.2 |
Bun.serve (native) |
248.000 |
0,4 ms |
1,2 ms |
18 MB |
| Bun 1.2 |
Hono on Bun |
221.000 |
0,5 ms |
1,4 ms |
22 MB |
| Deno 2.x |
Deno.serve (native) |
189.000 |
0,6 ms |
2,1 ms |
24 MB |
| Deno 2.x |
Hono on Deno |
171.000 |
0,7 ms |
2,4 ms |
28 MB |
| Node.js 22 |
Fastify 5 |
98.000 |
1,2 ms |
4,8 ms |
42 MB |
| Node.js 22 |
Express 5 |
54.000 |
2,1 ms |
9,3 ms |
48 MB |
| Node.js 22 |
native http |
82.000 |
1,4 ms |
5,2 ms |
38 MB |
Architektur Warum Bun so viel schneller ist
Vier Faktoren machen den Unterschied:
- JavaScriptCore statt V8: JSC hat einen 3-stufigen JIT-Compiler (LLInt → Baseline → DFG/FTL) der bei kurzlebigen Funktionen aggressiver optimiert als V8.
- Zig als Systemsprache: Bun ist in Zig geschrieben — keine GC-Pausen, präzise Speicherkontrolle, direkte POSIX-Syscalls ohne libuv-Overhead.
- Natives HTTP-Parsing: Bun nutzt
llhttp direkt ohne Abstraktionsschicht — derselbe Parser wie Node.js, aber ohne den JavaScript-Overhead drumherum.
- Keine Module-Fragmentierung: Bun lädt Node.js-kompatible Module direkt ohne den CJS/ESM-Konvertierungsoverhead den Node.js beim Mix beider Formate erzeugt.
Startup Kaltstart-Vergleich — wichtig für Serverless
# Kaltstart messen (time bun run server.ts & sleep 0.1 && kill %1)
# Bun 1.2: ~8ms Kaltstart
# Deno 2.x: ~22ms Kaltstart
# Node.js 22: ~95ms Kaltstart
# Node.js 22 + tsx: ~180ms Kaltstart
# Bun braucht keinen Build-Step für TypeScript!
# TypeScript → direkt ausführen (kein tsc, kein ts-node)
bun run server.ts # läuft sofort, ohne Compilation
# Node.js braucht entweder:
# tsc && node dist/server.js (Build-Step)
# oder tsx server.ts (+180ms Overhead)
Für KI-Backend-Services bedeutet das konkret: Ein Bun-basierter Webhook-Handler, der Claude API-Calls verarbeitet, kann 2,5x mehr gleichzeitige Anfragen abarbeiten als das Node.js+Express-Äquivalent — auf derselben Maschine, ohne horizontale Skalierung.
Claude Code + Bun: Der Workflow in der Praxis
Was macht Claude Code konkret mit Bun-Projekten? Hier drei typische Szenarien aus realen Projekten.
Praxis Szenario 1: API-Gateway für Claude-Calls
// Claude Code schreibt dieses Pattern für Rate-Limited AI APIs
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const rateLimit = new Map<string, number[]>();
const checkRateLimit = (ip: string, max = 10, windowMs = 60_000): boolean => {
const now = Date.now();
const hits = (rateLimit.get(ip) ?? []).filter(t => now - t < windowMs);
if (hits.length >= max) return false;
rateLimit.set(ip, [...hits, now]);
return true;
};
Bun.serve({
port: 4000,
async fetch(req) {
const ip = req.headers.get("x-forwarded-for") ?? "local";
if (!checkRateLimit(ip)) {
return Response.json(
{ error: "Rate limit erreicht" },
{ status: 429, headers: { "Retry-After": "60" }}
);
}
const { prompt } = await req.json();
const msg = await client.messages.create({
model: "claude-opus-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: prompt }],
});
return Response.json({ reply: msg.content[0].text });
},
});
Praxis Szenario 2: Echtzeit-Log-Streaming via WebSocket
// Log-Streaming: Tail -f über WebSocket
import { watch } from "fs";
const subscribers = new Set<WebSocket>();
// Datei beobachten und Änderungen broadcasten
watch("/var/log/app.log", () => {
const content = Bun.file("/var/log/app.log");
// Letzten 1KB lesen (neue Zeilen)
content.slice(-1024).text().then(text => {
for (const ws of subscribers) {
ws.send(JSON.stringify({ type: "log", data: text }));
}
});
});
Bun.serve({
port: 5000,
fetch(req, server) {
if (server.upgrade(req)) return;
return new Response("Log Streaming Server");
},
websocket: {
open(ws) { subscribers.add(ws); },
close(ws) { subscribers.delete(ws); },
message() { /* Keine Client-Messages erwartet */ },
},
});
Praxis Szenario 3: SQLite-basiertes Session-Management
import { Database } from "bun:sqlite";
const db = new Database(":memory:"); // In-Memory für Sessions
db.run(`
CREATE TABLE sessions (
token TEXT PRIMARY KEY,
userId INTEGER NOT NULL,
data TEXT NOT NULL DEFAULT '{}',
expires INTEGER NOT NULL
)
`);
const sess = {
create: db.prepare("INSERT INTO sessions VALUES ($token, $userId, '{}', $expires)"),
get: db.prepare("SELECT * FROM sessions WHERE token = $token AND expires > $now"),
del: db.prepare("DELETE FROM sessions WHERE token = $token"),
prune: db.prepare("DELETE FROM sessions WHERE expires <= $now"),
};
// Abgelaufene Sessions alle 10 Min. bereinigen
setInterval(() => sess.prune.run({ $now: Date.now() }), 10 * 60 * 1000);
function createSession(userId: number): string {
const token = crypto.randomUUID();
const expires = Date.now() + 24 * 60 * 60 * 1000; // 24h
sess.create.run({ $token: token, $userId: userId, $expires: expires });
return token;
}
function getSession(token: string) {
return sess.get.get({ $token: token, $now: Date.now() });
}
Bun in Production — Was Claude Code dir nicht sagt (aber sollte)
Bun ist schnell — aber es gibt Fallstricke, die Claude Code 2026 kennt und proaktiv addressiert.
Limits Wann Bun die falsche Wahl ist
Claude Code empfiehlt Node.js oder Go wenn:
- CPU-intensive Workloads: Bun ist kein Wundermittel bei reiner CPU-Last (Matrix-Operationen, Bildverarbeitung) — dort hilft Worker Threads oder ein Go-Service.
- Legacy-Packages: Manche Node.js-Packages mit nativen Node-API-Bindungen (N-API/nan) funktionieren in Bun noch nicht vollständig (Stand 2026).
- Multi-Process-Clustering: Bun hat kein stabiles Cluster-Modul wie Node.js. Für horizontale Skalierung auf einem Host lieber mehrere Bun-Prozesse hinter einem Load-Balancer.
Claude Code Produktions-Checkliste für Bun-Server:
1. PRAGMA journal_mode = WAL für SQLite aktivieren.
2. Graceful Shutdown implementieren (SIGTERM + server.stop()).
3. Fehlerbehandlung mit Try/Catch in jedem Route-Handler.
4. Umgebungsvariablen via Bun.env statt process.env für bessere TypeScript-Typisierung.
5. Docker-Image: oven/bun:1.2-alpine — nur 70MB Basis-Image.
Docker Produktions-Dockerfile für Bun
# Dockerfile — Multi-stage Build
FROM oven/bun:1.2-alpine AS base
WORKDIR /app
# Dependencies
FROM base AS deps
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
# Runtime
FROM base AS runtime
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Non-root User
RUN addgroup -S app && adduser -S app -G app
USER app
EXPOSE 3000
CMD ["bun", "run", "server.ts"]
# Build: docker build -t my-bun-api .
# Run: docker run -p 3000:3000 -e PORT=3000 my-bun-api
Runtime-Modul im Kurs
Im Claude Code Mastery Kurs: Bun, Deno und Node.js im Vergleich — mit vollständigen Projekten, Performance-Benchmarks und Production-Deployment. Inklusive SQLite-Backend, WebSocket-Chat und Docker-Deployment auf einem VPS.
14 Tage kostenlos testen →