# freno: a cooperative brake for MySQL replication lag

> freno is a Go service from GitHub Engineering that watches MySQL replication lag and tells clients whether they may write, using raft for high availability and a cooperative model where clients consult it and respect the answer. It suits bulk operations that would otherwise saturate replication, and it does nothing at all if your clients ignore it.

**github/freno** — freno: cooperative, highly available throttler service

- Repository: https://github.com/github/freno
- Stars: 733 · Forks: 64
- Language: Go
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/github-freno

## A brake that the client has to agree to press

freno is a cooperative, highly available throttler service. Clients use it to throttle writes to a resource, and the current implementation throttles writes to one or more MySQL clusters based on replication status for those clusters.

The cooperative part is the whole design, and the documentation is explicit about it. freno collects data from the backend stores, MySQL only at present, and answers the question "may I write to the backend store?" Clients, meaning applications, scripts and jobs, are expected to consult it and respect its recommendation. It is not a proxy between the client and the store; it merely observes the store and states that you are good to write or that you should stop.

That distinction is the one to hold on to. There is no enforcement anywhere in the path. A client that never asks freno will never be throttled by it. So the value of the service is entirely in how many clients you have convinced to call it, and a team evaluating it should treat client adoption as the main work rather than the deployment.

The name explains the rest. Freno is Spanish for brake, as in a car brake, chosen because the obvious names were already taken by other repositories. The story goes that a dictionary lookup turned up the phrase "Echa el freno, magdaleno!", which reminded the authors of the 80s and settled it.

## Metrics are absolute truths, and the thresholds are yours

The data model is per store. When probing MySQL clusters, freno collects replication lag per cluster independently, and backend store metrics are collected automatically and represent absolute truths. That word absolute is doing real work in the design: the measurement comes from the store itself rather than from a client report, so a client cannot lie about the state of replication.

The thresholds sit on the other side of the boundary, in the configuration. freno is configured with a pre-defined list of MySQL clusters, and each entry may include credentials, the lag or other inspection query, and the expected thresholds. When the measured value crosses a threshold, the store is considered not to meet its expected state, and clients are told to stop writing.

There is a second, blunter mechanism above that. Apps can be managed: freno can be instructed to forcibly throttle a certain app, so that other high priority apps can run to completion. The documentation is careful about the limits of this, saying that freno merely accepts instructions on who to throttle and does not have scheduling or prioritisation logic of its own. It is a manual override, not a queue. If you find yourself using it often, the thresholds are probably set wrong.

One design decision follows from the cooperative model and is worth knowing: because freno trusts apps to identify themselves, a client can claim to be any app it likes. The force-throttle instruction is therefore only as trustworthy as the identity behind it.

## Host discovery is static, or parsed out of a haproxy pool

For each cluster, freno needs to know which servers to probe, and the configuration offers two ways of saying so.

The first is a static, hard coded list of hostname[:port] entries. Straightforward, and fine when the topology is fixed.

The second is dynamic, and the documentation explains the reason rather than just the mechanism: hosts may come and go, and throttling may adapt to those changes. You give freno a haproxy URL and a backend or pool name, and it periodically parses the list of enabled servers in that pool so that the set of probed hosts follows the cluster.

This is a smaller feature than it sounds, because it changes what "replication lag" means to the throttle. With a static list, a server that failed over out of the list is simply no longer measured, and the lag that remains measured may not represent the replica that is actually serving reads. With haproxy-driven discovery, the probe follows what the pool is actually routing to. For a cluster with replicas being promoted and demoted regularly, that difference decides whether the throttle fires at the right moment.

The MySQL specific details, including the configuration dissection, live in doc/mysql.md, and the sample configuration file is resources/freno.conf.sample.json, with separate documentation for the general and raft configuration and for the MySQL configuration.

## The check request answers in status codes, and 429 is a normal state

freno serves requests over HTTP, and the most important one is the check request: may this app write to this store. The documentation says the service appreciates HEAD requests, with GET also accepted at more overhead.

The status codes are the entire protocol, and they are worth reading as a set rather than individually.

A 200 means the application may write. A 404 means the metric name is unknown, which is a configuration error rather than a throttling decision. A 417 means the requesting application is explicitly forbidden to write, which is the force-throttle instruction arriving at the client. A 429 means do not write, and the documentation is careful to call it a normal state indicating that the store's state does not meet the expected threshold. A 500 means an internal error, and the instruction is also do not write.

Two of those deserve emphasis. The 429 being normal means a client must not treat it as an exception to be retried blindly, because the whole point is that the store is not ready. The 417 existing at all is what makes the forced throttle of one app work as a mechanism rather than a hint: a client can distinguish being blocked on purpose from being blocked on measurement.

The readme also points at a specialised use of the same interface: determining actual lag in order to infer the validity of replicas, which can help with the write-then-read pain of reading from a master. The details are in doc/http.md.

## How often to ask is a design choice, not a number

Clients will commonly issue /check requests via HEAD, and the documentation addresses the rate question directly. Clients can be expected to issue many requests per second, freno is lightweight in resources, and it should be fine to hit it hundreds of times per second, with the caveat that it depends on your hardware and resources.

Then it gives the more useful advice, which is about matching the granularity of your throttle. If your client is to throttle on a 1000ms replication lag, checking freno 200 times per second may be overdoing it. But if the goal is to keep clients naive and without caching, hitting it frequently is fine.

That is a real design decision being handed to you. A client that caches the answer for a second or two reduces the load on freno dramatically, at the price of writing a little past the threshold. A client that asks on every batch of a bulk job is accurate and chatty. Neither is wrong, and the documentation's framing, that you should ask at the granularity you care about, is the right way to choose.

The use case the service is built for is bulk operations: massive loading or archiving tasks, schema migrations, mass updates. Such operations walk through thousands to millions of rows and can cause replication lag. The documented remedy is to break the task into small subtasks, 100 rows at a time in the example, and consult freno before applying each subtask, which reaches the same result without ill effect on the database or the application using it.

## Raft buys you one writer and a shared view of user events

freno is highly available and uses the raft consensus protocol to decide leadership and to pass user events between member nodes. The nodes compete for leadership, and only the leader collects metrics and should serve clients.

That is the standard raft arrangement, and the design point worth noting is what it does not change. The force-throttle instruction is a user event, and passing those between nodes through raft is what makes the manual override consistent across the cluster: an operator tells one node, and every node agrees. The metrics collection is leader-only, which avoids several nodes hammering the same MySQL cluster independently.

The deployment model follows from that. A freno deployment has several nodes, and the deployment documentation makes recommendations for a recommended setup. Because only the leader serves clients, client requests hit whichever node and are forwarded or fail over to the leader.

The topic list carries high-availability and replication alongside throttle, and the release history shows the project has been around long enough to have a GA release line, with v1.3.1 dated 2023-01-26, v1.3.0 from 2022-09-27 and v1.2.1 from 2021-10-13. The last push to the repository was on 2026-10-16, so the code is moving even though the tagged releases are older.

## Building and running it

The project is a Go module, github.com/github/freno, requiring Go 1.20 and built with the usual suspects: httprouter for the HTTP layer, the MySQL driver, boltdb and patrickmn/go-cache for local state, the hashicorp msgpack and go-metrics libraries, and vitess.io/vitess, which is where the MySQL replication probing comes from. The dependencies are vendored, in a vendor/ directory, and the licence note says the third party dependencies are released under their own licences and are found there.

The Dockerfile shows the deployment shape. The build stage is golang:1.16-alpine and the binary is built with go build -ldflags '-w -s' -o freno cmd/freno/main.go, and the runtime stage is plain alpine with the binary copied to /usr/local/bin/freno and the sample configuration copied to /etc/freno.conf.json. A system user named freno is created, /var/lib/freno is created and owned by it, and the container switches to that user before the entrypoint. The default command is --http.

There is a Dockerfile.test, a docker-compose.yml that builds the test image from it, a build.sh, a .goreleaser.yml for releases, and a script/ directory. The build and entrypoint lines from the Dockerfile are:

```dockerfile
RUN go build -ldflags '-w -s' -o freno cmd/freno/main.go

ENTRYPOINT ["freno"]
CMD ["--http"]
``` The source layout is cmd/ for the entry point, internal/ and pkg/ for the code, conf/ for configuration, doc/ for the documentation files the readme links to, and resources/ for the sample config and other setup material.

A Ruby client for the service is published separately as freno-client, open sourced and available as a Ruby Gem, which is a hint about where this service came from: it was built to serve GitHub's own Ruby applications.

## Where freno is the wrong tool

The first limit is that it only works if clients cooperate, and there is no fallback. This is not a proxy, so a client that does not call it writes as fast as it likes. For a team that cannot modify the code that generates the load, freno is inert, and the alternatives are a query proxy that enforces a limit or a database-side throttling mechanism that the application cannot bypass.

The second limit is MySQL only. The collector handles MySQL and the documentation describes the throttle in terms of replication lag, so a Postgres, DynamoDB or Kafka workload is out of scope. The abstract question the service answers is general, but the implementation is not.

The third is that a 429 is advisory, not a limit, and a client that ignores it will keep writing. For a shared cluster where one application is misbehaving, the force-throttle instruction exists, but it requires the operator to know which app to stop and to identify it correctly, and it stops that app rather than protecting the store from the aggregate.

The fourth is operational. Raft means several nodes to run and monitor, MySQL credentials to manage for the probe, and a configuration file to write per cluster. For a single small database with occasional bulk loads, running a small cluster of a Go service to ask before writing is more machinery than the problem deserves, and a slower migration run on a maintenance window solves the same problem once.

The last point is age. The latest tagged release is v1.3.1 from 2023-01-26, and the module still declares Go 1.20 while the Dockerfile builds with a Go 1.16 base image. The project status section says it is under active development and the last push was on 2026-10-16, so the code moves, but the release line is dated and the build image is older than the module's own requirement.

## Conclusion

freno is worth adopting for a team that runs bulk writes against MySQL and has replication lag as a recurring problem, because it gives every client one authority to ask about write permission, and because the force-throttle instruction lets an operator stop one application without touching the others. It is the wrong tool for an application that cannot be changed to make the call, since freno is not a proxy and a client that ignores the answer writes whenever it likes, and the wrong tool for a non-MySQL backend, since the collector currently handles MySQL only. Before you deploy it, check the three things the documentation puts in front of you: that your cluster's server inventory is either a static list of hostname[:port] entries or discoverable through a haproxy URL and pool name, because the dynamic path parses the enabled servers in that pool; that your client is prepared to treat a 429 as a normal state rather than an error, since 429 means the store is simply not ready; and how much of the operational work you want to own, because the raft setup, the sample config and the deployment docs are all in the repository and are yours to run.

## FAQ

### What does freno do and what does it not do?

It observes MySQL replication lag and answers whether an app may write to a store, but it is not a proxy between the client and the database. Clients are expected to consult it and respect its recommendation, and a client that never calls it will never be throttled.

### What HTTP status codes does the freno check request return?

A 200 means the app may write, 404 means the metric name is unknown, 417 means the app is explicitly forbidden to write, 429 means do not write because the store does not meet its threshold, and 500 means an internal error and also do not write. Clients commonly issue the check as a HEAD request.

### How does freno discover which MySQL servers to probe?

Either with a static, hard coded list of hostname[:port] entries, or dynamically by giving it a haproxy URL and a backend or pool name, in which case it periodically parses the enabled servers in that pool and adapts to hosts coming and going.

## Sources

- [github/freno on GitHub](https://github.com/github/freno)
- [Issues](https://github.com/github/freno/issues)
- [License: MIT](https://github.com/github/freno/blob/master/LICENSE)
- [README](https://github.com/github/freno/blob/master/README.md)
- [Releases](https://github.com/github/freno/releases)

---

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