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_keepalive for Redis/MySQL → connection storm.
  • Mutable global state across requests — use shared.dict / FFI.
  • LuaJIT incompatibilities with some pure Lua code.
  • lua_shared_dict size 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 .