DEV Community

appex.uk
appex.uk

Posted on

Wisp (LWS): A wisp of a web server

πŸ‘‹ Hi everyone!

I'm excited to share Wisp β€” originally codenamed LWS (Lightweight Web Server) β€” a tiny, purpose-built static file server written entirely in Go, using only the standard library. No third-party dependencies, no bloat, just a focused, secure, and blazing-fast service that gets straight to work. ✨

πŸ”— Repository: github.com/appex-uk/lws


πŸ“¦ What Makes It Special

Wisp started from a simple frustration: most web servers are either huge and complex or too limited for modern deployments. I wanted something that does exactly what you need β€” nothing more, nothing less β€” and fits comfortably in even the tightest environments.

🀏 It’s genuinely tiny

  • Scratch container image: ~3 MB β€” yes, megabytes
  • Zero runtime dependencies β€” statically compiled Go binary
  • Starts instantly β€” before your health check timeout can even fire
  • Minimal memory footprint β€” your resources stay yours

πŸ› οΈ Capable where it counts

Don't let the size fool you β€” Wisp is packed with thoughtful features:

Feature Details
πŸ“ Secure static serving Uses os.Root to confine all requests β€” paths and symlinks can never escape your content directory
🚫 Hidden file protection Dotfiles and directories are never served; .well-known/ works as expected
πŸ“¦ Precompressed content Serve .br (Brotli) or .gz files directly β€” compress once at build time
πŸ”€ SPA & custom routing SPA fallback mode, custom 404 pages, per-path header rules
πŸ“Š Built-in Prometheus metrics Separate metrics port β€” no exporters, no SDKs, just stdlib
🩺 Kubernetes-ready probes /livez and /readyz endpoints with graceful shutdown drain
πŸ”’ Custom headers & security Set cache policies, security headers, or anything else β€” globally or per path
⏱️ Graceful shutdown Drains connections before terminating β€” no dropped requests

πŸš€ Quick Start

Run it locally

# Clone and run
git clone [https://github.com/appex-uk/lws.git](https://github.com/appex-uk/lws.git)
cd lws
make run
Enter fullscreen mode Exit fullscreen mode

This serves the included ./www directory on port 8080.

Docker β€” the scratch image magic ✨

# Build the image yourself
make docker

# Run with your own content and config
docker run --rm -p 8080:8080 -p 9090:9090 \
  -v "$PWD/site:/srv/www:ro" \
  -v "$PWD/config.json:/etc/lws/config.json:ro" \
  lws:latest
Enter fullscreen mode Exit fullscreen mode

That's it. No pulling hundreds of MB of layers β€” just a tiny, secure container that starts in milliseconds.


βš™οΈ Configure It Your Way

Everything lives in a simple JSON config file. Every field is optional β€” sensible defaults apply:

{
  "address": ":8080",
  "root": "/srv/www",
  "fallback": "/index.html",
  "not_found_page": "/404.html",
  "precompressed": true,
  "headers": {
    "X-Content-Type-Options": "nosniff",
    "X-Frame-Options": "DENY"
  },
  "header_rules": [
    {"match": "*.html", "headers": {"Cache-Control": "no-cache"}},
    {"match": "/assets/*", "headers": {"Cache-Control": "public, max-age=31536000, immutable"}}
  ],
  "access_log": true
}
Enter fullscreen mode Exit fullscreen mode

Key highlights:

  • SPA mode β†’ missing routes without an extension get your index.html with status 200; missing files with an extension still 404 β€” so broken assets don't silently fail
  • Per-path headers β†’ fine-grained cache control or security policies
  • Precompressed β†’ ship .br/.gz alongside originals and Wisp serves the best one automatically

πŸ“Š Built-in Observability β€” Zero Dependencies

This is one of my favourite parts. Wisp exposes a Prometheus metrics endpoint β€” on a separate, non-public port β€” all written with pure Go stdlib. No Prometheus client library, no OTel SDK, nothing to pull in.

Available at: :9090/metrics (configurable)

What you get:

  • lws_http_requests_total β€” request count by method & status code
  • lws_http_request_duration_seconds β€” latency histogram
  • lws_http_requests_in_flight β€” concurrent requests gauge
  • lws_http_response_bytes_total β€” bytes served
  • Plus standard Go runtime & process metrics

Some useful queries you can run straight away:

# Traffic rate
sum(rate(lws_http_requests_total[5m]))

# Error rate
sum(rate(lws_http_requests_total{code=~"5.."}[5m]))
/
sum(rate(lws_http_requests_total[5m]))

# 99th percentile latency
histogram_quantile(0.99, sum by (le) (rate(lws_http_request_duration_seconds_bucket[5m])))
Enter fullscreen mode Exit fullscreen mode

Instrumentation overhead? ~170 ns per request. Negligible.


☸️ Kubernetes Friendly by Design

Wisp was built to run reliably in Kubernetes:

  • Liveness probe: /livez β€” just checks the process is responding
  • Readiness probe: /readyz β€” also verifies the content volume and goes 503 during shutdown drain
  • Graceful sequence on SIGTERM: β†’ readiness 503 β†’ finish in-flight requests β†’ clean exit
  • Example Deployment and Service manifests are in the repo under infra/k8s/

This means rolling deployments work smoothly β€” connections drain before the pod disappears.


πŸ’­ Why Not Just Nginx?

Great question β€” and to be clear: Nginx is amazing. It's battle-tested, incredibly capable, and I use it every day. But sometimes it's simply more than you need:

πŸš› Nginx = The workhorse β€” handles massive traffic, complex routing, modules, SSL termination at scale β€” and it's great at that.

🚲 Wisp = The nimble specialist β€” serve static files securely, fast, and tiny. Perfect for frontend deployments, edge workloads, sidecars, or anywhere "light and fast" matters more than "every feature ever."

If you need to serve a static site, a single-page app, or static assets reliably and securely β€” and you care about image size, startup time, and minimal resource consumption β€” Wisp is built exactly for you.


πŸ—ΊοΈ What's Next

Right now Wisp is focused on doing one thing β€” serving static content β€” and doing it really well. That said, there's always room to polish and improve:

  • More flexible routing options
  • Additional cache control conveniences
  • Real-world feedback from folks using it

Your thoughts, bug reports, or contributions are very welcome!


πŸ”— Links


πŸ’¬ Over to you

What's your biggest annoyance when setting up a static file server? Is it image bloat? Complex config? Missing metrics? Let's chat in the comments β€” I'm here to learn and build something useful together! πŸ‘‡

Top comments (0)