dprl

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 2, 2026 License: MIT Imports: 14 Imported by: 0

README

Distributed Proxy Rate Limiter (dprl)

Go Reference Go Report Card

An HTTP(S) forward proxy that enforces per-host concurrent connection limits. Run it as a local in-memory proxy for a single process, or as a distributed proxy fleet that shares limits through Redis. Good for protecting upstream APIs / services from overload by many workers.

The proxy enforces a hard cap. When a host is over its limit, new connections are rejected and the proxy returns HTTP 500 for those requests.

With LOG_LEVEL=debug enabled, the proxy can act as a lightweight connection tracker since it logs every outbound connection open/close event.

Built on top of the goproxy component: https://github.com/elazarl/goproxy

What it does

  • Acts as an HTTP/HTTPS forward proxy.
  • Tracks and limits active outbound connections per host.
  • Returns 500 if per-host connection limit exeeded.
  • Works both standalone (in-memory) and distributed (Redis).
  • Uses Redis keys per host and per worker to compute a global limit in distributed mode.
What it does not
  • Not an HTTP “requests per second” rate limiter - it caps only connections, not requests.
  • Not an API gateway or L7 router.
  • Does not inspect or MITM HTTPS traffic.

Quick start

Run a local proxy with a global per-host cap:

go install github.com/kotylevskiy/distributed-proxy-rate-limiter/cmd/dprl@latest

dprl --port 8080 --max-connections 25

Or with Docker:

docker build -t dprl .
docker run --rm -p 8080:8080 dprl --max-connections 25

Library usage (Go)

Local in-memory limiter:

package main

import (
	"context"
	"log/slog"
	"net/http"

	dprl "github.com/kotylevskiy/distributed-proxy-rate-limiter"
)

func main() {
	logger := slog.Default() // create logger for proxy internals
	prl := dprl.NewProxyConnectionRateLimiter(
		0, // 0 = auto port
		20, // default max per host
		logger // nil if you don't need it
		) 
	prl.SetHostLimit("api.example.com", 5) // per-host override

	if err := prl.Start(); err != nil { // start in background
		panic(err)
	}
	defer func() { _ = prl.Stop(context.Background()) }() // graceful shutdown

	proxyURL := prl.GetProxyURL() // actual proxy URL with auto-picked port
	client := &http.Client{
		Transport: &http.Transport{
			Proxy: http.ProxyURL(proxyURL), // route requests through proxy
		},
	}
	_, _ = client.Get("https://api.example.com/data") // outbound request
}

Distributed limiter with Redis:

package main

import (
	"log/slog"
	"time"

	dprl "github.com/kotylevskiy/distributed-proxy-rate-limiter"
	"github.com/redis/go-redis/v9"
)

func main() {
	logger := slog.Default() // create logger for proxy internals
	rOpts := &redis.Options{Addr: "127.0.0.1:6379"} // redis connection info

	prl := dprl.NewDistributedProxyRateLimiter(
		8080,       // proxy port
		50,         // default max per host
		rOpts,      // redis options
		5*time.Minute, // safety TTL for worker counters
		logger,     // logger instance
	)
	_ = prl.ListenAndServe() // block and serve until shutdown
}

Configuration scope

Key APIs:

  • SetDefaultMaxConnectionsPerHost(limit int)
  • SetHostLimit(host string, limit int)
  • SetHostLimits(map[string]int)
  • RemoveHostLimit(host string)
  • ActiveConnectionsForHost(host string)
  • Start() / Stop(ctx) / ListenAndServe()

[!IMPORTANT] Per-host limits are configured per proxy instance and are not shared via Redis. Redis is only used for distributed counters, so each proxy must load the same limits to enforce a consistent global cap.

CLI usage

Build and run:

go build -o dprl ./cmd/dprl
./dprl --port 8080 --max-connections 25

Distributed mode (via Redis):

./dprl --redis-addr 127.0.0.1:6379 --max-connections 10
CLI flags and env vars

Each flag can be set via environment variables.

  • --port / DPRL_PORT (default: 8080)
  • --max-connections / DPRL_MAX_CONNECTIONS (default: 0, unlimited)
  • --log-level / LOG_LEVEL (default: info; debug|info|warn|error)
  • --redis-addr / REDIS_ADDR (default: empty = local mode)
  • --redis-password / REDIS_PASSWORD
  • --redis-db / REDIS_DB (default: 0)

Redis keys

Distributed mode uses Redis and stores data under keys:

  • Prefix: proxy_rate_limiter
  • Format: proxy_rate_limiter:<host>:<worker-id>

Each worker tracks its own counter; the Lua script sums all workers for the host and enforces the global limit. The maxLifetime safety TTL protects against stale counters if a worker dies without cleanup.

Docker

Build and run locally:

docker build -t dprl .
docker run --rm -p 8080:8080 dprl --max-connections 25

With Redis:

docker run --rm -p 8080:8080 \
  -e REDIS_ADDR=redis:6379 \
  dprl --max-connections 100

Docker Compose

Minimal example without Redis (local in-memory limits):

services:
  dprl:
    build: .
    ports:
      - "8080:8080"
    environment:
      DPRL_MAX_CONNECTIONS: "25"
      LOG_LEVEL: info

Minimal example with Redis:

services:
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  dprl:
    build: .
    ports:
      - "8080:8080"
    environment:
      REDIS_ADDR: redis:6379
      DPRL_MAX_CONNECTIONS: "100"
      LOG_LEVEL: info
    depends_on:
      - redis

Documentation

Index

Constants

View Source
const RedisKeyPrefix = "proxy_rate_limiter"

RedisKeyPrefix is the base prefix for Redis keys used in distributed mode.

Variables

This section is empty.

Functions

This section is empty.

Types

type ProxyRateLimiter

type ProxyRateLimiter struct {
	// contains filtered or unexported fields
}

ProxyRateLimiter is an HTTP/HTTPS forward proxy that enforces per-host concurrent connection limits locally or across a Redis-backed cluster.

func NewDistributedProxyRateLimiter

func NewDistributedProxyRateLimiter(
	port uint16,
	defaultMaxConnectionsPerHost int,
	rOpts *redis.Options,
	maxLifetime time.Duration,
	logger *slog.Logger,
) *ProxyRateLimiter

NewDistributedProxyRateLimiter creates a Redis-backed proxy rate limiter that enforces per-host concurrent limits across multiple workers. defaultMaxConnectionsPerHost applies ONLY to hosts with no explicit limit set via SetHostLimit/SetHostLimits. 0 or less means "unlimited". maxLifetime is a safety TTL for this worker's Redis counters; avoid 0 unless you are sure cleanup always runs, otherwise stale counters can block traffic.

func NewProxyConnectionRateLimiter

func NewProxyConnectionRateLimiter(port uint16, defaultMaxConnectionsPerHost int, logger *slog.Logger) *ProxyRateLimiter

NewProxyConnectionRateLimiter creates an in-memory proxy rate limiter that enforces per-host concurrent limits without Redis. defaultMaxConnectionsPerHost applies ONLY to hosts with no explicit limit set via SetHostLimit/SetHostLimits. 0 or less means "unlimited".

func (*ProxyRateLimiter) ActiveConnectionsForHost

func (prl *ProxyRateLimiter) ActiveConnectionsForHost(host string) int

ActiveConnectionsForHost returns the current active connection count for a host. In distributed mode, it sums all workers' counters for that host. In local mode, it reads the in-memory map.

func (*ProxyRateLimiter) GetProxyURL

func (prl *ProxyRateLimiter) GetProxyURL() *url.URL

GetProxyURL returns the proxy URL for use in http.Transport.Proxy. Returns nil if the proxy has not been started yet.

func (*ProxyRateLimiter) ListenAndServe

func (prl *ProxyRateLimiter) ListenAndServe() error

ListenAndServe starts the proxy and blocks until it stops. It mirrors http.Server.ListenAndServe semantics.

func (*ProxyRateLimiter) RemoveHostLimit

func (prl *ProxyRateLimiter) RemoveHostLimit(host string)

RemoveHostLimit clears any host-specific limit, falling back to the default.

func (*ProxyRateLimiter) SetDefaultMaxConnectionsPerHost

func (prl *ProxyRateLimiter) SetDefaultMaxConnectionsPerHost(limit int)

SetDefaultMaxConnectionsPerHost sets the fallback per-host limit used when no explicit limit exists via SetHostLimit/SetHostLimits. 0 or less means "unlimited".

func (*ProxyRateLimiter) SetHostLimit

func (prl *ProxyRateLimiter) SetHostLimit(host string, limit int)

SetHostLimit sets or updates the limit for a single host. A non-positive limit removes the host-specific override.

func (*ProxyRateLimiter) SetHostLimits

func (prl *ProxyRateLimiter) SetHostLimits(limits map[string]int)

SetHostLimits replaces all per-host limits with the provided map. Hosts with non-positive limits are ignored.

func (*ProxyRateLimiter) Start

func (prl *ProxyRateLimiter) Start() error

Start begins serving in the background without blocking.

func (*ProxyRateLimiter) Stop

func (prl *ProxyRateLimiter) Stop(ctx context.Context) error

Stop gracefully shuts down the proxy and cleans up this worker's Redis keys. The provided context controls the shutdown deadline.

Directories

Path Synopsis
cmd
dprl command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL