Skip to content

Rate_limiting

Chapter 22: Rate Limiting & Load Balancing

Section titled “Chapter 22: Rate Limiting & Load Balancing”

Rate limiting protects your RPC nodes from abuse and ensures fair resource allocation.


  • Prevent DDoS attacks
  • Protect node resources
  • Ensure fair access
  • Manage costs

geth has no --http.ratelimit / --http.ratelimitburst / --ws.ratelimit / --ws.ratelimitburst flags — per-client rate limiting is not a geth feature and belongs at the reverse proxy (22.3). What geth does provide are structural caps:

Terminal window
# Caps on JSON-RPC batch requests (defaults: 1000 requests / 25 MB per batch)
geth --rpc.batch-request-limit 1000 --rpc.batch-response-max-size 25000000
# Per-call execution guards on the expensive methods
geth --rpc.gascap 50000000 # gas cap for eth_call / eth_estimateGas
--rpc.txfeecap 100 # tx fee cap (in ether) for RPC-submitted txs
--rpc.evmtimeout 5s # per-call EVM execution timeout

These stop a single oversized batch or an infinite eth_call loop from stalling the node — but they do not rate-limit by client IP. That is the job of the reverse proxy.


http {
# Define rate limit zone
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/s;
limit_req_zone $binary_remote_addr zone=write_limit:10m rate=10r/s;
server {
location / {
# Apply rate limit
limit_req zone=api_limit burst=200 nodelay;
}
location /eth_sendTransaction {
limit_req zone=write_limit burst=50 nodelay;
}
}
}

frontend eth_rpc_https
bind :443 ssl crt /etc/ssl/certs/server.pem
default_backend geth_nodes
backend geth_nodes
balance roundrobin
option httpchk GET /health
server geth1 10.0.0.1:8545 check inter 5s rise 2 fall 3
server geth2 10.0.0.2:8545 check inter 5s rise 2 fall 3
server geth3 10.0.0.3:8545 check inter 5s rise 2 fall 3

AlgorithmHow it worksBehavior under burstTypical use
Fixed windowCounter resets every N secondsBurst allowed at window edgeSimple per-IP caps
Sliding windowCounter over a rolling time windowSmooth but more stateBetter fairness
Token bucketBucket fills at rate r, capacity b; each request takes a tokenLimited burst b, then throttledMost common for RPC (Nginx limit_req / proxy)
Leaky bucketRequests queue and drain at fixed rateNever bursts; drops/queues overflowProtecting slow backends

Nginx limit_req_zone is the standard token-bucket implementation for RPC: rate= sets tokens/sec, burst= sets the bucket capacity.

EndpointReasoning
eth_* reads (getBalance, getBlockByNumber)Cheap; allow high rates
eth_call / eth_estimateGasExpensive (executes code); the main abuse vector — cap hard
eth_getLogsCan scan huge ranges; cap fromBlock-toBlock span at the proxy if possible
eth_sendRawTransactionVolume-limited by the mempool; rate-limit to prevent spam
debug_* / trace_*Never expose publicly — they are CPU/disk hogs

StrategyBehaviorBest for
Round-robinDistributes evenlyUniform workloads, HTTP only
Least-connectionsSends to the least busy backendMixed workloads, WS
IP hash / stickyPins a client to one backendWebSocket subscriptions
WeightedMore traffic to bigger nodesHeterogeneous hardware

The health check must test the actual RPC interface, not just TCP:

Terminal window
# /health on each backend via a small script (or haproxy http-check)
curl -s -X POST http://10.0.0.1:8545 \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'
# healthy when a valid hex result is returned
# haproxy.cfg — least-conn + RPC health check + failover
frontend eth_rpc_https
bind :443 ssl crt /etc/ssl/certs/server.pem
default_backend geth_nodes
backend geth_nodes
balance leastconn
option httpchk POST / HTTP/1.1\r\nContent-Type:\ application/json\r\n\r\n{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}
http-check expect string 0x
server geth1 10.0.0.1:8545 check inter 5s rise 2 fall 3
server geth2 10.0.0.2:8545 check inter 5s rise 2 fall 3
server geth3 10.0.0.3:8545 check inter 5s rise 2 fall 3
  • Sticky sessions: WS subscriptions must not migrate mid-connection; on backend failure the client reconnects and resubscribes (which is why the app layer needs reconnect logic — see Chapter 21).
  • Retries: non-idempotent calls (eth_sendRawTransaction — ambiguous outcome after a timeout) should be retried carefully: re-send with the same tx hash is safe (same-tx dedup is handled by the node), but retrying eth_call is fine. Never blindly retry eth_estimateGas after a timeout.
  • Read-after-write: eth_sendRawTransaction may be routed to a node that hasn’t seen the tx in its pool yet — keep the tx hash and expect eth_getTransactionReceipt to be empty until inclusion.

┌─────────────────────────────────────────────────────────────────┐
│ ABUSE DETECTION LAYERS │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. PER-IP CAPS — token bucket (Nginx / reverse proxy) │
│ 2. PER-KEY/PROJECT — API-key quotas (Infura-style) │
│ 3. COST-BASED — heavy methods (eth_call, getLogs) low │
│ 4. BEHAVIORAL — rapid connect/disconnect, parallel │
│ bursts, error floods → temporary ban │
│ 5. TRAFFIC SHAPING — WAF/rate-limit at edge (Cloudflare, │
│ AWS WAF) │
│ │
└─────────────────────────────────────────────────────────────────┘

Concrete signals to watch (see Chapter 36 metrics):

  • increase(rpc_duration_count{...}[5m]) spike from a single IP → query blasts (eth_getLogs over wide ranges).
  • WS connect/disconnect churn → subscription abuse (leaked WS clients).
  • eth_sendRawTransaction flood from one key → mempool spam.
  • Error-rate explosion (4xx/5xx) → protocol misuse or DDoS.

Counter-measures: per-IP ban after N violations at the proxy, API keys with per-project quotas, and heavy-method caps. Never put debug_/trace_ behind a public proxy without authentication.


Observing who calls what, how often, and how slow is what turns a proxy into an RPC platform:

  • Access logs with latency: log every JSON-RPC method, status, duration, and client IP (Nginx log_format or HAProxy logs).
  • Method latency histograms: capture per-method p50/p99 — eth_call and eth_getLogs will dominate; this guides rate-limit tuning.
  • Distributed tracing (OpenTelemetry): propagate a traceparent header from your gateway into downstream services and attach it to node logs; correlate a slow client request with node-side activity (trie reads, EVM execution).
  • Request IDs: require clients to send id and echo it in responses; it makes debugging “which request timed out” trivial.
# nginx — capture per-method timing into Prometheus-friendly logs
log_format rpc '$remote_addr - $request_time - $upstream_response_time - '
'"$request_body"';
access_log /var/log/nginx/rpc-access.log rpc;

  • Rate limit at application level (Geth)
  • Use Nginx for additional protection
  • Load balance across multiple nodes
  • Heavy methods (eth_call, eth_getLogs, debug_*) need tighter caps than cheap reads
  • WS backends need sticky sessions; clients need reconnect/resubscribe logic
  • Abuse detection = per-IP caps + per-key quotas + cost-based limits + behavioral signals
  • Trace every request (method, latency, IP) to tune limits and debug incidents

Last Updated: 2026-10-07