# Bloom, a caching reverse proxy that keeps per-user caches apart

> Bloom is a Rust middleware that sits between your load balancers and your REST API workers, caching read methods in Redis while proxying writes straight through. Its distinguishing feature is that the cache is clustered by authorization token, so a cached response can never leak across users, and the caching rules come from HTTP headers your API already sets.

**valeriansaliou/bloom** — :cherry_blossom: HTTP REST API caching middleware, to be used between load balancers and REST API workers.

- Repository: https://github.com/valeriansaliou/bloom
- Website: https://crates.io/crates/bloom-server
- Stars: 724 · Forks: 49
- Language: Rust
- License: MPL-2.0
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/valeriansaliou-bloom

## One Bloom instance per API worker, sharing one Redis

Bloom is a reverse proxy that you install on the same server as each of your API workers. From the load balancer's point of view there is one Bloom instance per worker, so an existing round-robin setup with health checks keeps working without changes. Each instance can be made visible on its own LAN IP, and Bloom then forwards to the API worker listeners on the local loopback.

The caching store is Redis, configured as a cache, and all instances share the same Redis instance available on the LAN. That arrangement is what makes the cache useful across a fleet: a request that misses on one worker can hit on another.

The proxy behaviour is asymmetric by design. Bloom caches read HTTP methods, which it names as GET, HEAD and OPTIONS, and directly proxies write methods, POST, PATCH, PUT and others, straight to the API worker.

The project states the constraint that matters most in bold in the README: Bloom works well if your API implements REST conventions, and your API needs to use HTTP read methods as read methods, with no HTTP GET parameters used as a way to update data. That single rule decides whether the cache is safe for you.

Bloom is written in Rust and can be compiled to native code for your server architecture.

## Why the cache is clustered by authorization token

Most naive HTTP caching in front of an authenticated API leaks data between users. The same URL returns different content depending on who is asking, and a cache that keys on the URL alone will serve one user's response to another.

Bloom avoids that by clustering the cache by authentication token, using the standard Authorization header, and the documentation states plainly that no cache leak across users is possible. This is the single feature that separates it from a generic caching proxy, and it is why the project argues it does something a load balancer cache cannot.

The argument in the README runs as follows. A simpler approach would have been to enable caching at the load balancer level for read methods, but that would not work with a REST API, because a REST API serves dynamic content that relies heavily on Authorization headers. And any cache needs to be purged at some point, if the content in cache becomes stale because of a data update in some database.

There is a third option the project explicitly declines. NGINX Lua scripts could do the job, but the author's position is that load balancers should be simple and configuration only, without scripting, because they are the entry point to all HTTP and WebSocket services and you would want to avoid frequent deployments and custom code there.

That is a design position rather than a technical fact, and it is a defensible one for a team that would rather not own scripting in the critical path.

## Caching rules live in Bloom-Response headers, not in a config file

Bloom has minimal static configuration and relies on HTTP response headers served by your API workers to configure caching per response. Those headers are formatted as Bloom-Response-*, they are intercepted by Bloom, and they are not passed on to your load balancer responses.

The header set is small. Bloom-Response-Ignore with value 1 disables all cache for an API route. Bloom-Response-Buckets sets the caching buckets for a route, comma-separated when there are several. Bloom-Response-TTL sets the caching TTL in seconds for a route, other than the default TTL.

Buckets are how a route's cache entry is grouped. Cache is stored on buckets, specified in your REST API responses using the Bloom-Response-Buckets header, which means you decide the granularity of invalidation. A route that returns many items can put them in one bucket and expire the bucket, rather than trying to expire entries one by one.

Requests coming from the load balancer can also carry configuration, using Bloom-Request-* headers, with the per-request caching strategy. The main one is Bloom-Request-Shard, which selects the caching shard for an API system, and the default shard is 0 with a maximum value of 15.

The sharding exists so that one Bloom server can serve different API workers at once: a main API can use shard 0 and a search API shard 1, with separate cache namespaces inside the same Redis.

## Bloom-Status, 304 responses and purging from the API

Debugging a cache is where most reverse proxies become opaque, so Bloom exposes a status header. On serving a response to your load balancers, Bloom sets Bloom-Status, which is visible publicly in HTTP responses, with a value of HIT, MISS or DIRECT. The documentation presents it as a help for debugging your cache configuration, and in practice it is the first thing you look at when a route seems to be serving stale content.

Bloom also serves 304 Not Modified for non-modified route contents, which lowers bandwidth usage and speeds up requests to your users.

Invalidation is handled by the API side rather than by the proxy guessing. Cache can be expired directly from your REST API workers via a control channel, so the component that knows a record changed is the one that tells Bloom to drop the affected bucket. Combined with the bucket model, that gives you coarse, cheap invalidation at the granularity you defined in Bloom-Response-Buckets.

For production needs, tooling is provided: bloom-log-analyzer is a separate project for analysing Bloom request logs, which is how you work out which routes are earning their keep and which are just adding latency.

## Installing Bloom from crates, from source or from a package

Bloom ships as a Rust crate named bloom-server, and the binary it produces is called bloom. If you pulled the source from Git, the build is a single cargo command:

```bash
cargo build --release
```

The resulting binaries land in the ./target/release directory. Installing from Cargo is the other route, and the crate name is not the same as the binary name:

```bash
cargo install bloom-server
```

After that you need to make sure your $PATH is configured to source the Crates binaries, and you run the server with the bloom command.

A prebuilt release can also be downloaded from the Bloom releases page, and Debian and Ubuntu packages are available, with the packaging notes in a separate PACKAGING.md file. The Docker route is documented in the repository's Dockerfile, which builds from rust:latest and produces a final image FROM scratch, copying a single binary and running it with the command bloom -c /etc/bloom.cfg, exposing ports 8080 and 8811.

The Dockerfile has a two-track design worth knowing about. If no PREBUILT_TAG build argument is set, it installs musl-tools, adds the musl target for the detected platform and runs a full cargo build. If a PREBUILT_TAG is set, it downloads a release tarball matching the platform instead. Only linux/amd64 and linux/arm64 are supported, and anything else exits the build with an unsupported platform error.

## Rust dependencies, release tuning and what the build costs you

The dependency list in Cargo.toml reads like the architecture of a small proxy. httparse and hyper with the http1 and server features handle the HTTP side, hyper-util provides the legacy client used to talk to your API workers, and tokio provides the multi-threaded runtime. The redis crate is used with the tokio-comp and connection-manager features, which is the asynchronous client with pooled connections.

Two dependencies are there for data handling rather than networking. farmhash provides fast hashing, which is the natural choice for deriving cache keys. zstd is the compression library, so cached payloads can be stored compressed.

The release profile is tuned hard: opt-level 3, lto set to fat, codegen-units at 1, debug assertions off and strip true. The intent is a single native binary with no runtime dependencies, which is why the Docker image can be built FROM scratch.

The project is tested at Rust version rustc 1.91.1, and the crate declares edition 2018 with a dependency floor of hyper 1.9 and tokio 1.52. Nothing in the crate requires nightly Rust, and the configuration file format is TOML, parsed with the toml crate.

## Maintenance, licence and the trade you are making

The repository is not archived and the last push was on 2026-05-25, the same day as the v1.39.2 release. Before that came v1.39.1 on 2026-05-19 and v1.39.0 on 2026-05-15. Three releases in ten days shows an active release cadence, and the version numbers follow a 1.39.x line where the minor component moves as features land.

The licence is MPL-2.0, which is a file-level copyleft licence. It lets you use Bloom in a larger proprietary product without imposing copyleft on the whole product, and it requires you to publish modifications to Bloom's own files. This is not legal advice, but the practical difference from permissive licences is narrow for a deployment: you are not distributing it, you are running it.

The real cost of Bloom is architectural rather than legal. You are adding a process to every API worker host, a Redis instance on the LAN, and a set of headers that your API must emit. That is a fair amount of coordination for a team that could instead put a cache in NGINX with a script. The project's answer is that scripting the load balancer is worse, since it is the entry point for everything.

The case for Bloom is strongest where responses are per-user and authenticated, because that is precisely the case a load balancer cache handles badly. If your API is public and its GET responses are identical for everyone, Bloom's main advantage largely disappears.

## Conclusion

Bloom is a good fit for a team running a REST API behind a load balancer that wants caching without touching the load balancer configuration or writing Lua into NGINX, because the per-response header model keeps all caching decisions inside the API and the authorization-token clustering removes the usual reason to avoid caching. It is a poor fit for an API that mutates state through GET parameters, since Bloom caches those read methods by design, and for a team without a Redis instance or without the willingness to run one process per API worker on the same host. Before deploying, confirm your API only uses GET, HEAD and OPTIONS for reads, decide how your workers will expire entries through the control channel, and pin a Bloom release version rather than tracking master, since the crate version and the release tags are the same numbering scheme.

## FAQ

### How do I install Bloom?

Build it with cargo build --release and use the binary in ./target/release, or install the crate with cargo install bloom-server and then run the bloom command. Prebuilt releases are also downloadable from the Bloom releases page, and Debian and Ubuntu packages are available.

### How does Bloom prevent cached responses from leaking between users?

The cache is clustered by authentication token using the standard Authorization header, so no cache leak across users is possible. That is the reason it sits behind the API rather than in the load balancer, where a URL-keyed cache would mix users.

### Which HTTP methods does Bloom cache and which does it proxy?

Bloom caches the read methods GET, HEAD and OPTIONS, and directly proxies write methods such as POST, PATCH and PUT. Your API must not use GET parameters as a way to update data, because those responses would be cached.

### How do I control what gets cached and for how long?

Your API workers set Bloom-Response-Ignore to disable caching for a route, Bloom-Response-Buckets to define the cache buckets, and Bloom-Response-TTL to set the TTL in seconds. Bloom-Request-Shard in forwarded requests selects the caching shard, with shard 0 as the default and 15 as the maximum.

## Sources

- [License: MPL-2.0](https://github.com/valeriansaliou/bloom/blob/master/LICENSE)
- [Project website](https://crates.io/crates/bloom-server)
- [README](https://github.com/valeriansaliou/bloom/blob/master/README.md)
- [Releases](https://github.com/valeriansaliou/bloom/releases)
- [valeriansaliou/bloom on GitHub](https://github.com/valeriansaliou/bloom)

---

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