# envoyproxy/ratelimit: the gRPC service that answers Envoy's rate limit questions

> A Go service that takes a domain and a set of descriptors, composes a cache key, and asks Redis whether the caller is over its limit. The odd part is its release history: no version tag since 2020, yet an image is published for every main commit.

**envoyproxy/ratelimit** — Go/gRPC service designed to enable generic rate limit scenarios from different types of applications.

- Repository: https://github.com/envoyproxy/ratelimit
- Stars: 2,698 · Forks: 537
- Language: Go
- License: Apache-2.0
- Published: 2026-10-06 · Updated: 2026-10-06 · Language: en
- Canonical page: https://hysenlabs.com/projects/envoyproxy-ratelimit

## What the service decides on a single request

The README describes the request path in four steps, and it is short enough to state plainly. An application asks for a rate limit decision based on a domain and a set of descriptors. The service reads configuration from disk through the `goruntime` library, composes a cache key, talks to the Redis cache, and returns a decision to the caller.

That is the whole mechanism, and it explains the design. The expensive part of rate limiting is not counting, it is deciding what counts: a gRPC descriptor list identifies the bucket, and the configuration maps that descriptor to a set of thresholds. Two callers in different domains hit different keys, so the same HTTP path can carry a tight limit for one tenant and an open one for another.

The service handles the negative case too. The README has a section on negative hits, which is how a request below the threshold still gets recorded. Skip that bookkeeping and your counts drift, which is the class of bug that only surfaces under load.

The supported contract is the Envoy v3 `rls.proto`. Support for the v2 proto is explicitly deprecated, and the README keeps an API deprecation history that notes the service had run in production at Lyft for over two years before v1.0.0 was tagged.

## Configuration arrives from disk or from an xDS management server

Two configuration paths are documented, and the choice determines how you operate the thing. File based loading reads YAML from a directory on local disk. xDS management server based loading takes the same definitions from a control plane over xDS, which is what you want when limits change often or several instances must agree.

The disk path is served by `github.com/lyft/goruntime`, visible in `go.mod`, and the environment variables that point at it appear in the docker-compose file:

```yaml
    environment:
      - USE_STATSD=false
      - LOG_LEVEL=debug
      - REDIS_SOCKET_TYPE=tcp
      - REDIS_URL=redis:6379
      - RUNTIME_ROOT=/data
      - RUNTIME_SUBDIRECTORY=ratelimit
```

`RUNTIME_ROOT` plus `RUNTIME_SUBDIRECTORY` is the pair to understand: the first is the mounted directory and the second selects the subdirectory limits are read from. The compose file mounts `./examples` at `/data`, so the examples directory in the repository is a working configuration set rather than documentation.

The configuration vocabulary is spelled out in the README's own table of contents: definitions, a descriptor list definition, a rate limit definition, replaces, and ShadowMode. `replaces` is the one to watch, because it is how one descriptor's limit substitutes for another rather than adding to it.

## The docker-compose file describes the real dependency set

`docker-compose.yml` is short and more informative than the overview paragraph. It runs three services: `redis`, `memcached` and `ratelimit`, joined by a network called `ratelimit-network`. The service publishes three ports, 8080, 8081 and 6070, and the README documents separate sections for an HTTP port with a `/json` endpoint, a debug port, and a gRPC server.

Only one dependency is declared:

```yaml
    depends_on:
      - redis
```

So Redis is the hard requirement in this example, while memcached is configured but never waited on. Memcache is a documented alternative and the compose file sets `MEMCACHE_HOST_PORT=memcached:11211`, which suggests the service supports either backend rather than needing both. There is also a documented Local Cache section, an in-process layer in front of the shared cache.

The README is unusually thorough about failure modes here, with dedicated sections on connection pool settings, pool size, connection timeout, pool on-empty behaviour, pipelining, health checking for active connections, and recovering from a failover that produces READONLY errors. That last one deserves attention before any Redis promotion: a client reconnecting to a promoted replica can hit READONLY on writes, and the README documents recovery from exactly that situation.

## ShadowMode for finding out what a limit would have done

ShadowMode appears twice in the README's structure, once as a property inside a rate limit definition and once as a global setting with its own configuration and statistics sections. Having both is the point: you can enable it for one descriptor or for the whole service.

The mechanism is the standard dry-run approach to rate limiting. The service still counts and still evaluates the limit, and still records the outcome, but instead of rejecting the caller it lets the request through and reports what would have happened. Statistics then show how many requests would have been limited. You get real traffic data before turning enforcement on, which is the only sensible order for a limit that might take down paying customers.

The same reasoning applies to the statistics options documented alongside it. DogStatsD and Prometheus are both supported, and the README includes an example configuration for each, so the counting layer is designed to be inspected rather than inferred from logs.

## Why the newest version tag is from March 2020

The release list ends at v1.4.0, published on 2020-03-16, after v1.3.0 in 2018 and v1.2.0 in 2018. Read on its own that suggests an abandoned project. The README says otherwise, in a short paragraph worth reading carefully because it explains the entire release policy: for every main commit an image is pushed to Dockerhub, and there is currently no versioning after v1.4.0, with tags based on commit sha.

So the project did not stop releasing, it stopped versioning. The operational consequence is concrete. You cannot pin a version number, and the tags move. What you can do is pin an image digest, which the Dockerfile already treats as normal practice because its base image is pinned that way.

The Dockerfile shows the same attention to supply chain as the README:

```dockerfile
RUN CGO_ENABLED=0 GOOS=linux go build -o /go/bin/ratelimit -ldflags="-w -s" -v github.com/envoyproxy/ratelimit/src/service_cmd
```

That one line names the entry point package, `src/service_cmd`, and the repository tree agrees, with `src/` holding the implementation and `api/` the service definitions. The last push was on 2026-09-23, so the commit stream the release policy depends on is still running.

## Two details that will cost you an afternoon

The first is the distroless base image. The image uses `gcr.io/distroless/static-debian12:nonroot`, pinned to a specific SHA digest, and runs as UID 65532. The README lists the reasons, a minimal attack surface, fewer components, non-root execution and deterministic builds, then concedes the cost: there is no shell for debugging. The remedy is a separate debug variant, documented as its own snippet:

```dockerfile
FROM gcr.io/distroless/static-debian12:debug
COPY --from=build /go/bin/ratelimit /bin/ratelimit
```

Decide which variant you deploy before you need a shell inside a running container.

The second is the TLS bootstrap in the Makefile. The `bootstrap_redis_tls` target generates a certificate with `openssl`, concatenates key and certificate into `private.pem`, then runs `sudo cp cert.pem /usr/local/share/ca-certificate`. A development convenience that writes into the machine's trusted certificate store will not run in a locked-down CI image or a container without `sudo`. `Makefile.local` is included at the top of the Makefile, so overriding such settings locally is anticipated. Build variables are also platform-scoped: `BUILDX_PLATFORMS` lists `linux/amd64` and `linux/arm64/v8`, so multi-architecture builds are part of the release path rather than an afterthought.

## Conclusion

The service is a good fit when you run Envoy and want limits defined as data rather than as code, since descriptors and thresholds live in configuration files and an xDS management server can push them. It is not a general-purpose quota library, and it is not much use outside an Envoy deployment, because the wire contract is the Envoy v3 rls.proto service. Before you plan around its release history, note that the newest version tag is v1.4.0 from 2020-03-16 while images are pushed to Dockerhub for every main commit with tags based on commit sha, which means you will be pinning a digest rather than a version. Start from the docker-compose file, where the three ports and the Redis, log and runtime-root variables are all spelled out.

## FAQ

### What does the Envoy rate limit service actually do?

It returns a yes-or-no decision for a caller identified by a domain and a set of descriptors. The service reads its configuration from disk, composes a cache key from those descriptors, consults Redis, and returns the decision over the Envoy v3 rls.proto gRPC interface.

### Do I need Redis to run the envoyproxy rate limit service?

In the documented docker-compose setup, yes. That file declares `depends_on: redis`, and the overview states that the service talks to the Redis cache. Memcached is supported as an alternative and a Local Cache layer is documented as well, but Redis is the dependency the example waits on.

### How is rate limit configuration loaded into the service?

Two ways are documented. File based loading reads limits from a directory on disk, located with `RUNTIME_ROOT` and `RUNTIME_SUBDIRECTORY`, which is how the docker-compose example works. The alternative is loading the same definitions from an xDS management server, which suits environments where limits change frequently.

### What is ShadowMode in the Envoy rate limit service?

It is a dry-run mode that can be set per rate limit definition or globally for the service. The service keeps counting and evaluating limits and records the outcome, but lets requests through instead of rejecting them, with statistics reporting how many requests would have been limited.

## Sources

- [envoyproxy/ratelimit on GitHub](https://github.com/envoyproxy/ratelimit)
- [Issues](https://github.com/envoyproxy/ratelimit/issues)
- [License: Apache-2.0](https://github.com/envoyproxy/ratelimit/blob/main/LICENSE)
- [README](https://github.com/envoyproxy/ratelimit/blob/main/README.md)
- [Releases](https://github.com/envoyproxy/ratelimit/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/envoyproxy-ratelimit
