require "rack/attack"
require "redis-store"
class Rack::Attack
cache.store = Redis::Store.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/1"))
safelist("allow localhost") do |req|
["127.0.0.1", "::1"].include?(req.ip)
end
throttle("req/ip", limit: 100, period: 60) do |req|
req.ip unless req.path.start_with?("/assets")
end
throttle("logins/token", limit: 5, period: 60) do |req|
if req.post? && req.path == "/login"
req.env["HTTP_X_API_TOKEN"] || req.ip
end
end
self.throttled_responder = lambda do |req|
match = req.env["rack.attack.match_data"] || {}
now = Time.now.to_i
retry_after = match[:period] - (now % (match[:period] || 1))
headers = {
"Content-Type" => "application/json",
"Retry-After" => retry_after.to_s,
"RateLimit-Limit" => match[:limit].to_s,
"RateLimit-Remaining" => [(match[:limit] || 0) - (match[:count] || 0), 0].max.to_s
}
body = { error: "rate_limited", retry_after: retry_after }.to_json
[429, headers, [body]]
end
end
require "sinatra/base"
require "json"
require_relative "config/rack_attack"
class ApiApp < Sinatra::Base
use Rack::Attack
configure do
set :show_exceptions, false
end
before do
content_type :json
end
post "/login" do
token = request.env["HTTP_X_API_TOKEN"]
halt 401, { error: "invalid_token" }.to_json unless Account.authenticate(token)
{ status: "ok", session: SecureRandom.hex(16) }.to_json
end
get "/search" do
query = params.fetch("q", "").strip
halt 422, { error: "missing_query" }.to_json if query.empty?
{ query: query, results: SearchIndex.query(query) }.to_json
end
error do
status 500
{ error: "internal_error" }.to_json
end
end
require "rubygems"
require "bundler/setup"
ENV["RACK_ENV"] ||= "development"
Bundler.require(:default, ENV["RACK_ENV"].to_sym)
require_relative "app"
use Rack::Deflater
run ApiApp
This snippet shows how a Sinatra JSON API enforces per-client rate limits using Rack::Attack, a Rack middleware that intercepts requests before they reach the application. Rate limiting protects an endpoint from abuse, accidental request storms, and credential-stuffing by capping how many requests a given key (IP or API token) may make inside a rolling window. The pattern is attractive because it lives entirely in middleware: no route handler needs to know about it, and the cap is enforced uniformly across the app.
In config/rack_attack.rb, the store is set to a Redis::Store via Rack::Attack.cache.store, which matters because throttle counters must be shared across every worker process and server — an in-memory store would let each process count independently and multiply the real limit. The throttle block for "req/ip" returns a discriminator (req.ip) for general traffic, while "logins/token" narrows to POST /login and keys on the API token so authentication attempts are limited independently of read traffic. Returning nil from a throttle block skips that rule for the request, which is how the login throttle ignores unrelated paths.
The throttled_responder builds a proper 429 Too Many Requests response with a JSON body and Retry-After, RateLimit-Limit, and RateLimit-Remaining headers derived from match_data, so well-behaved clients can back off instead of hammering. The safelist for localhost keeps development and health checks from being throttled.
In app.rb, the Sinatra application simply use Rack::Attack after requiring the config. Because middleware ordering is significant, Rack::Attack is inserted early so throttling happens before routing and session work. The /login and /search routes stay oblivious to the limiter — they only return data. The config.ru rackup file wires the environment and boots the app.
A key trade-off is that fixed-window counting (what throttle uses) can allow short bursts at window boundaries; for stricter smoothing a token-bucket approach would be needed. Another pitfall is keying on req.ip behind a proxy, where the real client address must come from a trusted forwarded header rather than the socket peer.
Related snips
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
Share this code
Here's the card — post it anywhere.