Designing a Minimal Internal HTTP Service with Bun.serve
A guide to building a fast, single‑process HTTP API using Bun.serve, covering requirements, trust boundaries, operational checks, failure modes and when to change the design.
13 Mar 2026, 01:33 UTC

Problem and Takeaway
You need a tiny internal HTTP API that starts instantly, runs with almost no operational baggage, and can be shipped as a single file. Fast startup reduces cold‑start latency for internal tools and enables rapid iteration during development. A single file simplifies deployment across environments and reduces the attack surface. The simplest way is to let Bun’s built‑in Bun.serve become the only process that listens on a port, handle requests directly, and shut down cleanly on termination signals. This design gives you fast startup, minimal moving parts, and a clear boundary for trust and data handling.
Requirements
- Fast startup and minimal runtime overhead.
- One deployable artifact – no separate web framework or reverse proxy.
- Single‑process design that can be containerized or run on a VM.
- Ability to expose a health check for automated monitoring.
- Minimal external dependencies.
Smallest Suitable Design
At program start call Bun.serve with the port taken from an environment variable. The port may be supplied via a configuration file or command‑line flag, but an environment variable is the most portable approach. Register a fetch(request) handler that routes to a small module, returns Response objects, and includes a /health endpoint that always returns 200 OK. The health endpoint returns JSON: {"status":"ok","ts":"2026-11-12T02:36:00Z"}. Use Bun.file only when static assets are required; otherwise keep routing logic in a single file. The handler should be wrapped in try/catch to return an explicit 500 on unexpected errors.
import { serve } from "bun";
const port = Number(process.env.PORT) || 3000;
serve({
port,
fetch(request) {
try {
const url = new URL(request.url);
if (url.pathname === "/health") {
return new Response(JSON.stringify({"status":"ok","ts":"2026-11-12T02:36:00Z"}), {
status: 200,
headers: { "Content-Type": "application/json" }
});
}
// route other paths here
return new Response("Not found", { status: 404 });
} catch (e) {
return new Response("Internal Server Error", { status: 500 });
}
},
// optional limits
}).then(() => console.log(`Listening on :${port}`));
Trust and Data Boundaries
Treat every request field, header, query parameter, and body as untrusted. Validate inputs at the top of the handler before using them. Reject any request method that is not GET, POST, or HEAD early to avoid unnecessary processing. Secrets must come from environment variables, never from the request payload or source control. Forward only those headers you control, such as X-Forwarded-For, when the service sits behind a proxy you manage, and enforce a maximum request body size (e.g., 1 MiB) to prevent memory exhaustion. This protects the process from malformed or malicious input.
Operational Checks
- Expose a lightweight
/healthendpoint for liveness probes. - Log each request as structured JSON containing method, path, status, latency, timestamp, and request ID for correlation.
- Run the process under a supervisor (Docker restart policy, systemd unit, etc.) to ensure automatic restart on crash.
- Pin the Bun version in the deployment image (for example
FROM bun:1.2.31) and re‑verify behavior after any upgrade. - Smoke‑test startup by asserting the health endpoint replies before routing real traffic.
- Configure an idle timeout and a request body size limit in the
serveoptions to mitigate slow‑client attacks. - The container image size stays under 30 MB because Bun bundles the runtime.
Failure Modes to Plan For
- Uncaught exceptions or unhandled rejections inside
fetchcan destabilize the single process. Wrap handler logic intry/catchand return an explicit500response. - Slow or hostile clients may hold connections open. Mitigate with idle timeouts, body size limits, and the
server.stop()graceful shutdown handler. - Process crashes lose in‑memory state. Keep durable state outside the process or in an embedded key‑value store.
- Memory leaks in long‑running handlers can eventually exhaust the process heap; periodic restarts help.
- Throughput is capped by a single process; scale by adding more instances behind a load balancer.
Conditions That Change the Design
- Sustained traffic that exceeds one process capacity – add a reverse proxy or run multiple Bun instances.
- Horizontal scaling is straightforward because each instance is independent and shares no state.
- Long‑running CPU‑intensive work – move it to worker threads or a queue rather than keeping it in the request handler.
- Need for Node‑specific native modules or APIs unsupported by Bun – consider migrating to Node or another runtime.
- Public internet exposure requiring hardened edge protections – introduce TLS termination, rate limiting, and a dedicated reverse proxy.
- If you need HTTP/2, you must place a reverse proxy in front of Bun.
- Compliance mandates favoring a more mature runtime – choose a different server implementation.
Verification Steps
- Run
bun --versionin the target environment and record the version. - Write a minimal script that starts
Bun.serveon a test port and returns a fixedResponse. Verify withcurl http://localhost:$PORTthat the response is received. - Add a temporary route that echoes request headers and body. Send oversized and malformed requests; observe status codes, logs, and whether the process stays alive.
- Send
SIGTERMto the running process (e.g.,kill -TERM) and confirm the shutdown handler callsserver.stop()and the port is released. - Consult the official Bun documentation and changelog for the pinned version to confirm option names such as idle timeout and request body size limits before using them in production.
- Use a CI pipeline to run the health‑check script on every image build and document the exact curl command and expected response in a test script.
- Load‑test with a simple concurrent client sending many parallel requests to measure throughput and memory usage of the single process on your hardware.
Limitations and Practical Checks
The design assumes low to moderate traffic and does not provide built‑in multi‑region redundancy or durable storage. Do not keep critical state solely in process memory; replicate it to a database or external store. Because Bun’s API evolves between releases, pin a specific version and re‑run the verification steps after any upgrade. Performance claims are version‑specific; measure on your own hardware before committing to production. The single‑process model is not a substitute for multi‑region redundancy or for persisting state outside the process. The server does not implement streaming responses; for large payloads you must buffer them yourself.
Conclusion
By using Bun.serve as the single entry point, you achieve fast startup, minimal operational overhead, and a clear boundary for trust and data handling. In practice, this pattern has been used for micro‑services that handle a few hundred requests per second, delivering sub‑100 ms cold starts. The design works well for small internal services, but you must monitor capacity, validate inputs, and be ready to evolve the architecture when traffic, workload, or compliance requirements change.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.