luaguides

Using Redis from Lua in OpenResty

The fastest way to get Redis data in OpenResty

Here is the smallest working example. You have a Redis instance running on 127.0.0.1:6379, and you want to read a key and return it in an HTTP response:

local redis = require "resty.redis"
local red = redis:new()

red:set_timeouts(1000, 1000, 1000)  -- connect, send, read (ms)

local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
    ngx.say("failed to connect: ", err)
    return
end

local res, err = red:get("mykey")
if err then
    ngx.say("redis error: ", err)
    red:close()
    return
end

if res == ngx.null then
    ngx.say("key not found")
else
    ngx.say("value: ", res)
end

red:set_keepalive(10000, 100)

That’s it. require the library, create an object, set timeouts, connect, run commands, return the connection to the pool. Everything else is details built on top of this pattern.

This article walks through all of those details so you can use Redis properly in production OpenResty applications.

Which library to use

Use lua-resty-redis. It is the community-standard Lua client for OpenResty, built on the ngx_lua cosocket API, and is fully nonblocking. It ships with OpenResty and covers every Redis command as a direct method call.

The alternative is ngx.redis2, the older built-in driver. It works, but the API is less ergonomic and it does not support connection pooling as cleanly. Unless you have a specific reason to use ngx.redis2, go with lua-resty-redis.

Setting timeouts before you connect

Always set timeouts before calling connect. The set_timeouts method (v0.28+) lets you configure connect, send, and read timeouts independently, in milliseconds:

red:set_timeouts(1000, 1000, 1000)  -- connect, send, read

The older set_timeout (singular) sets all three to the same value. Use set_timeouts when you need fine control — for example, giving reads more time than connects:

red:set_timeouts(1000, 1000, 5000)  -- 5s read timeout for large values

Connecting to Redis

The connect method accepts a host and port, or a Unix socket path:

-- TCP
local ok, err = red:connect("127.0.0.1", 6379)

-- Unix domain socket
local ok, err = red:connect("unix:/var/run/redis.sock")

If you need TLS, SSL verification, or custom pool settings, pass an options table as the third argument:

local ok, err = red:connect("redis.example.com", 6379, {
    ssl         = true,
    ssl_verify  = true,
    server_name = "redis.example.com",
    pool_size   = 100,
    backlog     = 50,
})

A couple of things to keep in mind: connecting to a hostname requires a resolver directive in your nginx.conf — without it, name resolution will fail. Also, the pool_size and backlog options require ngx_lua 0.10.14 or later.

For more complex setups — URL parsing, Sentinel support, automatic pool management — take a look at lua-resty-redis-connector.

Running Redis commands

Every Redis command is a method on the redis object, named in lowercase. The arguments follow the Redis command reference directly.

-- Strings
ok, err = red:set("session:abc", '{"user_id": 42}')
val, err = red:get("session:abc")

-- Integers (e.g. counters)
n, err = red:incr("request_count")

-- Expiry
ok, err = red:expire("session:abc", 3600)

-- Hashes
ok, err = red:hmset("user:42", {
    name  = "Alice",
    email = "alice@example.com",
})
name, err = red:hget("user:42", "name")

-- Sets
ok, err = red:sadd("online_users", "alice", "bob")
members, err = red:smembers("online_users")

-- Lists
ok, err = red:lpush("log_queue", "message one", "message two")
entries, err = red:lrange("log_queue", 0, -1)

Return types follow the Redis reply types: status replies become the string "OK" (or "PONG" for ping), integers become Lua numbers, bulk strings become Lua strings, and arrays become Lua tables.

Handling ngx.null correctly

When a key does not exist, Redis returns a nil bulk reply. In lua-resty-redis, this maps to the OpenResty constant ngx.null — not Lua nil. This catches a lot of people out.

Always compare with ngx.null:

local res, err = red:get("nonexistent")
if err then
    ngx.log(ngx.ERR, "redis error: ", err)
    return
end
if res == ngx.null then
    ngx.say("key not found, no value stored")
else
    ngx.say("value is: ", res)
end

Checking if res == nil will not work. ngx.null is a light userdata value, distinct from nil.

For pipeline results, individual commands that return nil will appear as ngx.null inside the results array. Iterate with ipairs:

for i, v in ipairs(results) do
    if v == ngx.null then
        ngx.say("command ", i, " returned nil")
    end
end

Pipelining for batch operations

If you need to run several commands at once, pipelining sends them all in a single network round-trip instead of waiting for a reply after each one. Use init_pipeline, fire your commands, then commit_pipeline:

red:init_pipeline()
red:get("key1")
red:get("key2")
red:get("key3")
red:set("key4", "value4")
local results, err = red:commit_pipeline()

commit_pipeline returns a table of replies in the same order as the commands were queued. If the pipeline itself fails (e.g. the connection drops mid-flight), it returns nil and an error string.

Pipeline errors are distinct from individual command errors. A failed command inside the pipeline returns an array entry like {false, "error message"} — you need to check for that:

for i, res in ipairs(results) do
    if type(res) == "table" and res[1] == false then
        ngx.log(ngx.WARN, "command ", i, " error: ", res[2])
    end
end

Pipelining mode is sticky — once you call init_pipeline, the redis object stays in that mode until you call commit_pipeline or cancel_pipeline. Forgetting to commit means commands are never sent over the network.

Pub/Sub

Redis Pub/Sub lets you subscribe to channels and receive messages pushed from the server. This is different from the regular request-response pattern: once you call subscribe, the connection enters a message loop and you read messages with read_reply.

local red2 = redis:new()
red2:set_timeouts(1000, 1000, 1000)
red2:connect("127.0.0.1", 6379)

red2:subscribe("my_channel")
while true do
    local res, err = red2:read_reply()
    if err then
        ngx.log(ngx.ERR, "subscribe error: ", err)
        break
    end
    ngx.say("got message: ", cjson.encode(res))
end

Use a separate redis connection for Pub/Sub — once you enter subscribe mode, that connection is dedicated to receiving messages and cannot run regular commands at the same time. The psubscribe method works the same way but accepts glob-style patterns, so you can listen to multiple channels with a single subscription.

Note that read_reply blocks until a message arrives. In an OpenResty content handler, this holds the request open until a message is received, which is the intended behaviour for server-push use cases. If you need to time out of the message loop, set a read timeout before subscribing.

Transactions

Redis transactions let you group commands atomically with multi and exec. The watch command adds optimistic locking — the transaction aborts if another client modifies a watched key before it runs.

red:watch("counter")
red:multi()
red:incr("counter")
local results, err = red:exec()

lua-resty-redis has no special helpers for transactions — you call the commands directly. Note that unlike pipelining, exec waits for a reply after each queued command, so transactions still involve a round-trip per command unless you combine them with pipelining.

Connection pooling and keepalive

You have two choices when you are done with a request: return the connection to the pool with set_keepalive, or close it with close. Always prefer set_keepalive. Closing kills the connection; keeping it alive lets the next request reuse it without the TCP handshake and authentication overhead.

-- Return to pool: max 100 idle connections, each idle for 10 seconds
local ok, err = red:set_keepalive(10000, 100)

The first argument is the maximum idle time in milliseconds before the connection is closed. The second is the maximum number of connections allowed in the pool per Nginx worker.

If you need to know whether the connection came from the pool (and therefore whether you need to re-authenticate), use get_reused_times:

if red:get_reused_times() == 0 then
    -- fresh connection, authenticate if needed
    local ok, err = red:auth("redis_password")
    if not ok then
        ngx.log(ngx.ERR, "auth failed: ", err)
        return
    end
end

Cosocket pools are per worker process, not shared across all Nginx workers. Size your pool accordingly — if you have 4 workers each with a pool of 100 connections, you can have up to 400 connections to Redis from a single machine.

Do not call both close and set_keepalive on the same connection. After set_keepalive, the connection is owned by the pool and the redis object is ready for reuse via another connect call.

Full request lifecycle

Putting it all together, here is a realistic request handler with proper error handling, timeout configuration, and connection pooling:

local redis = require "resty.redis"
local red = redis:new()

red:set_timeouts(1000, 1000, 5000)  -- 1s connect, 1s send, 5s read

local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
    ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE)
end

-- Optionally authenticate if Redis requires a password
if red:get_reused_times() == 0 then
    ok, err = red:auth("your_redis_password")
    if not ok then
        red:close()
        ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
    end
end

local data, err = red:get("cache:frontpage")
if err then
    red:close()
    ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end

if data == ngx.null then
    -- Cache miss — fetch from upstream, then store in Redis
    data = "expensive_upstream_response"  -- in practice: call your upstream here
    ok, err = red:setex("cache:frontpage", 300, data)
    if not ok then
        ngx.log(ngx.ERR, "failed to set cache: ", err)
    end
end

red:set_keepalive(10000, 100)

ngx.say(data)

This is the shape you will see in most production OpenResty code. The pattern is: set timeouts, connect, optionally authenticate on fresh connections, run your commands, return to pool.

Command method names are lowercase

A small but common mistake: Redis command methods are all lowercase. red:SET(...) will error. red:set(...) works. This trips people up when they copy-paste from the Redis CLI documentation where commands are shown in uppercase.

One more thing: resolver for hostnames

If you call connect with a hostname instead of an IP address, you must have a resolver directive in your nginx.conf:

resolver 8.8.8.8;

Without it, connect will hang waiting for a DNS result that never comes. For local development, 127.0.0.1 does not need a resolver.

Summary

Here is the concise checklist for using Redis in OpenResty:

  • Use lua-resty-redis (not ngx.redis2)
  • Call set_timeouts before connect
  • Always check for ngx.null on read operations, not nil
  • Use init_pipeline + commit_pipeline for batch commands
  • Return connections with set_keepalive, not close
  • Call close only when you genuinely want to drop the connection
  • Lowercase method names: red:get, not red:GET
  • Add a resolver directive if connecting to hostnames

The library is a thin, direct mapping of Redis commands to Lua methods — if you know the Redis command you want, the lua-resty-redis method has the same name and roughly the same arguments. The Redis command reference is your documentation.

See Also