Code by

Carter

Phan

Go Reverse Proxy with Dashboard

Go Reverse Proxy with Dashboard

Year2026
GoNext.jsRedisTypeScript

Project Description

A high-performance Go reverse proxy with a Next.js dashboard for dynamic route management, in-memory caching, and real-time metrics.

Reverse proxies are the silent backbone of microservice architectures. You deploy them once, and they sit quietly in front of your upstream services — routing traffic, terminating TLS, shielding your internals from the open internet.

But there's a catch: every operational task requires a config file edit and a reload. Add a route? SSH in and edit nginx.conf. Debug a failing upstream? Pipe logs into a separate tool. Check traffic patterns? Another dashboard, another login.

Each operation is a context switch. When you operate multiple services, these friction points compound. I wanted a proxy that didn't hide behind config files — one that exposed everything happening under the hood and let me change routing on the fly, from a browser.

So I built one.

Architecture: Two Processes, One Purpose

The system splits cleanly into two components communicating over HTTP:

  • A Go proxy server on port 8080 — handles all traffic forwarding, caching, metrics, and exposes a REST + SSE API
  • A Next.js dashboard on port 3000 — a Tailwind-styled UI consuming the proxy's API for live monitoring and route management

This separation is deliberate. The proxy runs perfectly without the dashboard — useful for production environments where the UI sits behind its own auth layer.

The Proxy Pipeline

Every incoming request flows through a staged pipeline:

code
Blocklist → Rate Limit → Cache Check → Route Match → Load Balance → Upstream → Cache Store → Response

Each stage is independently configurable and adds zero overhead when disabled.

Blocklist Middleware

Before a request reaches the proxy logic, it passes through a blocklist that rejects known scanner paths (/wp-admin, .env, /phpmyadmin), suspicious file extensions (.php, .asp, .bak), and known bad user agents. The pattern list is exhaustive — covering everything from WordPress exploit scanners to masscan and zgrab probes.

A per-IP sliding window rate limiter follows, with configurable request count and duration. The rate limit log cleans itself up every minute via a background goroutine.

In-Memory TTL Caching

GET responses are cached in a map[string]CacheItem protected by sync.RWMutex. Each cache item stores the full response body, headers, and status code with an expiration timestamp. A background goroutine evicts expired entries every 5 minutes.

On cache hit, the response is served directly from RAM — zero upstream load, sub-millisecond latency. The impact is dramatic: repeated requests to the same endpoint bypass the network entirely.

Dynamic Routing with Longest-Prefix Match

Routes are stored as a map[string][]string — each path prefix maps to one or more upstream targets. The proxy selects the route with the longest matching prefix:

code
/api/users/v2 → https://api-v2.example.com  (wins for /api/users/v2/profile)
/api/users    → https://api.example.com

Routes are editable at runtime via the dashboard's REST API or directly through the UI. Every mutation auto-persists to config.yaml, so routes survive restarts without a database. You can even edit them with vim if the UI is down.

Round-Robin Load Balancing with Retries

Each route can point to multiple upstream targets. The proxy maintains per-route atomic counters for round-robin selection. If an upstream returns a 5xx error, the request retries against the next target — up to a configurable number of attempts.

Real-Time SSE Stream

The dashboard connects via a single Server-Sent Events (SSE) endpoint at /api/stream. An SSEBroker fan-outs a combined state snapshot — metrics, recent events, routes, and health status — to all connected clients every second.

The broadcaster deduplicates: if nothing changed since the last tick, no event is sent. This saves bandwidth and CPU on both sides. If a client is slow, messages are dropped at the channel level — backpressure handled without blocking the broadcaster.

Thread-Safe Metrics

The metrics system uses sync/atomic for lock-free counters — total requests, error count, cache hits and misses, total latency. Derived values (average latency, error rate percentage) are computed on read inside Metrics.Get(). Every hot-path operation uses atomic increment, so there's zero mutex contention even under heavy concurrent load.

Event Ring Buffer

A channel-backed background worker maintains a circular buffer of the last 100 request events. Each event captures the path, status code, latency in milliseconds, cache hit status, and a unique request ID. The buffer is served newest-first and cleared every 24 hours to keep history fresh.

Built-in Proxy Terminal

The dashboard includes a terminal tool for firing GET requests at proxy endpoints directly from the browser. It's a quick smoke-test tool — no curl, no SSH, no leaving the UI to verify a route is working.

Configuration

Everything lives in config.yaml — no environment variables, no command-line flags, no separate config files for different concerns:

yaml
proxy:
    timeout:
        dial: 30s
        response_header: 30s
    retries: 3
cache:
    ttl: 10s
rate_limit:
    requests: 20
    window: 10s
health:
    interval: 30s
    timeout: 5s

The config loader also handles automatic migration from the legacy routes.json format — a small touch that prevented breaking changes during development.

API Surface

| Method | Endpoint | Purpose | |---|---|---| | GET | /api/stream | SSE — live dashboard state snapshot | | GET | /api/metrics | Current metrics (requests, errors, cache hit/miss, latency) | | GET | /api/events | Recent proxied requests (newest first) | | GET | /api/routes | All active route mappings | | POST | /api/routes | Create or update a route | | DELETE | /api/routes?path=... | Remove a route |

What I Learned

The standard library is enough. The entire proxy — routing, caching, load balancing, rate limiting, blocklisting, SSE, health checks — uses zero third-party Go HTTP libraries. httputil.ReverseProxy, sync/atomic, sync.Map, and net/http cover the ground well.

Atomic operations are surprisingly powerful. The metrics system handles concurrent increments from hundreds of goroutines with zero lock contention. You need complex lock-free data structures far less often than you think.

SSE is underrated for internal tools. For a dashboard talking to a single server on the same network, SSE is simpler than WebSockets and more efficient than polling. One connection, one stream, no reconnection logic needed on the client.

Persistence doesn't require a database. For a route map that changes infrequently, writing YAML to disk is simpler, more inspectable, and more portable than embedding a database. It also means you can recover from a broken UI with nothing but a text editor.

Getting Started

bash
# Clone and start the proxy
git clone https://github.com/thanhphan20/go-reverse-proxy
cd go-reverse-proxy
go run cmd/proxy/main.go

# In another terminal, start the dashboard
cd ui
pnpm install && pnpm run dev

Open http://localhost:3000 and you'll see every request flowing through the proxy in real-time — before your first upstream health check completes.