# API Rate Limiting with Rack Attack: Security Considerations for Rails APIs

> Secure your Rails API with Rack Attack. Explore rate limiting security: hashed tokens, tiered throttling, and opaque errors.

- Repository: [Maybe/maybe](https://github.com/maybe-finance/maybe)
- Tags: best-practices
- Published: 2026-03-07

---

**Implementing API rate limiting with Rack Attack requires conditional environment activation, cryptographically hashed token keys, tiered throttling strategies for self-hosted versus managed deployments, and opaque error responses to prevent information leakage.**

The maybe-finance/maybe repository uses Rack Attack to protect its public APIs and OAuth endpoints from abusive traffic and credential-stuffing attacks. The configuration in [`config/initializers/rack_attack.rb`](https://github.com/maybe-finance/maybe/blob/main/config/initializers/rack_attack.rb) implements a defense-in-depth strategy that adapts limits based on deployment mode while preventing token exposure in cache keys. Understanding these security considerations helps developers implement robust rate limiting without compromising usability or leaking implementation details.

## Core Security Architecture

### Environment-Based Activation

The middleware loads only in production and staging environments to prevent accidental throttling during local development. In [`config/initializers/rack_attack.rb`](https://github.com/maybe-finance/maybe/blob/main/config/initializers/rack_attack.rb), the activation logic checks the Rails environment before applying any rules:

```ruby
enabled = Rails.env.production? || Rails.env.staging?

```

This conditional approach reduces developer friction while guaranteeing protection where the service faces real traffic. Local development and test environments bypass the throttling layer entirely, allowing rapid iteration without hitting arbitrary request limits.

### OAuth Token Endpoint Protection

The OAuth token endpoint represents a high-value target for brute-force attacks. The configuration applies a strict throttle specifically to `/oauth/token`:

```ruby
throttle("oauth/token", limit: 10, period: 1.minute) do |request|
  request.ip if request.path == "/oauth/token"
end

```

This rule limits each IP address to **10 requests per minute**, mitigating credential-stuffing attempts while accommodating legitimate client authentication flows. By targeting this specific path separately from general API traffic, the system prioritizes protection for the most sensitive authentication surface.

## Tiered Throttling Strategy

### Self-Hosted vs. Managed Mode Configuration

The application distinguishes between self-hosted and managed (SaaS) deployments using `Rails.application.config.app_mode.self_hosted?`. This determination dramatically adjusts rate limits based on threat models:

- **Self-hosted mode**: 10,000 requests/hour per token, 20,000 per IP
- **Managed mode**: 100 requests/hour per token, 200 per IP

Self-hosted instances assume a smaller, trusted user base where the deployment owner already controls the infrastructure. Managed mode applies stricter limits to protect the shared multi-tenant environment from abuse.

### Token-Based Throttling with SHA256 Hashing

The primary defense mechanism throttles requests based on API tokens while preventing credential leakage. In [`config/initializers/rack_attack.rb`](https://github.com/maybe-finance/maybe/blob/main/config/initializers/rack_attack.rb) (lines 15-27), the implementation extracts bearer tokens and hashes them before using them as cache keys:

```ruby
throttle("api/requests", limit: self_hosted ? 10_000 : 100, period: 1.hour) do |request|
  if request.path.start_with?("/api/")
    auth_header = request.get_header("HTTP_AUTHORIZATION")
    if auth_header&.start_with?("Bearer ")
      token = auth_header.split(" ").last
      "api_token:#{Digest::SHA256.hexdigest(token)}"
    else
      "api_ip:#{request.ip}"
    end
  end
end

```

**Hashing the token** with `Digest::SHA256.hexdigest` ensures raw bearer tokens never appear in Rack Attack's internal cache keys, logs, or monitoring dashboards. This cryptographic step prevents token exposure even if the rate-limiting store is compromised or logged.

### IP-Based Fallback Throttling

A secondary, more permissive IP-based bucket catches requests missing valid authentication headers. This fallback protects the API during development and testing scenarios where generating real tokens may be cumbersome:

```ruby
throttle("api/ip", limit: self_hosted ? 20_000 : 200, period: 1.hour) do |request|
  request.ip if request.path.start_with?("/api/")
end

```

This dual-bucket approach ensures that unauthenticated or improperly authenticated requests still face limits, preventing unlimited anonymous traffic from overwhelming the service.

## Threat Mitigation Techniques

### Blocklisting Malicious User Agents

The configuration implements an early kill-switch for automated reconnaissance tools. Known scanning utilities are outright denied with 403 Forbidden responses:

```ruby
blocklist("block malicious requests") do |request|
  suspicious_user_agents = [/sqlmap/i, /nmap/i, /nikto/i, /masscan/i]
  user_agent = request.user_agent
  suspicious_user_agents.any? { |p| user_agent =~ p } if user_agent
end

```

Blocklisting tools like **sqlmap**, **nmap**, **nikto**, and **masscan** stops automated reconnaissance that often precedes targeted attacks. This reduces load on downstream services and prevents information gathering by potential adversaries.

### Response Obfuscation and Retry Logic

Both throttled and blocked responses return generic JSON errors without revealing which specific rule triggered the action. The throttled response (HTTP 429) includes a `Retry-After: 60` header:

```ruby
Rack::Attack.throttled_responder = lambda do |env|
  retry_after = (env['rack.attack.match_data'] || {})[:period]
  [
    429,
    {
      'Content-Type' => 'application/json',
      'Retry-After' => retry_after.to_s
    },
    [{ error: 'Rate limit exceeded. Try again later.' }.to_json]
  ]
end

```

The blocked response (HTTP 403) returns an equally generic message. This **security through obscurity** technique prevents attackers from reverse-engineering rate-limit thresholds or distinguishing between different types of restrictions.

## Validation and Testing

### Integration Test Coverage

The [`test/integration/rack_attack_test.rb`](https://github.com/maybe-finance/maybe/blob/main/test/integration/rack_attack_test.rb) file validates that the middleware remains present and correctly configured. These tests assert that expected throttle keys (`oauth/token`, `api/requests`) are registered and functional:

```ruby
test "rack attack is enabled" do
  assert Rack::Attack.enabled
end

test "oauth/token throttle exists" do
  throttle = Rack::Attack.throttles["oauth/token"]
  assert throttle
end

```

This test coverage protects against accidental removal of security controls during refactoring, ensuring that API rate limiting with Rack Attack remains active as the codebase evolves.

## Implementation Examples

### Handling Rate-Limit Responses in Ruby

When consuming the Maybe API, client applications should implement exponential backoff based on the `Retry-After` header:

```ruby
require 'net/http'
require 'json'

uri = URI('https://app.maybe.co/api/v1/accounts')
req = Net::HTTP::Get.new(uri)
req['Authorization'] = "Bearer #{access_token}"

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }

if res.code == '429'
  retry_after = res['Retry-After'].to_i
  puts "Rate limit hit – retry after #{retry_after}s"
  sleep(retry_after)
  # retry the request ...

elsif res.is_a?(Net::HTTPSuccess)
  puts JSON.parse(res.body)
else
  puts "Error: #{res.code} #{res.message}"
end

```

### Testing API Limits with Curl

To verify throttle behavior manually:

```bash
curl -i -H "Authorization: Bearer <TOKEN>" https://app.maybe.co/api/v1/transactions

```

Exceeded limits return:

```http
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 60

{"error":"Rate limit exceeded. Try again later."}

```

### Adding Custom Throttling Rules

Developers extending the Maybe codebase can add per-user throttling in the initializer:

```ruby

# Add a per-user login throttling rule (example only)

Rack::Attack.throttle('user/login', limit: 5, period: 20.seconds) do |req|
  req.ip if req.path == '/login' && req.post?
end

```

## Summary

- **Environment-aware activation** ensures rate limiting applies only to production and staging, preventing development friction while maintaining production security.
- **Cryptographic hashing** of bearer tokens in cache keys prevents credential leakage through logs or monitoring systems.
- **Tiered deployment modes** adjust limits dynamically based on self-hosted versus managed infrastructure threat models.
- **Dual throttling layers** combine token-based primary limits with IP-based fallback protection for comprehensive coverage.
- **Aggressive blocklisting** of known scanning tools stops automated reconnaissance before it reaches application logic.
- **Opaque error responses** with standardized `Retry-After` headers prevent information leakage while enabling client-side backoff strategies.
- **Comprehensive test coverage** in [`test/integration/rack_attack_test.rb`](https://github.com/maybe-finance/maybe/blob/main/test/integration/rack_attack_test.rb) prevents accidental security regression.

## Frequently Asked Questions

### How does Rack Attack prevent token leakage in rate-limiting keys?

According to the maybe-finance/maybe source code, the implementation hashes bearer tokens using `Digest::SHA256.hexdigest` before using them as cache keys in the `api/requests` throttle rule. This ensures that raw tokens never appear in Redis or Memcached store entries, protecting credentials even if the rate-limiting backend is compromised or logs are exposed.

### Why are the rate limits different for self-hosted versus managed deployments?

The configuration checks `Rails.application.config.app_mode.self_hosted?` to distinguish deployment modes. Self-hosted instances receive significantly higher limits (10,000 requests/hour) because the deployment owner controls the infrastructure and typically serves a smaller, trusted user base. Managed SaaS deployments use stricter limits (100 requests/hour) to protect the shared environment from abuse by untrusted tenants.

### What happens when the OAuth token endpoint throttle is triggered?

When a client exceeds the 10 requests per minute limit on `/oauth/token`, Rack Attack returns an HTTP 429 Too Many Requests response with a `Retry-After: 60` header and a generic JSON error message. This mitigates credential-stuffing attacks without revealing whether the credentials were valid, while allowing legitimate clients to retry after the cooldown period.

### How does the blocklist feature enhance API security?

The blocklist rule in [`config/initializers/rack_attack.rb`](https://github.com/maybe-finance/maybe/blob/main/config/initializers/rack_attack.rb) rejects requests containing user agents matching known security scanning tools like sqlmap, nmap, nikto, and masscan with an immediate HTTP 403 Forbidden response. This acts as an early kill-switch that prevents automated reconnaissance and reduces load on downstream application servers by stopping malicious traffic at the middleware layer.