DEV Community

Cover image for Taming the Surge: Rate Limiting Strategies in Phoenix APIs
HexShift
HexShift

Posted on • Edited on

Taming the Surge: Rate Limiting Strategies in Phoenix APIs

The first time your Phoenix API goes viral isn’t when you celebrate.

It’s when you worry.

That’s when you think about:

  • Abuse
  • Overload
  • Fairness
  • Survivability

And that’s when rate limiting stops being a nice-to-have.

It becomes critical infrastructure.


Rate Limiting Is the Silent Guardian

No alerts.

No crashes.

Just protection.

From what?

  • DoS attacks
  • Scraping bots
  • Infinite loops
  • Misconfigured clients
  • Overeager scripts

With no rate limiting, you’re exposed.

With it, you have control.


Phoenix Doesn’t Pick Your Strategy — You Do

Phoenix doesn’t ship with a rate limiter.

And that’s a good thing.

Why?

Because rate limiting is not one-size-fits-all.

Options range from:

  • ✅ Simple per-IP counters
  • ⚙️ Identity-aware token buckets
  • 🔐 Role-specific quotas
  • ⚡ Redis-backed shared rate stores
  • 🧠 Smart throttlers with burst handling

What Phoenix does give you is a plug pipeline — a perfect place to insert your own limiter.


It All Starts With a Plug

Every request goes through your plug stack before it touches a controller.

So you can:

  • Inspect headers
  • Extract auth tokens
  • Look up API keys
  • Apply policies
  • Enforce limits

All before anything else happens.

defmodule MyAppWeb.Plugs.RateLimiter do
  import Plug.Conn

  def call(conn, _opts) do
    client_id = extract_client_id(conn)

    if over_limit?(client_id) do
      conn
      |> put_resp_header("retry-after", "60")
      |> send_resp(429, "Rate limit exceeded")
      |> halt()
    else
      conn
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Strategy 1: Per-IP Rate Limiting

Basic, useful, fast.

Track requests from a single IP over time:

  • Store in ETS or Redis
  • Reset every minute
  • Block if over threshold

Good for:

  • Public APIs
  • Anonymous endpoints
  • Quick-and-dirty protection

Caveat: NAT and proxies can group multiple users under one IP.


Strategy 2: Identity-Based Rate Limiting

Track users, not locations.

Extract a user ID or token from the request:

client_id = conn.assigns[:current_user].id
Enter fullscreen mode Exit fullscreen mode

Or:

client_id = get_req_header(conn, "authorization") |> parse_token()
Enter fullscreen mode Exit fullscreen mode

Now your limit is about who, not where.

Use Redis for:

  • Atomic increments
  • Expiry
  • Cross-node coordination

Strategy 3: Role-Based or Route-Specific Policies

Customize limits:

Group Limit
Public 60 requests/min
Authenticated 600 requests/min
Admin 6,000 requests/min
Internal Unlimited

Control intent, not just speed.

Apply different rules per route or HTTP verb.


Token Buckets & Burst Handling

Some APIs can handle spikes — but not floods.

Token buckets let clients:

  • Accumulate tokens at a fixed rate
  • Spend them in bursts
  • Refill gradually

Useful for:

  • Download endpoints
  • Search APIs
  • Expensive queries

Use libraries like ExRated or build your own using Redis/ETS.


Observability Matters

You can’t fix what you can’t see.

Log every rejection:

Logger.warn("Rate limit hit", user_id: user.id, ip: conn.remote_ip, path: conn.request_path)
Enter fullscreen mode Exit fullscreen mode

Include headers in responses:

X-RateLimit-Limit: 1000  
X-RateLimit-Remaining: 542  
X-RateLimit-Reset: 1718123456
Enter fullscreen mode Exit fullscreen mode

Show users what’s happening.

Make limits transparent.

Make debugging easier.


Soft Throttling vs Hard Limits

Not all rejections need to be blocks.

Hard limit: Return 429 Too Many Requests.

Soft throttling: Introduce delay but allow the request.

Example:

if over_limit?, do: Process.sleep(200)
Enter fullscreen mode Exit fullscreen mode

It’s not punishment.

It’s system preservation.


Design for Predictability

Bad rate limits surprise people.

Good ones inform them.

That means:

  • Clear documentation
  • Consistent behavior
  • Predictable resets

No one wants a production failure at 3AM because of a quiet 429.


Bonus: Make It a Product Feature

Take it further:

  • Build a dashboard showing usage
  • Send alerts when nearing limits
  • Offer tiered plans with higher caps

This isn’t just backend logic —

It’s a business tool.

Expose rate info through your API:

{
  "rate_limit": {
    "limit": 1000,
    "remaining": 547,
    "reset": "2025-06-12T00:00:00Z"
  }
}
Enter fullscreen mode Exit fullscreen mode

Rate Limiting Is a Form of Stewardship

You’re not being paranoid.

You’re being a responsible platform owner.

It’s about:

  • Keeping things fast
  • Protecting your system
  • Being fair to all users
  • Maintaining uptime under pressure

Phoenix gives you visibility and control.

What you build with that is up to you.


Go Deeper with Phoenix LiveView

If you're serious about building scalable Phoenix apps, get my PDF guide:

📘 Phoenix LiveView: The Pro’s Guide to Scalable Interfaces and UI Patterns

  • 20 pages
  • Advanced architecture tips
  • Reusable LiveView patterns
  • Performance strategies
  • Real-world examples

A must-read if you care about UI that scales like your backend.

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.