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.