惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 三生石上(FineUI控件)
Blog — PlanetScale
Blog — PlanetScale
B
Blog
GbyAI
GbyAI
爱范儿
爱范儿
月光博客
月光博客
N
Netflix TechBlog - Medium
T
Tailwind CSS Blog
G
Google Developers Blog
大猫的无限游戏
大猫的无限游戏
Vercel News
Vercel News
H
Hackread – Cybersecurity News, Data Breaches, AI and More
WordPress大学
WordPress大学
The GitHub Blog
The GitHub Blog
Recent Announcements
Recent Announcements
腾讯CDC
MyScale Blog
MyScale Blog
V
Visual Studio Blog
The Cloudflare Blog
Microsoft Security Blog
Microsoft Security Blog
A
About on SuperTechFans
Google DeepMind News
Google DeepMind News
Last Week in AI
Last Week in AI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻

jola.dev

Migrating your Bluesky account the hard way | jola.dev Cluster singleton pattern | jola.dev cove.town, atproto self-hosted self-hosting | jola.dev Speeding up a Phoenix LiveView web app with a CDN | jola.dev Self-hosting an atproto container registry | jola.dev Migrating to the new Tangled knot2 | jola.dev Self-hosting and Tangled | jola.dev Self-hosting your PDS | jola.dev Taking control of your atproto account | jola.dev No cost, no value | jola.dev Latch - an Elixir atproto OAuth library | jola.dev Limited output is a feature | jola.dev A computer can never be held accountable | jola.dev Elixir Cluster 101 | jola.dev How to stop Claude from saying load-bearing | jola.dev Let libraries be libraries | jola.dev CI workflows on Tangled for Elixir | jola.dev Automatically syncing your blog to atproto and standard.site | jola.dev Appreciation for the small web | jola.dev Treating LLMs as programming books Publishing your blog to standard.site in Elixir Generating OG images in Elixir The social contract of writing Highest Random Weight in Elixir bunnyx: a bunny.net Elixir client library Building for the joy of building Running local models on an M4 with 24GB memory How to hit your Claude weekly limit so you can go outside and touch grass Dropping Cloudflare for bunny.net Building a blog with Elixir and Phoenix
Distributed rate limiter with HRW in Elixir | jola.dev
https://jola.dev/about · 2026-07-29 · via jola.dev

This is a continuation of Elixir Cluster 101. So let's talk about putting what we learned into practice using the case of ratelimiting. Many implementations default to running in memory, meaning that they don't synchronize across multiple nodes. In most programming languages you would immediately reach for something like Redis to tackle this. It’s a great tool for sharing state across nodes, especially where your expectations on consistency and fault tolerance are lower, like the case of rate limits.

But we have the option of avoiding adding another service to our stack: we can take a rate limiter that runs in local memory and make it (mostly) consistent across a cluster of nodes. We do this by using an algorithm for assigning each key, whether IP, user ID, or organization ID, to a specific node, and then ensure that all rate limit lookups are routed to the correct node.

Traditionally this has been done using ExHashRing (the battle-tested consistent hashing implementation for Elixir), but for clusters with less than 10 nodes there’s an alternative that’s potentially even faster and has slightly better distribution: HRW (highest random weight, also known as rendezvous hashing). I wrote about this before on this blog. They both do the same thing, use magic math to associate any given key with a specific node, given a specific set of nodes. And both HRW and consistent hashing share the same incredibly important property: they cause minimal key re-assignment as the list of nodes changes. This means that if you auto-scale a node here and there, it won’t invalidate every key→node assignment, instead just a minimal subset.

Ok, that’s enough of that. Let’s take a look at the example code. I’m using Hammer here, but you can use any rate limiter.

Setting up the Hammer backend.

defmodule HammerBackend do

use Hammer, backend: :ets

end

and then our rate limiter.

defmodule RateLimiter do

use GenServer

require Logger

@scale :timer.minutes(60)

@limit 10

def hit(ip) do

nodes = Cluster.members()

node = HRW.owner(ip, nodes)

GenServer.call({__MODULE__, node}, {:hit, ip})

catch

:exit, reason ->

Logger.warning("Tried to check rate limit but failed", reason: inspect(reason))

# We can fall back to a local check here, but you can also skip the check

# and allow it, and instead ensure the cluster is available most of the time.

hit_internal(ip)

end

def start_link(_opts) do

GenServer.start_link(__MODULE__, [], name: __MODULE__)

end

def init(_opts) do

{:ok, []}

end

def handle_call({:hit, ip}, _from, state) do

{:reply, hit_internal(ip), state}

end

defp hit_internal(ip) do

HammerBackend.hit(ip, @scale, @limit)

end

end

That’s it. That will do the job. The magic part is this

nodes = Cluster.members()

node = HRW.owner(ip, nodes)

GenServer.call({__MODULE__, node}, {:hit, ip})

We grab the latest state of the cluster from our Cluster tracking process that we previously set up, and then delegate to HRW to figure out what node is currently responsible for the given key (IP), and pass the request on to that node. If it’s the same node, it’s delivered to the local mailbox. If it’s not, it’s routed to the correct node and process, and the rate limit lookup is done there. You can even grab the node list directly with and add your own node, Node.list() ++ [Node.self()] as an inline call instead of using the cluster management process. There's no stateful thing to keep track of, like there is for ExHashRing, we can keep things stateless. It's still a good idea to keep track of nodes coming and going though, at least for observability reasons!

Tada! The local only rate limiter is now cluster aware, and will accurately maintain rate limits as long as the cluster is healthy. Redis is great software, but why add it if you don’t need it? Nice.

A reusable pattern

This pattern works across lots of different use cases. We’ve covered rate limiting, but it also works great for caching. Instead of a RateLimiter GenServer we would have a LocalCache one, but the pattern is the same. Another use case I’ve gotten good use of in the past is where you want to cheaply track events in an ephemeral way, for observability that doesn’t quite fit in Prometheus metrics, traces, or logs.

It’s a powerful pattern and low effort to implement, where it makes sense. Hope this is useful to people!