Dlaczego rate limiting jest niezbędny

Każde publiczne API jest narażone na nadużycia — od przypadkowego zalania serwera źle napisanym klientem, przez web scraping, aż po celowe ataki DDoS. Rate limiting to mechanizm ograniczający liczbę żądań, jakie dany klient może wysłać w określonym czasie. Bez niego nawet dobrze zaprojektowana aplikacja może paść ofiarą przeciążenia.

W naszych usługach stosujemy rate limiting na dwóch poziomach: w warstwie Nginx (jako pierwsza linia obrony) oraz wewnątrz aplikacji Go (dla bardziej precyzyjnej kontroli). Takie podwójne podejście zapewnia zarówno ogólną ochronę infrastruktury, jak i granularną kontrolę na poziomie endpointów.

Algorytmy rate limitingu

Istnieje kilka popularnych algorytmów ograniczania ruchu. Każdy ma swoje zalety i wady — wybór zależy od specyfiki aplikacji.

Token bucket to najpopularniejszy algorytm. Wyobraź sobie wiadro z żetonami — każde żądanie zużywa jeden żeton, a wiadro jest napełniane ze stałą szybkością. Jeśli żetony się skończą, kolejne żądania są odrzucane. Kluczowa cecha: algorytm pozwala na krótkie serie (burst) żądań, dopóki w wiadrze są żetony. To naturalnie odpowiada zachowaniu rzeczywistych użytkowników — przeglądają stronę, klikają kilka linków naraz, a potem czytają treść.

Sliding window (okno przesuwne) liczy żądania w ruchomym oknie czasowym. W przeciwieństwie do prostego fixed window, nie ma tu problemu z podwójnym limitem na granicy okien. Jeśli ustawimy limit 100 żądań na minutę, to w dowolnym 60-sekundowym przedziale klient nie przekroczy tego limitu. Algorytm jest bardziej sprawiedliwy, ale wymaga przechowywania timestampów poszczególnych żądań.

Leaky bucket (cieknące wiadro) działa jak kolejka FIFO z ograniczoną przepustowością. Żądania trafiają do kolejki i są przetwarzane ze stałą szybkością. Nadmiarowe żądania, które nie mieszczą się w kolejce, są odrzucane. Ten algorytm gwarantuje idealnie równomierny ruch wychodzący — przydatne, gdy backend nie radzi sobie z nagłymi skokami obciążenia.

Rate limiting w Nginx

Nginx oferuje wbudowany moduł ngx_http_limit_req_module, który implementuje algorytm leaky bucket. Konfiguracja jest prosta i skuteczna:

http {
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

    server {
        location /api/ {
            limit_req zone=api burst=20 nodelay;
            limit_req_status 429;
            proxy_pass http://backend;
        }
    }
}

Parametr zone=api:10m tworzy strefę pamięci współdzielonej o rozmiarze 10 MB, identyfikującą klientów po adresie IP. Starcza to na około 160 000 unikalnych adresów. Parametr rate=10r/s ustawia limit na 10 żądań na sekundę. Dyrektywa burst=20 pozwala na chwilowe przekroczenie limitu o 20 żądań, a nodelay oznacza, że te dodatkowe żądania są przetwarzane natychmiast, zamiast być kolejkowane.

Ważny szczegół: parametr limit_req_status 429 ustawia kod odpowiedzi na 429 Too Many Requests zamiast domyślnego 503. To poprawna semantycznie odpowiedź HTTP, którą klienci API mogą właściwie obsłużyć.

Implementacja w Go

W aplikacjach Go używamy pakietu golang.org/x/time/rate, który implementuje algorytm token bucket. Jednak w naszych projektach preferujemy rozwiązania bez zewnętrznych zależności. Prosty rate limiter można zbudować na bazie standardowej biblioteki:

type RateLimiter struct {
    visitors map[string]*visitor
    mu       sync.Mutex
    rate     int
    window   time.Duration
}

type visitor struct {
    count    int
    lastSeen time.Time
}

Kluczowe elementy implementacji to mapa przechowująca informacje o klientach (identyfikowanych po IP), mutex chroniący dostęp współbieżny oraz goroutine czyszcząca nieaktywne wpisy, aby uniknąć wycieku pamięci.

Middleware rate limitujący w Go wygląda naturalnie dzięki wzorcowi http.HandlerFunc:

func (rl *RateLimiter) Limit(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ip := extractIP(r)
        if !rl.allow(ip) {
            w.Header().Set("Retry-After", "60")
            http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
            return
        }
        next(w, r)
    }
}

Nagłówek Retry-After informuje klienta, po jakim czasie może ponowić żądanie — to dobra praktyka, szczególnie przy budowie API konsumowanego przez inne systemy.

Strategie dla różnych endpointów

Nie wszystkie endpointy wymagają takiego samego poziomu ochrony. Warto stosować zróżnicowane limity:

  • Strony statyczne — wysoki limit (50-100 req/s), głównie ochrona przed botami
  • API odczytu — średni limit (10-30 req/s), zależy od kosztu operacji
  • API zapisu (POST, PUT, DELETE) — niski limit (1-5 req/s), chroni przed spamem
  • Logowanie i rejestracja — bardzo niski limit (3-5 prób na minutę), zapobiega atakom brute force
  • Formularze kontaktowe — ekstremalnie niski limit (1-2 na minutę), ochrona przed spamem

W naszych serwisach formularz kontaktowy ma limit 2 żądań na minutę per IP. To wystarczy dla realnego użytkownika, a skutecznie blokuje automatyczne boty spamowe.

Nagłówki informacyjne

Dobrze zaprojektowane API informuje klienta o stanie limitów za pomocą nagłówków odpowiedzi:

  • X-RateLimit-Limit — maksymalna liczba żądań w oknie
  • X-RateLimit-Remaining — ile żądań zostało
  • X-RateLimit-Reset — timestamp resetu licznika

Te nagłówki pozwalają klientom API inteligentnie zarządzać częstotliwością żądań, zamiast ślepo ponawiać próby po otrzymaniu błędu 429.

Podsumowanie

Rate limiting to fundament bezpieczeństwa każdego publicznego API. Najskuteczniejsze podejście łączy ochronę na poziomie reverse proxy (Nginx) z precyzyjną kontrolą w aplikacji. Algorytm token bucket najlepiej sprawdza się w większości przypadków — pozwala na naturalne serie żądań, jednocześnie chroniąc przed nadużyciami.

Jeśli potrzebujesz pomocy w zabezpieczeniu swojego API lub konfiguracji rate limitingu, skontaktuj się z nami — chętnie pomożemy dobrać odpowiednią strategię.