OpenResty cheatsheet.
Install
# macOS
brew install openresty/brew/openresty
# Linux
apt install openresty
OpenResty bundles nginx + LuaJIT + many useful modules.
Hello world
location = /hello {
content_by_lua_block {
ngx.say("Hello ", ngx.var.remote_addr)
}
}
Phases (where Lua runs)
init_by_lua_block { -- once, at start }
init_worker_by_lua_block { -- per worker startup }
set_by_lua_block $var { return "..." } -- compute variable
rewrite_by_lua_block { -- before location match }
access_by_lua_block { -- access control }
content_by_lua_block { -- generate response }
header_filter_by_lua_block { -- modify response headers }
body_filter_by_lua_block { -- modify body }
log_by_lua_block { -- after response }
Access control
location / {
access_by_lua_block {
local token = ngx.var.http_authorization
if not token then return ngx.exit(401) end
-- validate
if token ~= "Bearer secret" then
ngx.status = 403
ngx.say("forbidden")
return ngx.exit(ngx.HTTP_FORBIDDEN)
end
}
proxy_pass http://app;
}
Reading request body
location /webhook {
access_by_lua_block {
ngx.req.read_body()
local body = ngx.req.get_body_data()
ngx.log(ngx.NOTICE, "got: ", body)
}
proxy_pass http://app;
}
Modifying response
location / {
proxy_pass http://app;
header_filter_by_lua_block {
ngx.header["X-Custom"] = "value"
}
body_filter_by_lua_block {
local chunk = ngx.arg[1]
if chunk then
-- modify chunk
ngx.arg[1] = string.gsub(chunk, "old", "new")
end
}
}
Redis
luarocks install lua-resty-redis
local redis = require("resty.redis")
local r = redis:new()
r:set_timeout(1000)
local ok, err = r:connect("127.0.0.1", 6379)
if not ok then ngx.exit(500) end
local count = r:incr("counter")
r:close() -- or r:set_keepalive(10000, 100)
ngx.say(count)
Rate limit (Redis-backed)
local limit = require("resty.limit.req").new("my_limit", 100, 50)
local key = ngx.var.binary_remote_addr
local delay, err = limit:incoming(key, true)
if not delay then
if err == "rejected" then return ngx.exit(429) end
return ngx.exit(500)
end
if delay > 0 then ngx.sleep(delay) end
JSON
local cjson = require("cjson")
ngx.req.read_body()
local data = cjson.decode(ngx.req.get_body_data())
ngx.say(cjson.encode({ ok = true, name = data.name }))
Subrequests
local res = ngx.location.capture("/_auth", { method = ngx.HTTP_GET })
if res.status == 200 then
ngx.var.auth_user = res.body
end
location = /_auth {
internal;
proxy_pass http://auth-service;
}
Async parallel
local res1, res2 = ngx.location.capture_multi({
{ "/a" },
{ "/b" },
})
Coroutines / sleep
ngx.sleep(0.5)
Cooperative; doesn’t block worker.
Timers (deferred work)
local function update()
-- runs detached
end
ngx.timer.at(0, update) -- now
ngx.timer.at(60, update) -- after 60s
ngx.timer.every(30, update) -- every 30s
Shared dict (in-process cache)
http {
lua_shared_dict cache 10m;
server {
location / {
content_by_lua_block {
local cache = ngx.shared.cache
local val = cache:get("key")
if not val then
val = expensive()
cache:set("key", val, 60)
end
ngx.say(val)
}
}
}
}
OPM / luarocks packages
opm install ledgetech/lua-resty-http
luarocks install lua-resty-jwt
JWT verification
local jwt = require("resty.jwt")
local validators = require("resty.jwt-validators")
local secret = "..."
local token = ngx.var.http_authorization:match("Bearer%s+(.+)")
local jwt_obj = jwt:verify(secret, token, {
exp = validators.is_not_expired(),
})
if not jwt_obj.verified then return ngx.exit(401) end
ngx.var.user_id = jwt_obj.payload.sub
Common mistakes
- Heavy blocking I/O (no cosocket) → worker stuck.
- Forgetting
set_keepalivefor Redis/MySQL → connection storm. - Mutable global state across requests — use shared.dict / FFI.
- LuaJIT incompatibilities with some pure Lua code.
lua_shared_dictsize too small → eviction churn.
Read this next
If you want my OpenResty starter (rate-limit + JWT + Redis), it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .