{}const=>[]async()letfn</>var
Development

Lua on the edge: unexpected tricks in Nginx and Redis

How to turn Nginx and Redis into smart automation points using Lua: edge-feature flags, smart routing and A/B, token buckets and coalescing requests, micro-WAF and atomic operations in Redis. When to take the logic to the edge and when to leave it in the backend.

К

Kodik

Author

4 min read

Imagine: 02:17, night peakOne upstream is dying, the queue is growing, analytics asks to enable a new feature only for part of the traffic, and marketing asks not to lose the speed of the pages. The solution is not in the backend. The solution is on the edge: Nginx with Lua, plus a fast "state notepad" — Redis. In milliseconds, you turn on the feature flag, redirect requests, limit "chatty" clients, and return the cache where it is safe. ⚡

Lua in Nginx is a mini-brain right at the front door of your service: it sees headers, cookies, strange patterns, talks to Redis and makes small but critical decisions before the request reaches the application. Redis provides atomicity and speed: counters, quotas, flags, idempotency — without races and heavy transactions.

🧠 Why Lua on the edge

  • Speed and non-blocking I/O: OpenResty/ngx_lua work in an event model, without locks.

  • 🧩 Embeddability: scripts rule the behavior directly in the Nginx phases (rewrite/access/header/body/log).

  • 🔒 Atomicity in Redis: Lua scripts are executed as a single operation (no data races).

The key idea: "a little logic at the entrance" saves backend resources and gives subtle control.

🔥 100,000+ students already with us

Tired of reading theory?
Time to code!

Kodik — an app where you learn to code through practice. AI mentor, interactive lessons, real projects.

🤖 AI 24/7
🎓 Certificates
💰 Free
🚀 Start learning
Joined today

🌐 Unexpected applications in Nginx + Lua

  • 🎛️ Edge-feature flags: we include features by segments (country, client version) before entering the application.

  • 🧭 Smart routing: we direct traffic according to subscriber plans/AB options, read the rules from Redis.

  • 🛡️ Micro-WAF: "soft" blocking based on behavioral signatures, without interfering with legitimate users.

  • 🧯 Circuit breaker at the input: temporarily cut off the sick upstream, give a cache/stub.

  • 🧹 URL normalization: canonicalization, garbage query compression, and duplicate control for the cache.

🔗 How it connects to Redis

  • 🏷️ Flags/rules live in Redis with TTL → instant distribution without restarting Nginx.

  • 🧪 A/B: user option is stored in Redis → consistency between requests.

  • 🚦 Rate limiting: token buckets/key buckets IP+endpoint or user_id.

  • 🔁 Request coalescing: one request to the backend, the rest are waiting (we mute the "herd effect").

🧩 Example: JWT validation and dynamic limit

1) Validate the token directly in access_by_lua

access_by_lua_block {
  local jwt = require "resty.jwt"
  local validators = require "resty.jwt-validators"
  local auth = ngx.var.http_authorization or ""
  local token = auth:match("Bearer%s+(.+)")
  if not token then return ngx.exit(ngx.HTTP_UNAUTHORIZED) end

  local ok = jwt:verify("shared-secret", token, {
    validators.set_system_leeway(60),
    validators.require_aud("my-api"),
  })
  if not ok.valid then return ngx.exit(ngx.HTTP_FORBIDDEN) end
  ngx.ctx.user_id = ok.payload.sub
}

2) Token bucket in Redis via Lua (atomic)

-- KEYS[1]=ключ ведра; ARGV: now(ms), rate(ток/сек), burst
local key = KEYS[1]
local now  = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local burst= tonumber(ARGV[3])
local data = redis.call("HMGET", key, "tokens", "ts")
local tokens = tonumber(data[1]) or burst
local ts = tonumber(data[2]) or now
local delta = math.max(0, now - ts) / 1000 * rate
tokens = math.min(burst, tokens + delta)
local allowed = tokens >= 1
if allowed then tokens = tokens - 1 end
redis.call("HMSET", key, "tokens", tokens, "ts", now)
redis.call("PEXPIRE", key, math.floor((burst/rate)*1000))
return allowed and 1 or 0

3) Call from Nginx-Lua

local redis = require "resty.redis"
local r = redis:new()
r:set_timeout(50)
assert(r:connect("redis", 6379))
local key = "tb:" .. (ngx.ctx.user_id or ngx.var.remote_addr) .. ":" .. ngx.var.uri
local allowed = r:eval(token_bucket_script, 1, key, ngx.now()*1000, 5, 10)
r:set_keepalive(1_000, 100)  -- пул коннекшенов
if allowed == 0 then return ngx.exit(429) end

💡 The logic of the limit can be mixed: VIPs get a larger burst from Redis.

🧰 More tricks with Redis+Lua

  • 🔐 API Idempotency: key of type idem:<hash> with TTL — cut off repetitions.

  • 📦 Unique task assignment: atomically move elements from the set to the "in progress" list.

  • 🔒 Light curls: SET key val NX PX ttl; for distributed — be careful with Redlock (evaluate network/watch risks).

  • 🧮 Quotas/limits: counters per user/organization, scheduled reset.

📏 When the "edge" is better and when it is not

Scenario

Lua on Nginx/Redis

Better in the app

Routing, features, limits

✅ Instant solution at the entrance

Heavy calculations, ML, render

❌ Not a place for CPU-eaters

✅ In the backend/worker

Atomic data operations

✅ Redis Lua — perfect

Business rules and complex transformations

⚠️ You can, but be careful

✅ Readability in the code base

In Codice we make programming training fun and easy to understand: we have interesting courses with tasks that help you improve your skills step by step.

And we also have an active Telegram channel, where we discuss cool ideas, share experiences and analyze tasks together — learning becomes not only useful, but also fun.

🧵 Results

  • Lua on the "edge" provides instant solutions: flags, limits, routing, coalescing.

  • Redis scripts provide atomicity and high throughput.

  • The main thing is not to transfer heavy business logic to Nginx; use it as a smart filter.

What task would you put on the "edge" next? Write it down, we'll figure it out in the sequel. 💬

🎯Stop procrastinating

Liked the article?
Time to practice!

In Kodik, you don't just read — you write code immediately. Theory + practice = real skills.

Instant practice
🧠AI explains code
🏆Certificate

No registration • No card