Optimize OpenResty performance with advanced caching strategies and Lua optimization

Advanced 45 min May 20, 2026 48 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

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;

Rest of existing configuration...

Enable and start services

Start OpenResty and Redis with the optimized configuration.

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/admin/cache/invalidate/page:*

Check Redis connection

redis-cli ping

Monitor OpenResty error log

sudo tail -f /var/log/openresty/error.log
Performance verification: Use tools like ab or wrk to load test your cached endpoints. You should see significantly improved response times and reduced backend load with cache hit rates above 90%.

Common issues

Symptom Cause Fix
Lua module not found Incorrect lua_package_path Check path in nginx.conf and verify module location
Redis connection failures Redis not running or connection pool exhausted sudo systemctl restart redis-server and check connection limits
Memory usage growing continuously Memory leaks in Lua code Enable lua_code_cache off temporarily and check for circular references
Cache not working Shared memory zones too small Increase shared dict sizes in nginx.conf
High CPU usage LuaJIT optimization disabled Ensure lua_code_cache on in production
502 Bad Gateway Lua runtime errors Check error log: tail -f /var/log/openresty/error.log

Next steps

Running this in production?

Need this managed? Running high-performance caching at scale adds complexity: capacity planning, cache warming strategies, Redis clustering, and performance monitoring across environments. See how we run infrastructure like this for European teams who need OpenResty performance without the operational overhead.

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle infrastructure performance optimization for businesses that depend on uptime. From initial setup to ongoing operations.