Setting Up Relay for API Rate Limiting: A Step-by-Step Tutorial

Relay protects your services from traffic spikes and abusive clients. This tutorial walks through configuring rate limiting rules, per-client quotas, and burst allowances from scratch.

tutorial
relay
api
rate-limiting
engineering

Rate limiting is one of those things that feels optional until it is not. A single runaway client, a misbehaving integration, or a sudden traffic spike can exhaust your service capacity before you have time to react. Relay is built to sit in front of that problem.

This tutorial walks through a complete Relay setup: installing the gateway, writing your first rate limiting rules, configuring per-client quotas, and testing that everything works before you put it in front of production traffic.

Installing and configuring the Relay gateway

Relay ships as a single binary with no external dependencies. Download the latest release from piqadot.com/app/relay and place it in your PATH.

Start with a minimal configuration file — Relay uses YAML:

gateway:
  port: 8080
  upstream: http://your-service:3000

limits:
  default:
    requests_per_minute: 60
    burst: 10

This tells Relay to listen on port 8080, forward requests to your service at port 3000, and apply a default limit of 60 requests per minute with a burst allowance of 10. Run relay serve --config relay.yaml to start the gateway.

At this point, Relay is proxying traffic and enforcing the default limit. Every client that exceeds 60 requests per minute (plus burst) will receive a 429 Too Many Requests response with a Retry-After header. Your upstream service sees nothing unusual.

Configuring per-client quotas

The default limit applies to all traffic. For most setups, you will want differentiated limits based on client identity — API key, IP address, or a combination.

Add a clients block to your configuration:

clients:
  identify_by: api_key
  header: X-Api-Key

  rules:
    - match: tier:free
      requests_per_minute: 30
      burst: 5
    - match: tier:pro
      requests_per_minute: 300
      burst: 50
    - match: tier:enterprise
      requests_per_minute: unlimited

The identify_by field tells Relay how to distinguish clients. Here we are reading an X-Api-Key header. The match expressions map to metadata you configure in the Relay dashboard or via the admin API — you tag each API key with a tier, and Relay applies the matching rule.

Clients without a recognized key fall through to the default limit. This is intentional: unknown clients get the most conservative quota rather than being blocked outright, which reduces the chance of breaking legitimate traffic during a misconfiguration.

Adding burst allowances

Burst allowances handle the reality of non-uniform traffic. A client might make 10 requests in two seconds during startup, then settle into a steady 1 request per second. Without burst, the startup spike triggers a 429 even though the sustained rate is well within quota.

Relay uses a token-bucket algorithm under the hood. The burst value is the initial bucket size; the requests_per_minute value determines the refill rate. This means a client with requests_per_minute: 60 and burst: 10 can make 10 immediate requests, then is limited to roughly 1 per second thereafter.

For services with predictable startup behavior, set burst to match the expected initialization call count. For services with more variable patterns, a burst of 10-20% of the per-minute limit is a reasonable starting point.

Testing your configuration

Before going to production, verify your limits behave as expected. Relay ships with a built-in load tester:

relay test --config relay.yaml --requests 120 --concurrency 1 --api-key test-key-free

This fires 120 sequential requests using the test-key-free key (which you tag as tier:free in your test environment). With the configuration above, requests 31 through 120 should receive 429 responses. The output shows a breakdown of response codes and timing.

Run the same test with a tier:pro key to confirm the higher quota applies correctly. Once both pass, your configuration is ready for production traffic.