L'erreur est survenue à 14h32 un mardi classique de production. Notre monitoring affichait soudain une cascade d'erreurs 503 Service Unavailable, tandis que les logs Nginx crachaient des lignes interminables :

2026/01/14 14:32:17 [error] 15234#0: *89234 lua tcp socket connect timed out
2026/01/14 14:32:17 [error] 15234#0: *89235 connection refused while connecting to upstream
2026/01/14 14:32:18 [error] 15234#0: *89236 upstream prematurely closed connection
2026/01/14 14:32:18 [error] 15234#0: *89237 no live upstreams while connecting to upstream

Un client enterprise avait déployé un nouveau script de scraping massif sans respecter nos quotas. En 90 secondes, plus de 12 000 requêtes submergeaient notre gateway. Mesure corrective immédiate : implémenter un système de rate limiting robuste avec Nginx et Lua. Voici comment j'ai résolu ce problème de manière définitive.

Pourquoi le Rate Limiting est Critique pour les APIs IA

En tant qu'architecte infrastructure qui a géré des millions de requêtes API mensuelles, je peux vous assurer : sans contrôle de flux, votre système est une bombe à retardement. Les fournisseurs comme HolySheep AI proposent des APIs performantes (<50ms latence moyenne), mais sans gestion inteligente du traffic, même leurs serveurs robustes peuvent faillir.

Le rate limiting protège contre :

Architecture de Notre Solution Nginx Lua

Nous utilisons openresty (Nginx + LuaJIT 2.1) pour implémenter un rate limiter distribué avec Redis comme store partagé. Cette approche offre :

Installation et Configuration

Prérequis

# Installation OpenResty sur Ubuntu 22.04
sudo apt-get update
sudo apt-get install -y gnupg2 ca-certificates lsb-core

Ajouter le repo OpenResty

wget -qO - https://openresty.org/package/pubkey.gpg | sudo apt-key add - echo "deb http://openresty.org/package/ubuntu $(lsb_release -sc) main" | \ sudo tee /etc/apt/sources.list.d/openresty.list sudo apt-get update sudo apt-get install -y openresty redis-server lua-redis-parser

Vérification

openresty -v

nginx version: openresty/1.21.4.1

built with LuaJIT 2.1.0-beta3

Configuration Nginx avec Rate Limiting

# /etc/openresty/nginx.conf

Worker process doit tourner en root pour certaines ops

worker_processes auto; error_log /var/log/nginx/error.log warn; pid /var/run/openresty.pid; events { worker_connections 1024; use epoll; } http { include /etc/openresty/mime.types; default_type application/json; # Configuration Redis lua_package_path "/etc/openresty/lua/?.lua;;"; lua_code_cache on; # Initialisation Redis shared dict lua_shared_dict ratelimit 10m; lua_shared_dict ip_whitelist 1m; init_by_lua_block { local redis = require "resty.redis" local red = redis:new() red:set_timeout(1000) local ok, err = red:connect("127.0.0.1", 6379) if not ok then ngx.log(ngx.ERR, "Redis connection failed: ", err) end -- Préchargement des configs package.loaded.ratelimit_config = { default_limit = 100, -- req/min par défaut default_burst = 20, -- burst autorisé api_key_limit = 1000, -- limite pour clés premium ip_whitelist = {"10.0.0.0/8", "172.16.0.0/12"}, } } server { listen 8080; server_name _; location /v1/ { access_by_lua_file /etc/openresty/lua/ratelimit.lua; # Proxy vers HolySheep AI proxy_pass https://api.holysheep.ai/v1/; proxy_set_header Host api.holysheep.ai; proxy_set_header X-API-Key $http_x_api_key; proxy_set_header Content-Type application/json; proxy_connect_timeout 5s; proxy_send_timeout 30s; proxy_read_timeout 30s; # Retry automatique proxy_next_upstream error timeout invalid_header http_502; proxy_next_upstream_tries 2; } # Endpoint santé location /health { content_by_lua_block { ngx.say('{"status":"healthy","redis":"ok"}') } } } }

Script Lua de Rate Limiting

-- /etc/openresty/lua/ratelimit.lua

local redis = require "resty.redis"
local cjson = require "cjson"

local RATELIMIT_KEY_PREFIX = "rl:"
local WINDOW_SECONDS = 60

-- Extract API key from header
local function get_api_key()
    local key = ngx.var.http_x_api_key or ngx.var.http_authorization
    if key then
        -- Supporter "Bearer xxx" et "xxx" directement
        key = key:gsub("^Bearer%s+", "")
    end
    return key or "anonymous"
end

-- Vérifier si IP est whitelistée
local function is_ip_whitelisted(ip)
    local whitelist = package.loaded.ratelimit_config.ip_whitelist
    for _, cidr in ipairs(whitelist) do
        if is_ip_in_cidr(ip, cidr) then
            return true
        end
    end
    return false
end

local function is_ip_in_cidr(ip, cidr)
    local function ip_to_num(ip)
        local parts = {}
        for p in ip:gmatch("%d+") do
            table.insert(parts, tonumber(p))
        end
        return (parts[1]*16777216) + (parts[2]*65536) + (parts[3]*256) + parts[4]
    end

    local ip_num = ip_to_num(ip)
    local mask_bits = tonumber(cidr:match("/(%d+)$")) or 32
    local mask = (2^32 - 1) - (2^(32 - mask_bits) - 1)
    local base_ip = ip_to_num(cidr:match("^(%d+.%d+.%d+.%d+)"))
    
    return (ip_num ~ base_ip) == (ip_num & ~mask)
end

-- Algorithme Sliding Window Counter
local function sliding_window_check(key, limit, window)
    local red = redis:new()