ideas.
June 21, 2026 3 min read apidev-toolsautomation

Local rate-limit proxy for third-party API calls

A single-binary local reverse proxy that enforces configurable rate limits on outbound API requests, preventing 429s from third-party services without changing a line of application code.

The idea

A lightweight local reverse proxy that sits between your application and any third-party HTTP API, enforcing configurable rate limits on outbound requests before they leave your machine. You point your app at localhost:8080/github instead of api.github.com, and the proxy queues or rejects requests that exceed the configured limit — preventing 429s from reaching your process in the first place.

Config is a single YAML file: list your routes, their target base URLs, and the rate you want enforced. The proxy handles the rest.

Why build this

Hitting third-party rate limits in staging or CI is a solved problem in theory but a persistent pain in practice. Most teams discover limits the hard way — a batch job runs too fast, a test suite hammers an endpoint, or a misconfigured retry loop burns through a daily quota in minutes. Client-side rate-limiting libraries exist per language but require instrumenting every call site and keeping limits in sync across services.

A proxy handles all outbound calls from any process, any language, zero code changes.

The rise of AI APIs with strict per-minute token and request tiers sharpens this problem further. A single runaway batch job can exhaust an OpenAI or Anthropic key in seconds and break everything else sharing it. A proxy gives you a single enforcement point regardless of how many services share a credential.

Stack sketch

  • Runtime: Go — single static binary, ships as a release artifact with no runtime dependencies
  • Config: YAML file listing named routes: target URL, rate limit (e.g., 60/minute), and burst allowance
  • Rate limiter: token bucket via golang.org/x/time/rate; sliding window per route
  • Queuing: in-process channel per route; requests wait up to a configurable timeout before the proxy returns a synthetic 429 with a Retry-After header
  • Logging: structured JSON to stdout — method, route, status, queue wait time, whether throttled
  • Stats endpoint: GET /_proxy/stats returns a JSON snapshot of per-route counters (requests, throttled, queued, queue depth)
  • TLS: HTTP-only by default (app talks HTTPS directly to the real host); optional MITM mode with a generated local root cert for full inspection

Scope for v1

  • YAML config with named routes and per-route token-bucket limits
  • In-process request queue with max-wait timeout
  • Structured stdout logging
  • /_proxy/stats JSON endpoint
  • --dry-run flag that logs what would have been throttled without actually blocking
  • Single static binary for Linux, macOS, and Windows via GitHub Actions release
  • Out of scope: response caching, credential injection, retry logic, a UI, persistent metrics storage

Where it could go

The most natural first extension after v1 is a minimal HTML dashboard at /_proxy/ that plots request volume and throttle events per route over the last hour using a small in-memory ring buffer. This turns the tool from a black-box enforcer into something you can use to tune limits — you see your real traffic shape before committing to a number.

A second direction is replay mode: the proxy records real responses to disk in a structured format, then serves them back in tests without hitting the real API. The transport layer is already in place; recording adds a small write step. Teams already use tools like VCR and WireMock for this, but a proxy-level solution requires no test framework integration.

Watch out for

HTTPS interception requires installing a self-generated root certificate into the OS trust store, which corporate security tooling often flags or blocks — defaulting to HTTP-only proxy mode sidesteps this entirely and covers most development and CI use cases. Token-bucket and sliding-window rate limiting behave differently at burst edges; document which semantics the proxy uses so users don't miscalibrate limits against an API that counts differently.