Getting Started with OpenResty
What is OpenResty?
If you’ve used Nginx, you know it’s typically configured with static files and simple directives. OpenResty takes Nginx and bundles it with LuaJIT — a fast just-in-time compiler for Lua — plus a collection of useful Lua libraries. The result is a web platform that can execute Lua code during request processing, letting you build dynamic behavior without leaving Nginx. Getting started with OpenResty means learning how to write Lua code inside Nginx configuration blocks and use the ngx API to read requests, send responses, and connect to external services.
Think of OpenResty as giving Nginx a scripting brain. Instead of just serving files, it can make routing decisions, talk to databases, aggregate responses from multiple backends, and implement access controls — all while keeping Nginx’s reputation for handling thousands of concurrent connections.
The current stable version is OpenResty 1.29.2.1, which includes Nginx 1.29.2 and supports LuaJIT 2.0 or 2.1. Note that standard Lua (the PUC-Rio interpreter) isn’t supported — OpenResty specifically requires LuaJIT.
Prerequisites
Before you start, make sure you have a Linux or macOS machine (or WSL on Windows) with a terminal and basic familiarity with the command line. You will install OpenResty through your system’s package manager, so you need the ability to run commands with sudo. No prior experience with Nginx or Lua is required, but knowing the basics of HTTP — what a request method is, what a status code means — will help you follow the examples more easily.
Installing OpenResty
Getting OpenResty running depends on your operating system. Here’s how to set it up on common platforms. The official OpenResty packages include Nginx, LuaJIT, and all the core lua-resty-* libraries — you do not need to install Nginx or Lua separately beforehand. After installation, you will have both the openresty binary (which wraps Nginx) and the resty command-line tool for quick scripting.
Linux
Use the official pre-built packages. On Ubuntu or Debian:
# Add the OpenResty repository
wget -qO - https://openresty.org/package/pubkey.gpg | sudo apt-key add -
sudo apt-get install -y software-properties-common
sudo add-apt-repository -y "deb http://openresty.org/package/debian bookworm openresty"
sudo apt-get update
sudo apt-get install -y openresty
On CentOS or RHEL, the process is similar but uses yum instead of apt. CentOS and RHEL users should enable the EPEL repository first since some OpenResty dependencies live there. The yum-config-manager tool adds the OpenResty repository to your system’s package sources, and then yum install openresty pulls in Nginx, LuaJIT, and the resty libraries as a single bundle.
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
sudo yum install -y openresty
macOS
If you have Homebrew, installation is straightforward. The openresty/brew tap is maintained by the OpenResty project and stays current with the latest stable releases. After tapping, the brew install command handles all the dependencies automatically, including OpenSSL and PCRE — the same libraries that a production deployment relies on.
brew tap openresty/brew
brew install openresty
Windows
OpenResty doesn’t run natively on Windows. Use WSL (Windows Subsystem for Linux) or Docker. Install WSL, then follow the Linux instructions above inside your WSL distribution.
Docker
For quick experimentation, Docker works well. The official openresty/openresty image on Docker Hub bundles everything you need into a single container. The command below starts OpenResty in the background and maps port 8080 on your host to port 80 inside the container, so you can hit http://localhost:8080 immediately.
docker pull openresty/openresty
docker run -d -p 8080:80 openresty/openresty
Verifying your installation
After installation, check that everything is working by querying the Nginx version string and the LuaJIT version. The Nginx version output should mention “openresty” — if it says “nginx” alone, you may have the standard Nginx binary in your PATH instead of the OpenResty one. On some systems, the binary is named openresty rather than nginx, so try both if the first one doesn’t show the OpenResty branding.
# Check Nginx version (should show OpenResty)
nginx -v
# Check LuaJIT version
luajit -v
You should see output indicating OpenResty and LuaJIT 2.0 or 2.1.
How Lua integrates with Nginx
When a request arrives, Nginx passes it through several processing stages. At each stage, specific directives run in order. OpenResty adds *_by_lua_block directives that let Lua code execute during those phases.
The main directives you’ll encounter:
| Directive | Phase | Common Use |
|---|---|---|
init_by_lua_block | http init | Load shared data at startup |
init_worker_by_lua_block | worker init | Set up per-worker timers |
rewrite_by_lua_block | rewrite | Redirects, modify URI |
access_by_lua_block | access | Authentication, rate limiting |
content_by_lua_block | content | Generate the response |
header_filter_by_lua_block | header filter | Modify response headers |
log_by_lua_block | log | Custom logging |
Use *_by_lua_block for inline code and *_by_lua_file for external Lua files. The plain *_by_lua variants without _block or _file are deprecated; don’t use them.
Hot reloading
During development, reload your configuration without restarting Nginx:
nginx -s reload
This sends Nginx a reload signal, and worker processes will pick up the new configuration gracefully.
The ngx API
When your Lua code runs inside OpenResty, it communicates with Nginx through the ngx table. This is your primary interface for reading request data, sending responses, and controlling the request lifecycle. Every Lua script you write inside a *_by_lua_block directive runs in a sandboxed environment where the ngx global is always available — you never need to require it.
Sending Output
Two functions handle output. ngx.say appends a newline character after the output, which makes it convenient for building line-by-line responses like HTML pages or plain-text API results. ngx.print writes exactly what you give it with no trailing newline, useful when you need precise control over the output format or are streaming partial responses. Both functions accept multiple arguments and concatenate them, the same way Lua’s built-in print does.
-- ngx.say adds a newline after output (like print with \n)
ngx.say("Hello, ", "World") -- prints: Hello, World\n
-- ngx.print outputs without adding newline
ngx.print("No newline here")
Reading request data
Access Nginx variables through ngx.var. The entire set of Nginx variables — everything you can reference as $remote_addr or $uri in an Nginx config file — is available through this table. Variable names follow the same convention but with underscores instead of dollar signs: $http_user_agent becomes ngx.var.http_user_agent. This lets you inspect every part of the incoming request from your Lua code.
-- Get client IP address
local client_ip = ngx.var.remote_addr
-- Get the request URI
local uri = ngx.var.uri
-- Get HTTP method (GET, POST, PUT, etc.)
local method = ngx.var.request_method
-- Get a specific request header
local user_agent = ngx.var.http_user_agent
For more complex request handling, use the ngx.req object. While ngx.var gives you access to parsed Nginx variables, ngx.req provides lower-level control: reading the raw request body, getting all headers as a Lua table, and accessing POST arguments. The body is not read automatically by OpenResty — you must explicitly call ngx.req.read_body() before any body-access functions will return data. This is different from frameworks like Express or Flask where the body is parsed for you.
-- Must read body before accessing it
ngx.req.read_body()
-- Get request body as string
local body = ngx.req.get_body_data()
-- Get all headers as a table
local headers = ngx.req.get_headers()
Handling different request body types
OpenResty is commonly used for API gateways, so you’ll receive various body formats. Here’s how to handle them. The strategy is to inspect the Content-Type header and branch on it: JSON bodies are decoded with cjson, form-encoded bodies are accessed through ngx.req.get_post_args(), and everything else is treated as plain text. The cjson library is built into OpenResty and handles JSON encoding and decoding — it’s the standard choice for API work since it’s fast, correct, and always available without installing anything extra.
ngx.req.read_body()
local headers = ngx.req.get_headers()
local content_type = headers["Content-Type"] or ""
local body = ngx.req.get_body_data()
if string.match(content_type, "application/json") then
-- Parse JSON request body
local cjson = require "cjson"
local decoded = cjson.decode(body)
ngx.say("Received JSON: ", decoded.name)
elseif string.match(content_type, "application/x-www-form-urlencoded") then
-- Parse form data
local args = ngx.req.get_post_args()
ngx.say("Form field: ", args.field_name)
else
-- Plain text or unknown
ngx.say("Body: ", body)
end
The cjson library is built into OpenResty and handles JSON encoding and decoding.
Controlling Response
Set status codes and redirect. ngx.status sets the HTTP status code for the current response, and you can use the ngx.HTTP_* constants (like ngx.HTTP_OK for 200, ngx.HTTP_FORBIDDEN for 403) instead of remembering numeric codes. ngx.redirect sends a 302 redirect by default, though you can pass a second argument to specify a different status code. ngx.exit terminates request processing immediately — the status code you pass becomes the final response. Any code after ngx.exit will not execute.
-- Set response status code
ngx.status = ngx.HTTP_OK
-- Redirect to a different URL (302 by default)
ngx.redirect("/new-location")
-- Or use ngx.exit to terminate the request
ngx.exit(ngx.HTTP_FORBIDDEN) -- Returns 403 Forbidden
Subrequests
OpenResty can make internal requests to other locations. This is powerful for aggregation. A subrequest is an HTTP call handled entirely within Nginx’s event loop — it never leaves the process, never hits the network stack, and is non-blocking from Lua’s perspective. The server processes the subrequest as if it were a normal incoming request, routing it through the configured locations, and returns the full response (headers, body, status) to your Lua code.
-- Single subrequest
local res = ngx.location.capture("/api/users")
-- Check response status
if res.status == 200 then
ngx.say(res.body)
end
-- Multiple subrequests in parallel
local res1, res2, res3 = unpack(ngx.location.capture_multi({
{ "/api/users" },
{ "/api/posts" },
{ "/api/comments" }
}))
Logging
Write to Nginx’s error log using the ngx.log function, which takes a log level constant and a message string. Log levels let you control verbosity in production: set the error_log directive to warn in your Nginx config and only WARN and higher messages will appear. This means you can leave DEBUG and INFO calls in your code without worrying about disk usage, since they are filtered at the Nginx level rather than in your Lua code.
ngx.log(ngx.INFO, "Request processing started")
ngx.log(ngx.ERR, "Failed to connect to database: ", err)
ngx.log(ngx.DEBUG, "Debug information here")
Log levels, from most to least severe: ngx.EMERG, ngx.ALERT, ngx.CRIT, ngx.ERR, ngx.WARN, ngx.INFO, ngx.DEBUG.
Your first OpenResty application
Let’s build a simple HTTP server that returns “Hello, World!” with some dynamic information. This example demonstrates the core OpenResty pattern: an Nginx server block with a location that runs Lua code via content_by_lua_block. The Lua code reads request properties from ngx.var and writes HTML to the response body using ngx.say. Every line inside the Lua block executes on each request — this is where your application logic lives.
Create a file called nginx.conf:
worker_processes 1;
error_log logs/error.log;
events {
worker_connections 1024;
}
http {
server {
listen 8080;
location / {
default_type text/html;
# This Lua code runs for every request to /
content_by_lua_block {
local method = ngx.var.request_method
local uri = ngx.var.uri
local ip = ngx.var.remote_addr
ngx.say("<html><body>")
ngx.say("<h1>Hello, World!</h1>")
ngx.say("<p>Method: ", method, "</p>")
ngx.say("<p>URI: ", uri, "</p>")
ngx.say("<p>Your IP: ", ip, "</p>")
ngx.say("</body></html>")
}
}
}
}
Start Nginx with this configuration. The -p flag sets the prefix path — OpenResty looks for the logs/ directory relative to this path — and -c specifies the config file. Using $(pwd) keeps everything relative to your working directory, avoiding collisions with any system-wide Nginx installation. If port 8080 is already in use, change the listen directive in your config to another port like 8081:
nginx -p $(pwd)/ -c nginx.conf
Test it with curl:
curl http://localhost:8080/
You should see HTML output showing the request method, URI, and your client IP. This demonstrates the core pattern: Nginx handles the networking, Lua handles the dynamic content.
Working with external libraries
OpenResty includes many lua-resty-* libraries for connecting to external services. These live in the resty namespace and are designed specifically for the cosocket API — OpenResty’s non-blocking I/O layer. Unlike standard Lua socket libraries that would block the entire Nginx worker process, cosocket operations yield to the Nginx event loop, letting other requests be processed while waiting for network I/O. This is what lets a single OpenResty worker handle thousands of concurrent connections.
Here’s how to connect to Redis using resty.redis:
local redis = require "resty.redis"
-- Create a new Redis connection
local red = redis:new()
-- Set a timeout (in milliseconds)
red:set_timeout(1000)
-- Connect to Redis
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "failed to connect: ", err)
return ngx.exit(500)
end
-- Make a request
local value, err = red:get("my_key")
if err then
ngx.log(ngx.ERR, "failed to get key: ", err)
return ngx.exit(500)
end
-- Use the value
ngx.say("Redis returned: ", value)
-- Return connection to pool for reuse
red:set_keepalive(0, 100)
The connection pooling is important: creating a new TCP connection for every request is slow. The set_keepalive call returns the connection to a pool so the next request can reuse it.
Other useful resty modules include resty.mysql for MySQL, resty.http for HTTP clients, resty.dns for DNS queries, and resty.websocket for WebSocket connections.
Installing additional libraries
Use OpenResty’s package manager opm to install additional lua-resty libraries. You can also use LuaRocks, which gives access to the broader Lua ecosystem beyond the resty namespace. Libraries installed via opm go into OpenResty’s own package path and require no additional configuration. LuaRocks-installed packages may need the lua_package_path directive set in your Nginx config so OpenResty knows where to find them.
opm get openresty/lua-resty-limit-traffic
The opm tool is OpenResty’s own package manager and installs libraries directly into OpenResty’s search path. You can also use LuaRocks if you prefer the broader Lua ecosystem. When using LuaRocks with OpenResty, make sure to invoke the LuaRocks that ships with your OpenResty installation rather than a system-wide LuaRocks, to avoid version mismatches:
luarocks install lua-resty-http
HTTPS and SSL/TLS
For production deployments, you’ll need to configure SSL. Here’s a basic HTTPS setup. The ssl_certificate and ssl_certificate_key directives point to your certificate and private key files — on a production server, these would come from Let’s Encrypt or a commercial CA. The ssl_protocols directive restricts the server to TLS 1.2 and 1.3, which are the only versions considered secure as of 2024. The ssl_ciphers setting further tightens the cipher suite to avoid weak algorithms.
http {
server {
listen 443 ssl;
ssl_certificate /path/to/certificate.crt;
ssl_certificate_key /path/to/private.key;
# Recommended SSL settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
content_by_lua_block {
ngx.say("Secure connection established")
}
}
}
}
For local development, generate a self-signed certificate. The openssl req command below creates a 4096-bit RSA key and a certificate valid for 365 days. The -nodes flag skips passphrase encryption on the key — convenient for development but do not use it in production key generation. Browsers will show a security warning when connecting to a server using a self-signed certificate, which is expected and fine for local testing.
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
Then update your config to use cert.pem and key.pem.
Practical examples
Blocking IPs in the access phase
Implement IP-based access control using the access phase. The access_by_lua_block directive runs before the content phase, which means blocked clients never reach your application code at all — they get a 403 response as early as possible in the request lifecycle. This is more efficient than checking IPs inside content_by_lua_block because Nginx can close the connection without ever generating a full response body for blocked requests.
server {
listen 8080;
location / {
access_by_lua_block {
-- List of blocked IP addresses
local blocked = {
["192.168.1.100"] = true,
["10.0.0.50"] = true
}
local client_ip = ngx.var.remote_addr
if blocked[client_ip] then
ngx.exit(ngx.HTTP_FORBIDDEN)
end
}
-- Your normal location handler
content_by_lua_block {
ngx.say("Welcome!")
}
}
}
The access_by_lua_block runs before the content phase, so blocked IPs never reach your application code.
Simple in-memory cache
Use shared memory to cache responses. The lua_shared_dict directive allocates a named memory zone that all Nginx worker processes can read and write. Each entry you store has a TTL (time-to-live) in seconds, after which it expires automatically — no manual cleanup needed. This is ideal for caching database query results, external API responses, or any computation that is expensive to repeat on every request. The memory zone size should be tuned to your expected cache volume: each entry takes a few hundred bytes of overhead plus the data itself.
http {
-- Declare a shared memory zone (10 megabytes)
lua_shared_dict my_cache 10m;
server {
listen 8080;
location /api/data {
content_by_lua_block {
local cache = ngx.shared.my_cache
local key = ngx.var.arg_key or "default"
-- Check cache first
local cached = cache:get(key)
if cached then
ngx.say("cached: ", cached)
return
end
-- Simulate fetching fresh data
local fresh_data = os.date("%H:%M:%S")
-- Store in cache with 60 second TTL
cache:set(key, fresh_data, 60)
ngx.say("fresh: ", fresh_data)
}
}
}
}
Shared memory is shared across all worker processes, but each worker has its own Lua VM. Data written from one worker is visible to others because Nginx manages the shared memory directly.
Dynamic Routing
Route requests to different backends based on the URI. This pattern uses the content phase to inspect the request path, select an upstream group, and then delegate the actual proxying to a named location via ngx.exec. The @proxy internal location never responds directly — it always forwards to the upstream selected in the variable $backend. This separation keeps routing logic (in Lua) cleanly isolated from proxying mechanics (in Nginx configuration).
http {
upstream backend_v1 {
server 127.0.0.1:8001;
}
upstream backend_v2 {
server 127.0.0.1:8002;
}
server {
listen 8080;
location / {
content_by_lua_block {
local path = ngx.var.uri
-- Route based on API version in path
if string.match(path, "^/api/v1/") then
ngx.var.backend = "backend_v1"
elseif string.match(path, "^/api/v2/") then
ngx.var.backend = "backend_v2"
else
ngx.exit(ngx.HTTP_NOT_FOUND)
end
-- Internal redirect to proxy location
ngx.exec("@proxy")
}
}
location @proxy {
proxy_pass http://$backend;
}
}
}
This pattern lets you implement sophisticated routing logic in Lua before passing requests to upstream servers.
JSON API Response
Here’s a complete example of building a JSON API response. The pattern is common in microservice architectures: receive a request, optionally read the body, construct a Lua table with the response data, encode it with cjson, set the correct Content-Type header, and send it. The os.time() call provides a Unix timestamp that clients can use to detect stale cached responses.
content_by_lua_block {
local cjson = require "cjson"
-- Read request body if present
ngx.req.read_body()
local args = ngx.req.get_get_args()
local headers = ngx.req.get_headers()
-- Build response table
local response = {
status = "ok",
data = {
message = "Hello from OpenResty",
timestamp = os.time(),
method = ngx.var.request_method,
uri = ngx.var.uri,
client_ip = ngx.var.remote_addr
}
}
-- Set JSON content type
ngx.header["Content-Type"] = "application/json"
-- Encode and send response
ngx.say(cjson.encode(response))
}
Common pitfalls
Several issues trip up newcomers to OpenResty:
Cosockets aren’t available everywhere. Cosockets (the network socket API used by resty libraries) work in most phases but not in set_by_lua_block or timer callbacks. If you try to connect to Redis in the wrong phase, you’ll get an error about cosockets not being available.
Lua variables and Nginx variables are different. Setting a Lua variable doesn’t create an Nginx variable:
-- WRONG: This doesn't do what you expect
set_by_lua_block $foo {
local x = "bar"
-- x is a Lua variable, not accessible as $foo
}
-- CORRECT: Return the value
set_by_lua_block $foo {
return "bar" -- The return value sets $foo
}
Request body must be explicitly read. Unlike some frameworks, OpenResty doesn’t automatically read the request body. Calling ngx.req.get_body_data() without first calling ngx.req.read_body() returns nil, even if the client sent data. This catches many newcomers who are used to frameworks that handle body parsing automatically. Always call ngx.req.read_body() before interacting with the body.
-- REQUIRED before accessing body data
ngx.req.read_body()
local body = ngx.req.get_body_data()
Avoid blocking operations. Never use os.execute, io.popen, or standard Lua file I/O in request handlers; they block the entire worker process. Use cosockets or ngx.location.capture instead.
Each worker is isolated. Every Nginx worker runs its own Lua VM. Module-level variables persist across requests within one worker but aren’t shared between workers. Use ngx.shared.DICT for cross-worker data sharing.
Summary
OpenResty extends Nginx with Lua scripting capabilities, letting you build dynamic request handling directly inside Nginx. The key concepts to remember:
- Nginx phases determine when your Lua code runs. Use the right directive for your use case
- The ngx API is your interface to request data, responses, and Nginx internals
- resty libraries connect to external services like Redis, MySQL, and HTTP backends
- Shared memory caches data across all workers
- Subrequests aggregate multiple backend calls into one response
- cjson handles JSON encoding and decoding for API work
- Use SSL/TLS for production deployments
This tutorial covered the fundamentals. The next in this series explores advanced topics like rate limiting, complex routing patterns, and optimizing OpenResty for production workloads.
Next steps
You have installed OpenResty, understood the Nginx phase model, and built several working examples — from a simple “Hello, World” handler to Redis-backed caching and API version routing. The next tutorial in this series covers routing and middleware, where you will learn to build a full-featured API gateway with rate limiting, authentication, and request transformation using OpenResty’s cosocket-based libraries.
See also
- OpenResty routing and middleware — the next tutorial in this series, building a full-featured API gateway
- OpenResty rate limiting — implement rate limiting and traffic shaping with lua-resty-limit-traffic
- OpenResty REST API — build a complete REST API with OpenResty and PostgreSQL