Skip to content

Repository files navigation

static-web

A production-grade, high-performance static web file server written in Go. Built on fasthttp for maximum throughput — ~141k req/sec, 55% faster than Bun's native static server.

Table of Contents


Quick Start

# Go install (requires Go 1.26+)
go install github.com/BackendStack21/static-web/cmd/static-web@latest
# Serve the current directory
static-web .# Serve a build output directory on port 3000
static-web --port 3000 ./dist
# Scaffold a config file
static-web init

CLI

For the full flag reference, subcommand documentation, and installation options, see CLI.md.

static-web --help

Features

FeatureDetail
In-memory LRU cacheSize-bounded, byte-accurate; ~28 ns/op lookup with 0 allocations. Optional startup preload for instant cache hits.
CompressionOn-the-fly gzip; pre-compressed .gz/.br/.zst sidecar support; priority: br > zstd > gzip
HTTP/2Automatic ALPN negotiation when TLS is configured
Conditional requestsETag, 304 Not Modified, If-Modified-Since, If-None-Match
Range requestsByte ranges via custom parseRange/serveRange implementation for video and large files
TLS 1.2 / 1.3Modern cipher suites; configurable cert/key paths
Security headersX-Content-Type-Options, X-Frame-Options, Content-Security-Policy, Referrer-Policy, Permissions-Policy
HSTSStrict-Transport-Security on all HTTPS responses; configurable max-age
HTTP→HTTPS redirectAutomatic 301 redirect on the HTTP port when TLS is active
Method whitelistOnly GET, HEAD, OPTIONS are accepted (TRACE/PUT/POST blocked)
Dotfile protectionBlocks .env, .git/, etc. by default
Directory listingOptional HTML directory index with breadcrumb navigation, sorted entries, human-readable sizes, and dotfile filtering
Symlink escape preventionEvalSymlinks re-verified against root; symlinks pointing outside root are blocked
CORSConfigurable per-origin or wildcard (* emits literal *, never reflected)
Graceful shutdownSIGTERM/SIGINT drains in-flight requests with configurable timeout
Live cache flushSIGHUP flushes both the in-memory file cache and the path-safety cache without downtime

Architecture

HTTP request
│
▼
┌─────────────────┐
│ recoveryMiddleware │ ← panic → 500, log stack
└────────┬────────┘
│
┌────────▼────────┐
│ loggingMiddleware │ ← logs method/path/status/duration
└────────┬────────┘
│
┌────────▼────────────────────────────────────────┐
│ security.Middleware │
│ • Method whitelist (GET/HEAD/OPTIONS only) │
│ • Security headers (set BEFORE path check) │
│ • PathSafe: null bytes, path.Clean, EvalSymlinks│
│ • Path-safety cache (bounded LRU, pre-warmed) │
│ • Dotfile blocking │
│ • CORS (preflight + per-origin or wildcard *) │
│ • Injects validated path into ctx.SetUserValue │
└────────┬────────────────────────────────────────┘
│
┌────────▼────────────────────────────────────────┐
│ handler.FileHandler │
│ • Cache hit → direct ctx.SetBody() fast path │
│ • Range/conditional → custom serveRange() │
│ • Cache miss → os.Stat → disk read → cache put │
│ • Large files (> max_file_size) bypass cache │
│ • Encoding negotiation: brotli > zstd > gzip > plain │
│ • Preloaded files served instantly on startup │
│ • Custom 404 page (path-validated) │
└─────────────────────────────────────────────────┘
│
┌────────▼────────────────────────────────────────┐
│ compress.Middleware (post-processing) │
│ • Compresses response body after handler runs │
│ • Skips 1xx/204/304, non-compressible types │
│ • Respects q=0 explicit denial │
└─────────────────────────────────────────────────┘

Request path through the cache

GET /app.js
│
├─ cache.Get("/app.js") hit?
│ YES → serveFromCache (direct ctx.SetBody, no syscall) → done
│
└─ NO → resolveIndexPath → cache.Get(canonicalURL) hit?
YES → serveFromCache → done
NO → os.Stat → os.ReadFile → cache.Put → serveFromCache

When preload = true, every eligible file is loaded into cache at startup. The path-safety cache (bounded LRU) is also pre-warmed, so the very first request for any preloaded file skips both filesystem I/O and EvalSymlinks. Symlink targets are validated against the root during the preload walk — symlinks pointing outside root are skipped.


Performance

End-to-end HTTP benchmarks

Measured on Apple M-series, localhost (no Docker), serving 3 small static files via bombardier -c 100 -n 100000:

ServerAvg Req/secp50 Latencyp99 LatencyThroughput
static-web (fasthttp + preload)~141,000619 µs2.46 ms469 MB/s
Bun (native static serve)~90,0001.05 ms2.33 ms306 MB/s
static-web (old net/http)~76,0001.25 ms3.15 ms

With preload = true and the fasthttp engine, static-web delivers ~141k req/sec55% faster than Bun's native static serving, while offering full security headers, TLS, and compression out of the box.

Micro-benchmarks

Measured on Apple M2 Pro (go test -bench=. -benchtime=5s):

Benchmarkops/sns/opallocs/op
BenchmarkCacheGet35–42 M28–290
BenchmarkCacheGetParallel6–8 M139–1480

Key design decisions

  • fasthttp engine: Built on fasthttp — pre-allocated per-connection buffers with near-zero allocation hot path. Cache hits bypass all string formatting; headers are pre-computed at cache-population time.
  • tcp4 listener: IPv4-only listener eliminates dual-stack overhead on macOS/Linux — a 2× throughput difference vs "tcp".
  • Preload at startup: preload = true reads all eligible files into RAM before the first request — eliminating cold-miss latency.
  • Direct ctx.SetBody() fast path: cache hits bypass range/conditional logic entirely; pre-formatted Content-Type and Content-Length headers are assigned directly.
  • Custom Range implementation: parseRange()/serveRange() handle byte-range requests without http.ServeContent.
  • Post-processing compression: compress middleware runs after the handler, compressing the response body in a single pass.
  • Path-safety cache: Bounded LRU cache (default 10,000 entries) eliminates per-request filepath.EvalSymlinks syscalls. Pre-warmed from preload.
  • GC tuning: gc_percent = 400 reduces garbage collection frequency — the hot path avoids all formatting allocations, with only minimal byte-to-string conversions from fasthttp's []byte API.
  • Cache-before-stat: os.Stat is never called on a cache hit — the hot path is pure memory.
  • Zero-alloc AcceptsEncoding: walks the Accept-Encoding header byte-by-byte without strings.Split.
  • Pre-computed ETagFull: the W/"..." string is built when the file is cached.

Security Model

Path Safety (internal/security)

Every request URL is validated through PathSafe before any filesystem access:

  1. Null byte rejection — prevents C-level path truncation.
  2. path.Clean normalisation — collapses /../, //, etc.
  3. Prefix check — ensures the resolved path starts with the absolute root (separator-aware to prevent /rootsuffix collisions).
  4. EvalSymlinks re-verification — resolves the canonical real path and re-checks the prefix. Symlinks pointing outside root return ErrPathTraversal. Non-existent paths (ENOENT) fall back to the already-checked candidate.
  5. Dotfile blocking — each path segment is checked for a leading ..

HTTP Security Headers

Set on every response including 4xx/5xx errors:

HeaderDefault Value
X-Content-Type-Optionsnosniff
X-Frame-OptionsSAMEORIGIN
Content-Security-Policydefault-src 'self'
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policygeolocation=(), microphone=(), camera=()
Strict-Transport-Securitymax-age=31536000(HTTPS only)

Method Whitelist

Only GET, HEAD, and OPTIONS are accepted. All other methods (including TRACE, PUT, POST, DELETE, PATCH) receive 405 Method Not Allowed. This means TRACE-based XST attacks are impossible by design.

CORS

  • Wildcard (["*"]): emits the literal string *. The request Origin is never reflected. Vary: Origin is not added (correct per RFC 6454).
  • Specific origins: each allowed origin is compared exactly. Matching origins receive Access-Control-Allow-Origin: <origin> and Vary: Origin.
  • Preflight (OPTIONS): returns 204 with Access-Control-Allow-Methods, Access-Control-Allow-Headers, and Access-Control-Max-Age: 86400.

DoS Mitigations

MitigationValue
ReadTimeout10 s (covers full read phase including headers — Slowloris protection)
WriteTimeout10 s
IdleTimeout75 s (keep-alive)
MaxRequestBodySize1024 bytes (static file server needs no large request bodies)
MaxConnsPerIPConfigurable (default 0 = unlimited)

Configuration Reference

Copy config.toml.example to config.toml and edit as needed. The server starts without a config file using sensible defaults.

[server]

KeyTypeDefaultDescription
addrstring:8080HTTP listen address
tls_addrstring:8443HTTPS listen address
redirect_hoststringCanonical host used for HTTP→HTTPS redirects
tls_certstringPath to TLS certificate (PEM)
tls_keystringPath to TLS private key (PEM)
read_timeoutduration10sFull request read deadline (covers headers; Slowloris protection)
write_timeoutduration10sResponse write deadline
idle_timeoutduration75sKeep-alive idle timeout
shutdown_timeoutduration15sGraceful drain window
max_conns_per_ipint0Max concurrent connections per IP (0 = unlimited)

[files]

KeyTypeDefaultDescription
rootstring./publicDirectory to serve
indexstringindex.htmlIndex file for directory requests
not_foundstringCustom 404 page (relative to root)
max_serve_file_sizeint1073741824Max file size to serve in bytes (0 = unlimited; default 1 GB). Files exceeding this limit receive 413.

[cache]

KeyTypeDefaultDescription
enabledbooltrueToggle in-memory LRU cache
preloadboolfalseLoad all eligible files into cache at startup
max_bytesint268435456Cache size cap (bytes)
max_file_sizeint10485760Max file size to cache (bytes)
ttlduration0Entry TTL (0 = no expiry; flush with SIGHUP)
gc_percentint0Go GC target percentage (0 = use Go default of 100)

[compression]

KeyTypeDefaultDescription
enabledbooltrueEnable compression
min_sizeint1024Minimum bytes to compress
levelint5gzip level (1–9)
precompressedbooltrueServe .gz/.br/.zst sidecar files
max_compress_sizeint10485760Max body size for on-the-fly gzip compression in bytes (0 = unlimited; default 10 MB)

[headers]

KeyTypeDefaultDescription
immutable_patternstringGlob for immutable assets
static_max_ageint3600Cache-Control max-age for non-HTML (seconds)
html_max_ageint0Cache-Control max-age for HTML (seconds)
enable_etagsbooltrueEnable ETag generation and If-None-Match validation for cache revalidation

[security]

KeyTypeDefaultDescription
block_dotfilesbooltrueBlock .-prefixed path components
directory_listingboolfalseEnable directory index listing
cors_origins[]string[]Allowed CORS origins (["*"] for wildcard)
cspstringdefault-src 'self'Content-Security-Policy value
referrer_policystringstrict-origin-when-cross-originReferrer-Policy value
permissions_policystringgeolocation=(), microphone=(), camera=()Permissions-Policy value
hsts_max_ageint31536000HSTS max-age in seconds (HTTPS only; 0 disables)
hsts_include_subdomainsboolfalseAdd includeSubDomains to HSTS header

Environment Variables

All environment variables override the corresponding TOML setting. Useful for containers.

VariableConfig Field
STATIC_SERVER_ADDRserver.addr
STATIC_SERVER_TLS_ADDRserver.tls_addr
STATIC_SERVER_REDIRECT_HOSTserver.redirect_host
STATIC_SERVER_TLS_CERTserver.tls_cert
STATIC_SERVER_TLS_KEYserver.tls_key
STATIC_SERVER_READ_TIMEOUTserver.read_timeout
STATIC_SERVER_WRITE_TIMEOUTserver.write_timeout
STATIC_SERVER_IDLE_TIMEOUTserver.idle_timeout
STATIC_SERVER_SHUTDOWN_TIMEOUTserver.shutdown_timeout
STATIC_SERVER_MAX_CONNS_PER_IPserver.max_conns_per_ip
STATIC_FILES_ROOTfiles.root
STATIC_FILES_INDEXfiles.index
STATIC_FILES_NOT_FOUNDfiles.not_found
STATIC_FILES_MAX_SERVE_FILE_SIZEfiles.max_serve_file_size
STATIC_CACHE_ENABLEDcache.enabled
STATIC_CACHE_PRELOADcache.preload
STATIC_CACHE_MAX_BYTEScache.max_bytes
STATIC_CACHE_MAX_FILE_SIZEcache.max_file_size
STATIC_CACHE_TTLcache.ttl
STATIC_CACHE_GC_PERCENTcache.gc_percent
STATIC_COMPRESSION_ENABLEDcompression.enabled
STATIC_COMPRESSION_MIN_SIZEcompression.min_size
STATIC_COMPRESSION_LEVELcompression.level
STATIC_COMPRESSION_MAX_COMPRESS_SIZEcompression.max_compress_size
STATIC_HEADERS_ENABLE_ETAGSheaders.enable_etags
STATIC_SECURITY_BLOCK_DOTFILESsecurity.block_dotfiles
STATIC_SECURITY_CSPsecurity.csp
STATIC_SECURITY_CORS_ORIGINSsecurity.cors_origins (comma-separated)

TLS / HTTPS

Set tls_cert and tls_key to enable HTTPS:

[server]
addr = ":80"tls_addr = ":443"redirect_host = "static.example.com"tls_cert = "/etc/ssl/certs/server.pem"tls_key = "/etc/ssl/private/server.key"

When TLS is configured:

  • HTTP requests on addr are automatically redirected to HTTPS. Set redirect_host when tls_addr listens on all interfaces (for example :443) so redirects use a canonical host instead of the incoming Host header.
  • HTTP/2 is enabled automatically via ALPN negotiation.
  • HSTS (Strict-Transport-Security) is added to all HTTPS responses (configurable max-age).
  • Minimum TLS version is 1.2; preferred cipher suites are ECDHE+AES-256-GCM and ChaCha20-Poly1305.

Pre-compressed Files

Place .gz, .br, and .zst sidecar files alongside originals. The server serves them automatically when the client signals support:

public/
app.js
app.js.gz ← served for Accept-Encoding: gzip
app.js.br ← served for Accept-Encoding: br (preferred)
app.js.zst ← served for Accept-Encoding: zstd (fastest decompress)
style.css
style.css.gz
style.css.br
style.css.zst

Generate sidecars from the Makefile:

make precompress # runs gzip, brotli, and zstd on all .js/.css/.html/.json/.svg

Note: On-the-fly brotli encoding is not implemented. Only .br sidecar files are served with brotli encoding. Zstandard is available both as pre-compressed sidecar files and on-the-fly compression.


HTTP Signals

SignalAction
SIGTERMGraceful shutdown (drains in-flight requests up to shutdown_timeout)
SIGINTGraceful shutdown
SIGHUPFlush in-memory file cache and path-safety cache; re-reads config pointer in main

Note: SIGHUP reloads the config pointer in main but the live middleware chain holds references to the old config. A full restart is required for config changes to take effect. SIGHUP is useful for flushing both the file cache and the path-safety cache without downtime.


Building & Development

Prerequisites

  • Go 1.26+
  • GNU Make

Commands

make build # compile → bin/static-web
make release # compile stripped binary → bin/static-web
make install # install to $(GOPATH)/bin
make run # build + run with ./config.toml
make test# go test -race ./...
make bench # go test -bench=. -benchtime=5s ./...
make lint # go vet ./...
make precompress # generate .gz/.br sidecars for public/
make clean # remove bin/

Running Tests

go test -race ./... # full suite with race detector
go test -run TestPathSafe ./internal/security/... # specific test
go test -bench=BenchmarkCacheGet -benchtime=10s ./internal/cache/

Code Quality Gates

All PRs must pass:

go build ./... # clean compile
go vet ./... # static analysis
go test -race ./... # all tests, race-free

Known Limitations

LimitationDetail
Brotli on-the-flyNot implemented. Only pre-compressed .br sidecar files are served.
SIGHUP config reloadReloads the config struct pointer in main only. Live middleware chains hold old references — full restart required for config changes to propagate.

About

High performance HTTP server for exposing static web files.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages