Configure high-performance caching with Redis integration, implement microcaching strategies, and optimize Lua scripts for memory management in production OpenResty deployments.
Prerequisites
- Root or sudo access
- Basic nginx/OpenResty knowledge
- Redis server
- Understanding of Lua scripting
What this solves
OpenResty combines nginx with LuaJIT to create a powerful web platform, but default configurations leave significant performance on the table. This tutorial shows you how to implement Redis-backed caching, configure advanced HTTP microcaching, and optimize Lua scripts for memory efficiency in high-traffic production environments.
Step-by-step configuration
Install OpenResty with performance modules
Install OpenResty with Redis and caching modules enabled for maximum performance capabilities.
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 update
sudo apt install -y openresty openresty-opm redis-server luarocks
sudo dnf install -y dnf-plugins-core
sudo dnf config-manager --add-repo https://openresty.org/package/centos/openresty.repo
sudo dnf install -y openresty openresty-opm redis luarocks
Install Lua Redis and caching libraries
Install essential Lua libraries for Redis connectivity and advanced caching functionality.
sudo /usr/local/openresty/bin/opm get ledgetech/lua-resty-redis-connector
sudo /usr/local/openresty/bin/opm get openresty/lua-resty-redis
sudo /usr/local/openresty/bin/opm get openresty/lua-resty-lrucache
sudo /usr/local/openresty/bin/opm get bungle/lua-resty-template
sudo luarocks install lua-resty-http
Configure Redis for OpenResty caching
Optimize Redis configuration for high-performance caching with OpenResty integration.
maxmemory 1gb
maxmemory-policy allkeys-lru
save ""
tcp-keepalive 300
timeout 0
tcp-backlog 511
maxclients 10000
notify-keyspace-events Ex
sudo systemctl restart redis-server
sudo systemctl enable redis-server
Create optimized OpenResty main configuration
Configure OpenResty with performance-tuned worker processes, connection pooling, and shared memory zones.
worker_processes auto;
worker_rlimit_nofile 65535;
worker_cpu_affinity auto;
error_log /var/log/openresty/error.log warn;
pid /var/run/openresty.pid;
events {
worker_connections 8192;
use epoll;
multi_accept on;
}
http {
include mime.types;
default_type application/octet-stream;
# Lua shared memory zones
lua_shared_dict cache_dict 100m;
lua_shared_dict locks 10m;
lua_shared_dict stats 10m;
# Redis connection pool
lua_socket_pool_size 100;
lua_socket_keepalive_timeout 60s;
# Performance optimizations
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 30;
keepalive_requests 1000;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript application/x-font-ttf font/opentype image/svg+xml;
# Rate limiting zones
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
include /usr/local/openresty/nginx/conf/conf.d/*.conf;
}
Create Lua Redis connection module
Build a reusable Lua module for efficient Redis connections with connection pooling and error handling.
sudo mkdir -p /usr/local/openresty/nginx/lua
local redis = require "resty.redis"
local cjson = require "cjson"
local _M = {}
-- Redis connection configuration
local redis_config = {
host = "127.0.0.1",
port = 6379,
timeout = 5000,
pool_size = 100,
keepalive = 60000
}
function _M.connect()
local red = redis:new()
red:set_timeout(redis_config.timeout)
local ok, err = red:connect(redis_config.host, redis_config.port)
if not ok then
ngx.log(ngx.ERR, "Failed to connect to Redis: ", err)
return nil, err
end
return red
end
function _M.set_keepalive(red)
if not red then
return
end
local ok, err = red:set_keepalive(redis_config.keepalive, redis_config.pool_size)
if not ok then
ngx.log(ngx.ERR, "Failed to set Redis keepalive: ", err)
end
end
function _M.get_cached(key)
local red, err = _M.connect()
if not red then
return nil, err
end
local res, err = red:get(key)
_M.set_keepalive(red)
if not res or res == ngx.null then
return nil, "cache miss"
end
local data = cjson.decode(res)
return data, nil
end
function _M.set_cached(key, value, ttl)
local red, err = _M.connect()
if not red then
return false, err
end
ttl = ttl or 3600
local json_value = cjson.encode(value)
local ok, err = red:setex(key, ttl, json_value)
_M.set_keepalive(red)
if not ok then
ngx.log(ngx.ERR, "Failed to cache data: ", err)
return false, err
end
return true, nil
end
function _M.invalidate(pattern)
local red, err = _M.connect()
if not red then
return false, err
end
local keys, err = red:keys(pattern)
if keys and #keys > 0 then
red:del(unpack(keys))
end
_M.set_keepalive(red)
return true, nil
end
return _M
Implement advanced microcaching strategy
Create a multi-layer caching system with shared memory, Redis, and intelligent cache warming.
local lrucache = require "resty.lrucache"
local redis_connector = require "redis_connector"
local cjson = require "cjson"
local _M = {}
-- L1 cache (in-memory LRU)
local l1_cache = lrucache.new(1000)
-- Cache configuration
local cache_config = {
l1_ttl = 60, -- 1 minute L1 cache
l2_ttl = 3600, -- 1 hour Redis cache
l3_ttl = 86400, -- 24 hour fallback cache
warm_threshold = 300 -- Warm cache when TTL < 5 minutes
}
function _M.generate_key(uri, args)
local key_parts = {"page", uri}
if args then
for k, v in pairs(args) do
if k ~= "_" then -- Skip cache buster
table.insert(key_parts, k .. "=" .. v)
end
end
end
return table.concat(key_parts, ":")
end
function _M.get_content(key)
-- Try L1 cache first
local l1_data = l1_cache:get(key)
if l1_data then
ngx.header["X-Cache-Status"] = "HIT-L1"
return l1_data.content, l1_data.headers
end
-- Try Redis cache
local l2_data, err = redis_connector.get_cached(key)
if l2_data then
-- Store in L1 cache
l1_cache:set(key, l2_data, cache_config.l1_ttl)
ngx.header["X-Cache-Status"] = "HIT-L2"
-- Check if we need to warm the cache
if l2_data.expires and (l2_data.expires - ngx.time()) < cache_config.warm_threshold then
_M.warm_cache_async(key)
end
return l2_data.content, l2_data.headers
end
ngx.header["X-Cache-Status"] = "MISS"
return nil, nil
end
function _M.store_content(key, content, headers, ttl)
ttl = ttl or cache_config.l2_ttl
local cache_data = {
content = content,
headers = headers or {},
created = ngx.time(),
expires = ngx.time() + ttl
}
-- Store in L1 cache
l1_cache:set(key, cache_data, cache_config.l1_ttl)
-- Store in Redis
redis_connector.set_cached(key, cache_data, ttl)
end
function _M.warm_cache_async(key)
-- Background cache warming using ngx.timer
local ok, err = ngx.timer.at(0, function()
ngx.log(ngx.INFO, "Warming cache for key: ", key)
-- Here you would regenerate the content
-- This is a placeholder for your content generation logic
end)
if not ok then
ngx.log(ngx.ERR, "Failed to create cache warming timer: ", err)
end
end
function _M.invalidate_pattern(pattern)
-- Clear L1 cache (simple approach - clear all)
l1_cache:flush_all()
-- Clear Redis pattern
return redis_connector.invalidate(pattern)
end
function _M.get_stats()
local stats_key = "cache_stats"
local stats = ngx.shared.stats:get(stats_key) or {
hits = 0,
misses = 0,
stores = 0
}
return stats
end
function _M.increment_stat(stat_name)
local stats_key = "cache_stats"
local stats = _M.get_stats()
stats[stat_name] = (stats[stat_name] or 0) + 1
ngx.shared.stats:set(stats_key, stats)
end
return _M
Configure virtual host with advanced caching
Set up a virtual host that implements the caching strategy with content-specific TTL and intelligent invalidation.
server {
listen 80;
server_name example.com;
access_log /var/log/openresty/access.log;
error_log /var/log/openresty/error.log;
# Rate limiting
limit_req zone=api burst=20 nodelay;
# Security headers
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
location / {
# Lua caching logic
access_by_lua_block {
local cache_manager = require "cache_manager"
local key = cache_manager.generate_key(ngx.var.uri, ngx.req.get_uri_args())
local content, headers = cache_manager.get_content(key)
if content then
-- Set cached headers
if headers then
for k, v in pairs(headers) do
ngx.header[k] = v
end
end
cache_manager.increment_stat("hits")
ngx.say(content)
ngx.exit(200)
else
cache_manager.increment_stat("misses")
ngx.ctx.cache_key = key
end
}
# Your application backend
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Cache response from backend
header_filter_by_lua_block {
if ngx.status == 200 then
ngx.ctx.should_cache = true
ngx.ctx.response_headers = ngx.resp.get_headers()
end
}
body_filter_by_lua_block {
if ngx.ctx.should_cache and not ngx.ctx.cached then
local cache_manager = require "cache_manager"
local body = ngx.arg[1]
if ngx.arg[2] then -- Last chunk
ngx.ctx.cached = true
-- Determine TTL based on content type
local ttl = 3600 -- default 1 hour
local content_type = ngx.header.content_type or ""
if string.match(content_type, "application/json") then
ttl = 300 -- 5 minutes for API responses
elseif string.match(content_type, "text/html") then
ttl = 1800 -- 30 minutes for HTML
elseif string.match(content_type, "image/") then
ttl = 86400 -- 24 hours for images
end
cache_manager.store_content(ngx.ctx.cache_key, body, ngx.ctx.response_headers, ttl)
cache_manager.increment_stat("stores")
end
end
}
}
# Cache invalidation endpoint
location ~ ^/admin/cache/invalidate/(.+)$ {
access_by_lua_block {
-- Simple IP-based access control
local allowed_ips = {"127.0.0.1", "203.0.113.10"}
local client_ip = ngx.var.remote_addr
local allowed = false
for _, ip in ipairs(allowed_ips) do
if ip == client_ip then
allowed = true
break
end
end
if not allowed then
ngx.status = 403
ngx.say("Forbidden")
ngx.exit(403)
end
local cache_manager = require "cache_manager"
local pattern = ngx.var[1]
local ok, err = cache_manager.invalidate_pattern(pattern)
if ok then
ngx.say('Cache invalidated for pattern: ' .. pattern)
else
ngx.status = 500
ngx.say('Error invalidating cache: ' .. (err or 'unknown error'))
end
ngx.exit(ngx.status)
}
}
# Cache statistics endpoint
location /admin/cache/stats {
access_by_lua_block {
local cache_manager = require "cache_manager"
local stats = cache_manager.get_stats()
ngx.header.content_type = "application/json"
local cjson = require "cjson"
ngx.say(cjson.encode(stats))
ngx.exit(200)
}
}
}
Optimize Lua memory management
Configure LuaJIT for optimal memory usage and garbage collection tuning for high-performance applications.
local _M = {}
-- Memory optimization settings
local gc_config = {
gc_step_size = 200,
gc_threshold = 1024 * 1024, -- 1MB
max_string_cache = 10000
}
function _M.init_worker()
-- Set LuaJIT memory limits
if jit then
jit.opt.start("maxtrace=10000", "maxrecord=20000", "minstitch=3")
end
-- Initialize periodic GC
local ok, err = ngx.timer.every(30, _M.periodic_gc)
if not ok then
ngx.log(ngx.ERR, "Failed to create GC timer: ", err)
end
end
function _M.periodic_gc()
local before = collectgarbage("count")
collectgarbage("step", gc_config.gc_step_size)
local after = collectgarbage("count")
if before > gc_config.gc_threshold then
collectgarbage("collect")
ngx.log(ngx.INFO, "Full GC executed, freed: ", (before - after), "KB")
end
end
function _M.optimize_table(t)
-- Pre-allocate table if we know the size
if type(t) == "table" then
local count = 0
for _ in pairs(t) do
count = count + 1
end
if count > 100 then
-- For large tables, consider using weak references
local mt = getmetatable(t) or {}
mt.__mode = "v" -- Weak values
setmetatable(t, mt)
end
end
return t
end
function _M.string_intern(str)
-- Simple string interning to reduce memory usage
local cache = ngx.shared.cache_dict
local cached_str = cache:get("str_" .. str)
if cached_str then
return cached_str
end
-- Only cache small strings to avoid memory bloat
if #str < 1000 then
cache:set("str_" .. str, str, 3600)
end
return str
end
function _M.pool_objects()
-- Object pooling for frequently created/destroyed objects
local pool = {}
return {
get = function()
local obj = table.remove(pool)
if not obj then
obj = {}
end
return obj
end,
put = function(obj)
-- Clear object data
for k in pairs(obj) do
obj[k] = nil
end
-- Return to pool if not too large
if #pool < 100 then
table.insert(pool, obj)
end
end
}
end
return _M
Create initialization script
Set up the init_worker_by_lua block to initialize memory optimization and connection pools.
-- Initialize memory optimizer
local memory_optimizer = require "memory_optimizer"
memory_optimizer.init_worker()
-- Pre-compile templates if using lua-resty-template
local template = require "resty.template"
template.caching(true)
template.load_lua_template = true
-- Initialize shared dictionaries
local cache_dict = ngx.shared.cache_dict
local stats = ngx.shared.stats
-- Set up global error handling
_G.handle_error = function(err)
ngx.log(ngx.ERR, "Lua error: ", err)
ngx.status = 500
ngx.say("Internal Server Error")
ngx.exit(500)
end
-- Initialize Redis connection pool
local redis_connector = require "redis_connector"
local red = redis_connector.connect()
if red then
redis_connector.set_keepalive(red)
ngx.log(ngx.INFO, "Redis connection pool initialized")
else
ngx.log(ngx.WARN, "Failed to initialize Redis connection pool")
end
Update main nginx configuration
Add the initialization script and performance directives to the main configuration.
worker_processes auto;
worker_rlimit_nofile 65535;
worker_cpu_affinity auto;
# Add to existing config
init_worker_by_lua_file /usr/local/openresty/nginx/conf/init.lua;
# Lua package path
lua_package_path '/usr/local/openresty/nginx/lua/?.lua;;';
# Lua code cache (disable only in development)
lua_code_cache on;
# DNS resolver for dynamic backends
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
</code><h2><code>Rest of existing configuration...</code></h2></pre>
</div>
<div class="step">
### Enable and start services
<p>Start OpenResty and Redis with the optimized configuration.</p>
<pre class="terminal"><code>sudo systemctl enable openresty
sudo systemctl restart openresty
sudo systemctl status openresty
Verify your setup
Test the caching system and performance optimizations to ensure everything works correctly.
# Test basic functionality
curl -H "Host: example.com" http://localhost/
# Check cache headers
curl -I -H "Host: example.com" http://localhost/
# Test cache statistics
curl http://localhost/admin/cache/stats
# Test cache invalidation
curl http://localhost/a
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Default configuration
REDIS_MAXMEM="${REDIS_MAXMEM:-1gb}"
DOMAIN="${1:-example.com}"
# Usage message
usage() {
echo "Usage: $0 [DOMAIN]"
echo "Example: $0 api.example.com"
exit 1
}
# Logging functions
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Cleanup on failure
cleanup() {
log_error "Installation failed. Cleaning up..."
systemctl stop openresty 2>/dev/null || true
systemctl stop redis-server redis 2>/dev/null || true
}
trap cleanup ERR
# Check prerequisites
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root"
exit 1
fi
if [[ $# -gt 1 ]]; then
usage
fi
# Auto-detect distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update"
REDIS_SERVICE="redis-server"
REDIS_CONFIG="/etc/redis/redis.conf"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf makecache"
REDIS_SERVICE="redis"
REDIS_CONFIG="/etc/redis.conf"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum makecache"
REDIS_SERVICE="redis"
REDIS_CONFIG="/etc/redis.conf"
;;
*)
log_error "Unsupported distro: $ID"
exit 1
;;
esac
else
log_error "Cannot detect distribution"
exit 1
fi
log_info "[1/8] Installing OpenResty and dependencies..."
$PKG_UPDATE
if [[ "$PKG_MGR" == "apt" ]]; then
wget -qO - https://openresty.org/package/pubkey.gpg | apt-key add -
echo "deb http://openresty.org/package/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/openresty.list
$PKG_UPDATE
$PKG_INSTALL openresty openresty-opm redis-server luarocks curl
else
$PKG_INSTALL dnf-plugins-core
dnf config-manager --add-repo https://openresty.org/package/centos/openresty.repo
$PKG_INSTALL openresty openresty-opm redis luarocks curl
fi
log_info "[2/8] Installing Lua Redis and caching libraries..."
/usr/local/openresty/bin/opm get ledgetech/lua-resty-redis-connector
/usr/local/openresty/bin/opm get openresty/lua-resty-redis
/usr/local/openresty/bin/opm get openresty/lua-resty-lrucache
/usr/local/openresty/bin/opm get bungle/lua-resty-template
luarocks install lua-resty-http
log_info "[3/8] Configuring Redis for high performance..."
cp "$REDIS_CONFIG" "${REDIS_CONFIG}.backup"
cat > "$REDIS_CONFIG" << 'EOF'
maxmemory 1gb
maxmemory-policy allkeys-lru
save ""
tcp-keepalive 300
timeout 0
tcp-backlog 511
maxclients 10000
notify-keyspace-events Ex
bind 127.0.0.1
port 6379
daemonize yes
EOF
log_info "[4/8] Creating OpenResty directories and configuration..."
mkdir -p /usr/local/openresty/nginx/conf/conf.d
mkdir -p /usr/local/openresty/nginx/lua
mkdir -p /var/log/openresty
chown -R nobody:nogroup /var/log/openresty 2>/dev/null || chown -R nobody:nobody /var/log/openresty
cat > /usr/local/openresty/nginx/conf/nginx.conf << 'EOF'
worker_processes auto;
worker_rlimit_nofile 65535;
worker_cpu_affinity auto;
error_log /var/log/openresty/error.log warn;
pid /var/run/openresty.pid;
events {
worker_connections 8192;
use epoll;
multi_accept on;
}
http {
include mime.types;
default_type application/octet-stream;
lua_shared_dict cache_dict 100m;
lua_shared_dict locks 10m;
lua_shared_dict stats 10m;
lua_socket_pool_size 100;
lua_socket_keepalive_timeout 60s;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 30;
keepalive_requests 1000;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript application/x-font-ttf font/opentype image/svg+xml;
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
include /usr/local/openresty/nginx/conf/conf.d/*.conf;
}
EOF
log_info "[5/8] Creating Lua Redis connection module..."
cat > /usr/local/openresty/nginx/lua/redis_connector.lua << 'EOF'
local redis = require "resty.redis"
local cjson = require "cjson"
local _M = {}
local redis_config = {
host = "127.0.0.1",
port = 6379,
timeout = 5000,
pool_size = 100,
keepalive = 60000
}
function _M.connect()
local red = redis:new()
red:set_timeout(redis_config.timeout)
local ok, err = red:connect(redis_config.host, redis_config.port)
if not ok then
ngx.log(ngx.ERR, "Failed to connect to Redis: ", err)
return nil, err
end
return red
end
function _M.set_keepalive(red)
if not red then
return
end
local ok, err = red:set_keepalive(redis_config.keepalive, redis_config.pool_size)
if not ok then
ngx.log(ngx.ERR, "Failed to set keepalive: ", err)
end
end
function _M.get_cached(key, ttl, callback)
local red, err = _M.connect()
if not red then
return callback()
end
local cached, err = red:get(key)
_M.set_keepalive(red)
if cached and cached ~= ngx.null then
return cjson.decode(cached)
end
local data = callback()
if data then
local red2, err = _M.connect()
if red2 then
red2:setex(key, ttl or 300, cjson.encode(data))
_M.set_keepalive(red2)
end
end
return data
end
return _M
EOF
log_info "[6/8] Creating sample virtual host configuration..."
cat > /usr/local/openresty/nginx/conf/conf.d/default.conf << EOF
server {
listen 80;
server_name $DOMAIN;
location /api/ {
limit_req zone=api burst=20 nodelay;
content_by_lua_block {
local redis_connector = require "redis_connector"
local cache_key = "api:" .. ngx.var.request_uri
local result = redis_connector.get_cached(cache_key, 300, function()
return {message = "Hello from cached API", timestamp = ngx.time()}
end)
ngx.header.content_type = "application/json"
ngx.say(require("cjson").encode(result))
}
}
location /health {
access_log off;
return 200 "OK\n";
add_header Content-Type text/plain;
}
location / {
return 200 "OpenResty with Redis caching is running";
add_header Content-Type text/plain;
}
}
EOF
log_info "[7/8] Setting permissions and starting services..."
chmod 644 /usr/local/openresty/nginx/conf/nginx.conf
chmod 644 /usr/local/openresty/nginx/conf/conf.d/default.conf
chmod 644 /usr/local/openresty/nginx/lua/redis_connector.lua
chmod 644 "$REDIS_CONFIG"
systemctl enable $REDIS_SERVICE
systemctl start $REDIS_SERVICE
systemctl enable openresty
systemctl start openresty
log_info "[8/8] Verifying installation..."
sleep 2
if ! systemctl is-active --quiet $REDIS_SERVICE; then
log_error "Redis is not running"
exit 1
fi
if ! systemctl is-active --quiet openresty; then
log_error "OpenResty is not running"
exit 1
fi
# Test Redis connection
if ! redis-cli ping >/dev/null 2>&1; then
log_error "Cannot connect to Redis"
exit 1
fi
# Test OpenResty
if ! curl -s http://localhost/health | grep -q "OK"; then
log_error "OpenResty health check failed"
exit 1
fi
log_info "Installation completed successfully!"
echo
log_info "Services status:"
echo "Redis: $(systemctl is-active $REDIS_SERVICE)"
echo "OpenResty: $(systemctl is-active openresty)"
echo
log_info "Test URLs:"
echo "Health check: http://$DOMAIN/health"
echo "Cached API: http://$DOMAIN/api/test"
echo
log_info "Configuration files:"
echo "OpenResty: /usr/local/openresty/nginx/conf/nginx.conf"
echo "Virtual host: /usr/local/openresty/nginx/conf/conf.d/default.conf"
echo "Redis config: $REDIS_CONFIG"
echo "Lua modules: /usr/local/openresty/nginx/lua/"
Review the script before running. Execute with: bash install.sh