VGOV LLC · Rate limiting for containerized APIs

Every request pays the toll.

Charon is a drop-in reverse proxy that puts rate limiting, abuse detection, and bot mitigation in front of any containerized API — with no code changes to your service, no config language to learn, and no traffic leaving your VPC.

Apache-2.0 core 44 MB container ECS · EKS · Fargate · Docker HTTP/1.1 + h2c

The situation

Three doors, and all of them cost you something.

You have a container serving an API. It needs rate limiting before someone brute-forces your login route or scrapes you into an outage. Today you get three choices, and each one asks for something you'd rather not give.

Managed gateway

AWS API Gateway

Rate limiting works, but you adopt its request/response model, its quotas, and its bill — and bolting it onto a service that already runs happily in a container is not a ten-minute job.

$1.00–$3.50 per million requests, forever

Self-hosted gateway

Kong, Tyk, Envoy

Enormously capable, and priced accordingly — in money and in people. You inherit a config language, a plugin ecosystem, and the standing assumption that someone on staff owns it.

~$105/mo per service, plus $200 per extra million

Edge network

Cloudflare, Fastly

Excellent at the edge — but every request now transits a third party. For teams with data-residency rules, latency budgets, or a security review to pass, that is where the conversation ends.

Your traffic leaves your VPC

The fourth door

Charon

One container, one YAML file, one environment variable pointing at your app. It speaks HTTP/1.1 and h2c, meters every caller, and turns the over-limit ones back before they ever touch your service. It runs inside your VPC as a sidecar or a standalone gateway, and it is Apache-2.0 at the core.

Flat rate per container. No per-request tax.

“Charon is not trying to out-feature Kong. It is trying to be the thing a two-person team deploys on a Tuesday afternoon and never thinks about again.”

— the design brief, verbatim


How it works

Identify the caller. Charge the toll. Forward what's paid for.

Every request takes the same four steps. Nothing reaches your service without passing through all of them, and the whole path is a handful of atomic operations and a sharded map lookup — not a plugin chain.

STAGE 1 Identify Key the caller by IP, API-key header, JWT subject, or any custom header. Honors X-Forwarded-For when you tell it to trust the proxy in front.
STAGE 2 Inspect Burst detection and header-consistency checks catch scrapers and crude bots. Each rule blocks or just logs — your call.
STAGE 3 Charge Token bucket or sliding window, per caller, with tighter limits on the paths that need them — your login route doesn't deserve the same budget as your homepage.
STAGE 4 Forward Paid-up requests stream through to your upstream untouched. Everything is counted in Prometheus on the way past.
200 — crossed, forwarded to your service
429 + Retry-After — turned back at the ferry

Over-limit callers are turned back at the ferry, not at your backend.


Deployment

Three steps, and the third one is optional.

Put Charon in front

Run it as a sidecar next to your app container, or as a standalone gateway in front of the service. Your load balancer points at Charon's port instead of your app's.

Point it at your app

One environment variable — CHARON_UPSTREAM. Your application code does not change. Not one line, not one dependency.

Tune the limits

Edit the YAML and send SIGHUP. Limits, abuse rules, and path overrides reload live; a config with a typo in it is rejected and the running one kept.

charon.yaml — the whole configuration surface
# Where your app lives. The only required field.
upstream: "http://app:3000"

key:
  from: ip            # ip | header | jwt
  trusted_proxy: true  # behind an ALB/NLB

limits:
  algorithm: token_bucket
  rate: 100
  window: "1s"
  burst: 200
  overrides:
    # the login route gets a much shorter leash
    - path_prefix: "/auth/"
      rate: 10
      window: "1s"

abuse:
  burst:
    threshold: 50
    action: block    # or: log
docker — try it against your own service
docker run -p 8080:8080 -p 9110:9110 \
  -e CHARON_UPSTREAM=http://your-api:3000 \
  charon:marketplace
and the toll is already being counted
$ curl localhost:9110/metrics | grep charon_

charon_requests_total{decision="allow"} 14208
charon_requests_total{decision="deny_ratelimit"} 311
charon_requests_total{decision="deny_abuse"} 27
charon_active_keys 642

Metrics and health live on a separate admin port, so your scrapes and health checks are never rate limited and never compete with real traffic.


What ships today

Everything below is in the container you can run right now.

No asterisks, no "available soon" hidden in a footnote. What isn't built yet is on the roadmap further down, named plainly.

Rate limiting

Two algorithms, four ways to key

Token bucket for burst-tolerant traffic, sliding window for strict ceilings. Key on IP, API key, JWT subject, or a custom header — and give individual path prefixes their own tighter budget.

Abuse detection

Burst and bot heuristics

Per-caller burst detection catches credential stuffing and scraping runs. Header-consistency checks catch clients that don't look like the browsers or SDKs they claim to be. Start in log mode, promote to block when you trust it.

Transparent proxy

HTTP/1.1 and h2c, streamed

Bodies stream through rather than buffering, so large uploads and downloads pass untouched. Your service sees the original method, path, query, and headers, plus a correct X-Forwarded-For chain.

Configuration

One YAML file. No DSL.

Everything Charon does is described in about forty lines of YAML, with environment variable overrides for the deployment-shaped fields. Reload with SIGHUP; an invalid edit is refused and the running config survives.

Observability

Prometheus and JSON logs

A Prometheus endpoint and a health check on a dedicated admin port, structured JSON logs to stdout for CloudWatch or whatever you already run, and a Grafana dashboard in the repo to start from.

Posture

Distroless, non-root, in your VPC

A 44 MB distroless image with no shell, running as an unprivileged user. It never phones home and never proxies your traffic through anyone else's network.


The arithmetic

Per-request pricing is a tax on your own growth.

Everyone else meters you by the request, so the better your product does, the more your rate limiter costs. Charon is priced per running container. Traffic doubles; the bill doesn't.

At 100 million requests / month Pricing model Monthly
AWS API Gateway — HTTP API $1.00 per million $100
AWS API Gateway — REST API $3.50 per million $350
Kong Konnect Plus ~$105 per service + $200 per extra million $105 + overage
Cloudflare / Fastly Plan-based Traffic leaves your VPC
Charon Pro — two containers Flat hourly, per container $116

Kong Konnect Plus includes one million requests per gateway service and caps at ten million per month, so a hundred-million-request workload sits on a custom Enterprise contract rather than the list price shown. Competitor figures are their published US-region list prices as of August 2026 and are given for comparison only — check them yourself before making a decision. Charon's own figures are a draft proposal, as flagged below.


Pricing

Flat rate per container. The core is free forever.

Draft pricing — proposed figures, not yet final. Confirm before launch.

Open source
$0 Apache-2.0

The whole rate-limiting core, in production, for free. Not a trial and not crippled — the same binary the paid tiers are built on.

  • Both limiting algorithms, all keying modes
  • Abuse heuristics and path overrides
  • Prometheus metrics and health endpoint
  • Hot reload, unlimited requests, unlimited containers
  • Community support via GitHub issues
Read the source
Pro · AWS Marketplace
$0.08 / container-hour

About $58 per container per month, billed through your existing AWS account. No per-request charges at any volume.

  • Everything in Open Source
  • Shared limits across replicas, backed by Redis or ElastiCache
  • IP reputation feeds
  • TLS client fingerprinting for bot detection
  • JWT signature verification
  • Business-hours support, one business day response
AWS Marketplace — listing in progress
Enterprise
from $18,000 / year

For platforms reselling capacity to their own customers, and for teams who have to hand something to an auditor.

  • Everything in Pro
  • Adaptive thresholds baselined per endpoint
  • Traffic dashboard and blocked-request drill-down
  • Multi-tenant, per-customer limiting
  • SIEM export to Splunk, S3, or CloudWatch
  • Exportable compliance reports
  • Private Marketplace offer and priority support
Talk to us

Pro and Enterprise capabilities are on the roadmap below and are not in the shipping container yet. We would rather tell you that here than have you find out after signing.


Evidence, not testimonials

We have no customer logos to show you yet. Here is the engineering instead.

Charon is new. Rather than borrow credibility we haven't earned, here is what is actually measured and verified in the repository — every number below comes from a test you can run yourself.

automated checks 231 Across CLI, proxy, metrics, reload, container, and packaging suites.
concurrency 200 / 500 500 simultaneous requests against a 200-token budget admitted exactly 200. No over-admission.
latency p99 0.4 ms End-to-end over loopback, including the test upstream's own time.
image size 44 MB Distroless, non-root, no shell in the image.

Denied means denied

The test suite counts upstream hits, not just status codes — proving that a rate-limited or abuse-blocked request never reaches your service at all.

A bad config can't take you down

Reload is tested with deliberately broken YAML: Charon refuses it, logs why, and keeps serving on the last configuration that worked.

The limits are honest

The roadmap below states plainly what is not built yet — single-instance limits, JWT decoded but not verified — because you will find out in production otherwise.


Roadmap

What's shipping, what's next, and what's honestly not built.

Shipping now

The core

  • Token bucket and sliding window
  • Keying by IP, API key, JWT subject, header
  • Per-path limit overrides
  • Burst and header-consistency heuristics
  • HTTP/1.1 and h2c, streamed bodies
  • Prometheus metrics, health, JSON logs
  • SIGHUP hot reload
Next — not yet built

Scale and signal

  • Shared limits across replicas via Redis/ElastiCache
  • TLS JA3/JA4 client fingerprinting
  • JWT signature verification
  • IP reputation feeds
  • Usage metering for Marketplace billing
  • WebSocket upgrade pass-through
Enterprise layer

Commercially licensed

  • Adaptive per-endpoint thresholds
  • Traffic dashboard and drill-down
  • Multi-tenant per-customer limiting
  • SIEM export
  • Compliance report templates

Today, limits are enforced per instance and held in memory, so running several replicas multiplies the effective limit until shared limiting ships. JWT subjects are decoded to identify a caller but signatures are not verified — safe for keying, not a substitute for authentication.


Questions

What teams ask before putting us in the request path.

What happens to my traffic if Charon falls over?

Charon sits in the request path, so treat it like any other proxy: run it as a sidecar so its blast radius is one pod or task, and let your orchestrator restart it. It is a single static binary with no database and no external dependencies, so a restart takes well under a second. Health and metrics are on a separate port from the data plane, so a saturated data plane doesn't blind your monitoring.

Does it terminate TLS?

Not today. Charon speaks plaintext HTTP/1.1 and h2c, which is the normal shape behind an ALB, an NLB doing TLS termination, or a service mesh. Put it behind your existing terminator and it slots in without changing your certificate story.

I run more than one replica. Do my limits still hold?

Not yet, and this is the most important caveat to understand. Limits are currently held in memory per instance, so three replicas means roughly three times the effective limit. If you need a hard global ceiling right now, run Charon as a single standalone gateway rather than as a sidecar per pod. Shared limiting backed by Redis or ElastiCache is the next thing we're building.

How much latency does it add?

The decision path is a handful of atomic counter increments and one sharded map lookup — no allocation, no lock held across the request, no plugin chain. In our own measurements the p99 for the full round trip over loopback, including the test upstream's own processing, was under half a millisecond. Measure it in your own environment: the latency histogram is on the metrics endpoint from the first request.

Do I have to change my application?

No. That is the entire point. Charon is a reverse proxy in front of your service, so your code, your framework, and your dependencies stay exactly as they are. If you want the real client IP in your logs, read the X-Forwarded-For header Charon maintains for you.

Why would I use this instead of Kong or Envoy?

If you already run Kong or Envoy well, you probably shouldn't — they do far more than Charon does and you've already paid the operational cost of learning them. Charon is for the team that needs rate limiting this week, doesn't have a platform engineer, and doesn't want to take on a config language and a plugin ecosystem to get it.

Is the open-source version crippled?

No. The rate limiting core is Apache-2.0 and production-ready, with no request caps, container caps, or time limits. The commercial layer is a separate directory in the repository covering the operational and multi-tenant features — adaptive thresholds, the dashboard, SIEM export — that larger organizations pay for. You can read that code too; you just need a subscription to run it in production.

Does any of my traffic or data leave my network?

None. Charon runs inside your VPC, makes no outbound calls, and has no telemetry or licence phone-home. Metrics stay on your metrics endpoint and logs stay on your stdout.

Get started

Put a toll on your API this afternoon.

Run the container against your own service and watch the metrics endpoint. If it isn't useful in ten minutes, it isn't the product we set out to build.