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_timeoutsbeforeconnect - Always check for
ngx.nullon read operations, notnil - Use
init_pipeline+commit_pipelinefor batch commands - Return connections with
set_keepalive, notclose - Call
closeonly when you genuinely want to drop the connection - Lowercase method names:
red:get, notred:GET - Add a
resolverdirective 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
- OpenResty Getting Started — set up your first OpenResty application from scratch
- OpenResty Routing and Middleware — build request routing and middleware chains with OpenResty