Nginx Rate Limiting and DDoS Protection for Tor Hidden Services
Tor hidden services face a unique challenge: all incoming connections appear to originate from 127.0.0.1 (the Tor daemon loopback) rather than client IP addresses. Standard IP-based rate limiting (which limits requests per IP) is useless - you cannot distinguish clients by IP because Tor hides them. Protecting a .onion service from abuse, DoS floods, and scrapers requires Tor-aware rate limiting strategies that do not rely on client IP addresses. Nginx provides several mechanisms that work without IP-based discrimination: connection limits, request rate limits with custom keys, and proof-of-work challenges. Combine these with Tor's own PoW defense (v3 hidden service proof-of-work, available in recent Tor versions) for comprehensive protection. This guide covers implementing all of these layers for a production .onion service.
Need this done for your project?
We implement, you ship. Async, documented, done in days.
Normal Nginx rate limiting uses $binary_remote_addr (the client's IP address) as the rate limit key. For hidden services, all requests come through the Tor daemon at 127.0.0.1, so $binary_remote_addr is always 127.0.0.1. Applying a rate limit of 10r/s to 127.0.0.1 would block all clients simultaneously. The solution is to find alternative keys that distinguish clients without IP addresses. Options: session cookies (for authenticated services), custom client identifiers embedded in request headers or URL parameters, or connection-level limits that throttle based on concurrent connections regardless of key. Each option has tradeoffs: cookies require initial page load before rate limiting activates, custom identifiers require client cooperation, and connection limits do not distinguish individual clients.
Connection-Level Rate Limiting with limit_conn
Nginx's limit_conn module limits concurrent connections per key. For hidden services, configure limit_conn based on the $server_name or a custom header. More practically, limit total concurrent connections to the server regardless of key: limit_conn_zone $server_name zone=total:10m; limit_conn total 200; This limits total concurrent connections from the Tor daemon to 200, which is a server-wide limit. Adjust based on your server capacity. For per-service limits in a multi-tenant setup, use $host as the key to limit per virtual host. Add limit_conn_status 429 to return a proper 'Too Many Requests' response rather than 503. The limit_conn approach protects against connection exhaustion attacks (opening thousands of connections without sending requests) but does not rate-limit actual request throughput.
Cookie-Based Session Rate Limiting
For services where users receive a session cookie on first visit, use the session cookie value as the rate limit key. In Nginx: map $cookie_session_id $rate_limit_key { default $cookie_session_id; '' $request_id; } - this uses the session cookie if present, falling back to $request_id (unique per request) for cookieless clients. limit_req_zone $rate_limit_key zone=per_session:50m rate=30r/m; The 30 requests per minute limit applies per session rather than per IP. New sessions (before cookie assignment) use $request_id as key, which is unique per request - meaning unauthenticated requests are not rate-limited per-session but are limited by total connection limits. After login, session-based limits apply. This effectively rate-limits authenticated users while allowing unauthenticated browsing at connection-level limits.
Tor's Built-In PoW Defense
Tor version 0.4.8+ includes a proof-of-work defense for v3 hidden services. When enabled, the Tor daemon requires connecting Tor clients to solve a computational puzzle before completing the rendezvous. The puzzle difficulty adjusts dynamically based on the rate of introduction cell requests - during a DoS flood, difficulty increases automatically, making the attack expensive without affecting normal users significantly (the puzzle is fast on modern hardware under normal conditions). Enable PoW defense in torrc: HiddenServicePoW enable. Monitor PoW activity in Tor logs: PoW rejection rate and current difficulty level appear in INFO-level logs. The PoW defense operates at the Tor protocol level before any TCP connection reaches Nginx - it blocks flood attacks before they consume Nginx connection slots, making it complementary to Nginx-level rate limiting.
Application-Level Abuse Prevention
Beyond connection and request rate limiting, implement application-level anti-abuse measures: (1) CAPTCHA challenges for form submissions - use a self-hosted CAPTCHA (Friendly Captcha, mCaptcha) that works without clearnet API calls, or implement a simple math challenge in your application, (2) honeypot fields in forms - include hidden input fields that legitimate browsers leave blank but bots fill in, (3) rate-limit expensive operations (search, database queries) more aggressively than page views, (4) implement progressive slowdown - first violation: 1s delay; second: 5s delay; third: temporary block (using a Redis blocklist keyed on session), (5) monitor server resource consumption (CPU, memory, connections) and auto-enable stricter limits when thresholds are exceeded. Log all 429 responses for post-incident analysis. Retain logs in a privacy-preserving format (hash session identifiers before logging) to protect legitimate users who hit limits by mistake.