lua-resty-redis-cluster-fast

Asynchronous Redis Cluster client for OpenResty, with authentication, TLS, pipelines, subscriptions, and transactions.

Name

lua-resty-redis-cluster-fast - asynchronous Redis Cluster client for OpenResty

Table of Contents

Status

This library is considered production ready.

Description

This Lua library is a Redis client driver for the ngx_lua nginx module:

https://github.com/openresty/lua-nginx-module/#readme

This library provides asynchronous Redis Cluster access for OpenResty.

The new 1.0.2 implementation supports only Nginx 1.31.8, bundled with OpenResty 1.31.8.x, and requires OpenResty 1.31.8.1 or newer in that series. RPM and DEB packages use the same configuration and Lua API. The only supported entry point is require "resty.redis.cluster.fast"; replace any previous require "resty.redis.cluster" import when upgrading.

Legacy packages remain available for their matching OpenResty versions. The legacy implementation requires at least ngx_lua 0.10.26 or OpenResty 1.21.4.3. This older minimum does not apply to the new implementation. Differences below are marked explicitly; legacy installation and configuration examples are retained.

See the installation and usage guide for the version matrix and a complete configuration for the new implementation.

Performance relative to the open-source client

In a typical concurrent-read Redis Cluster application scenario, lua-resty-redis-cluster-fast achieved 7.14 times the throughput of open-source lua-resty-redis-cluster (latest on 2026-11-05), a 613.8% increase.

Metriclua-resty-redis-cluster-fastlua-resty-redis-cluster (latest on 2026-11-05)
Median GET throughput192,848 commands/s27,019 commands/s
Median GET p99 latency0.972 ms6.821 ms

The comparison uses shared mode at equal Lua concurrency with different physical connection counts. Actual gains depend on workload, connection settings, and deployment.

Synopsis

For the new 1.0.2 implementation, load the following module in the main context of nginx.conf and use the Lua calls in the example below:

load_module /usr/local/openresty/nginx/modules/ngx_redis_cluster_module.so;

The new implementation does not require coro_preload or coro_stack_size. The package installs Lua bytecode modules in OpenResty’s default /usr/local/openresty/site/lualib search path. Custom lua_package_path settings must preserve the defaults with ;; or include /usr/local/openresty/site/lualib/?.ljbc. Custom OpenResty installations must also provide lua-resty-core and lua-resty-lrucache on the Lua search path.

The full configuration below is retained for the legacy implementation. Its coro module loading and preload directives apply only to that implementation.

    # you need to load the corresponding dynamic modules
    load_module "/usr/local/openresty-coro-nginx-module/lib/ngx_http_coro_module.so";
    load_module "/usr/local/openresty-coro-nginx-module/lib/ngx_http_coro_hiredis_module.so";

    http {
        # you do not need the following line if you are using
        # the OpenResty bundle:
        lua_package_path "/usr/local/openresty/site/lualib/?.lua;;";

        # specify the location of the hiredis
        coro_preload /usr/local/openresty-plus/hiredis/lib/libhiredis.so;
        coro_stack_size  32768;

        server {
            location /test {
                # need to specify the resolver to resolve the hostname
                resolver 8.8.8.8;

                content_by_lua_block {
                    local redis = require "resty.redis.cluster.fast"
                    local red = redis:new()

                    red:set_timeouts(3000, 60000, 60000) -- 3 sec, 60s, 60s

                    -- connect via ip address directly
                    local ok, err = red:connect("127.0.0.1", 6379, {
                        cluster = "cluster",
                        other_nodes = {
                            { "127.0.0.1", 6380 },
                            { "127.0.0.1", 6381 },
                            { "127.0.0.1", 6382 },
                        },
                        pool_size = 16,
                        backlog = 512,
                        no_slaves = true
                    })

                    if not ok then
                        ngx.say("failed to connect: ", err)
                        return
                    end

                    ok, err = red:set("dog", "an animal")
                    if not ok then
                        ngx.say("failed to set dog: ", err)
                        return
                    end

                    ngx.say("set result: ", ok)

                    local res, err = red:get("dog")
                    if not res then
                        ngx.say("failed to get dog: ", err)
                        return
                    end

                    if res == ngx.null then
                        ngx.say("dog not found.")
                        return
                    end

                    ngx.say("dog: ", res)

                    local ok, err = red:set_keepalive()
                    if not ok then
                        ngx.say("failed to set keepalive: ", err)
                        return
                    end

                    -- or just close the connection right away:
                    -- local ok, err = red:close()
                    -- if not ok then
                    --     ngx.say("failed to close: ", err)
                    --     return
                    -- end
                }
            }
        }
    }

Back to TOC

Methods

All of the Redis commands have their own methods with the same name except all in lower case.

You can find the complete list of Redis commands here:

http://redis.io/commands

You need to check out this Redis command reference to see what Redis command accepts what arguments.

The Redis command arguments can be directly fed into the corresponding method call. For example, the “GET” redis command accepts a single key argument, then you can just call the “get” method like this:

    local res, err = red:get("key")

Similarly, the “LRANGE” redis command accepts threee arguments, then you should call the “lrange” method like this:

    local res, err = red:lrange("nokey", 0, 1)

For example, “SET”, “GET”, “LRANGE”, and “BLPOP” commands correspond to the methods “set”, “get”, “lrange”, and “blpop”.

Here are some more examples:

    -- HMGET myhash field1 field2 nofield
    local res, err = red:hmget("myhash", "field1", "field2", "nofield")
    -- HMSET myhash field1 "Hello" field2 "World"
    local res, err = red:hmset("myhash", "field1", "Hello", "field2", "World")

Command methods return their Redis reply on success. In the new implementation, Redis errors and connection or transport failures return nil, err. Use err ~= nil to detect failure: a successful RESP3 boolean reply can be false, nil. When upgrading from the earlier false, err behavior, update checks such as res == false accordingly. Nested array, pipeline, and EXEC error entries remain {false, err}.

A Redis “status reply” results in a string typed return value with the “+” prefix stripped.

A Redis “integer reply” results in a Lua number typed return value.

A Redis “error reply” returns nil and a string describing the error in the new implementation. The legacy implementation returns false, err.

A non-nil Redis “bulk reply” results in a Lua string as the return value. A nil bulk reply results in a ngx.null return value.

A non-nil Redis “multi-bulk reply” results in a Lua table holding all the composing values (if any). If any of the composing value is a valid redis error value, then it will be a two element table {false, err}.

A nil multi-bulk reply returns in a ngx.null value.

With RESP3 enabled in the new implementation, maps become Lua key/value tables, sets and push frames become arrays, doubles become numbers, and booleans become Lua booleans. A false boolean reply without an error string is a valid result. Big numbers remain strings to preserve their representation.

See http://redis.io/topics/protocol for details regarding various Redis reply types.

In addition to all those redis command methods, the following methods are also provided:

Back to TOC

new

syntax: red, err = redis:new()

Creates a redis object. In case of failures, returns nil and a string describing the error.

In the new implementation, new() creates an unconnected client and does not consume configuration arguments. Passing new(config) does not connect or apply those options. Configure timeouts with the setters and call connect() explicitly.

Back to TOC

connect

Unix domain sockets are not supported. SSL/TLS is supported by the new 1.0.2 implementation; the legacy implementation does not support SSL.

syntax: ok, err = red:connect(ip, port, options_table)

Attempts to connect to the host and port that the Redis entry node is listening to.

Before actually connecting to the remote backend, this method will always look up the connection pool for matched idle connections created by previous calls of this method.

In the new implementation, ordinary clients can also borrow an active shared physical connection within the same worker. Pools distinguish the node, cluster name, credentials, protocol, and TLS settings. Configure Nginx resolver when seeds or discovered nodes use hostnames.

The options_table argument is a Lua table holding the following keys:

  • cluster

    Specifies a custom name for the Redis cluster connection pool being used, this is a required field.

  • pool_size

    Limits retained idle connections for each node and pool identity. The new implementation defaults to 8; the legacy default is 128. This does not set the number of active ordinary-command connections; use connection_count for that. A pool_size smaller than connection_count can discard idle connections before their next round-robin turn.

  • backlog

    In the new implementation, this option is accepted for compatibility and ignored. It does not limit concurrent clients or create a waiting queue. pool_size limits retained idle connections per node, not concurrent Lua clients.

    In the legacy implementation, it specifies the size of the backlog, and the default is -1 (which means disable by default).

    For the legacy implementation, when the backlog parameter is not specified, the number of connections from the same upstream is not limited. In this case, the pool_size parameter limits the total number of idle connections in the connection pool. When configuring the backlog parameter, the concurrent request count from the same upstream is limited to pool_size, and requests exceeding pool_size will be queued for waiting. The backlog value determines the maximum length of the waiting queue, and requests exceeding this value will directly return an error.

  • other_nodes

    Specifies other nodes that can be connected to, and it will try to connect to other nodes when the connection to the entry node fails.

    The other_nodes argument is a Lua table that holds configuration information for multiple nodes, such as:

    {
        { "127.0.0.1", 6379 },
        { "127.0.0.1", 6380 },
        { "127.0.0.1", 6381 },
    }
    
  • no_slaves

    Whether to prohibit reading data from Redis slave nodes, the default is false.

    Replica reads can return stale data. Set this to true when reads must observe preceding writes. In the new implementation, writes, conditional writers such as SORT, transactions, and pipelines containing writes use masters; pure read pipelines can use replicas. Replica connections receive READONLY after authentication and use the same TLS and RESP settings as master connections.

The following options apply to the new 1.0.2 implementation:

OptionMeaning
connection_countShared ordinary-command connections per worker, node, and pool identity; default 3. Integer from 1 through 2147483647; connections open on demand.
max_pendingMaximum outstanding operations per physical connection; default 0 (unlimited). Integer from 0 through 2147483647. A full queue waits within the read timeout, then returns nil, "pending queue full"; a zero read timeout permits an unbounded wait.
max_idle_timeoutIdle connection expiry in milliseconds, default 300000; 0 disables expiry.
username, passwordRedis 6+ ACL credentials; username requires password. Set only password for the default user.
use_resp3Set to true to use RESP3; defaults to RESP2.
sslSet to true to enable TLS. TLS-specific options also enable TLS; combining them with ssl = false is an error.
ssl_verifyVerify the certificate chain; defaults to true.
ssl_ca_certCA certificate file; otherwise use OpenSSL’s default trust store.
ssl_cert, ssl_keyClient certificate and private key files for mutual TLS.
ssl_server_nameSNI name; server_name and ssl_sni are aliases.
ssl_verify_nameExpected certificate DNS name or IP; requires verification. SNI alone does not set this check.
pubsub_max_messagesMaximum queued replies per subscription or MONITOR connection, default 1024; positive integer.
pubsub_max_bytesMaximum bytes for those queued replies, default 8388608; positive integer.

For compatibility with earlier interfaces, serv_list = {{ip = host, port = port}, ...} supplies additional seeds, and no_slave = true or no_slave = 1 is an alias for no_slaves when that option is omitted.

Credentials and TLS settings apply to seeds, discovered masters and replicas, redirects, and subscription connections. For example:

local ok, err = red:connect("redis.example.com", 8000, {
    cluster = "orders-tls",
    username = "orders-app",
    password = password,
    ssl = true,
    ssl_ca_cert = "/etc/redis/ca.crt",
    ssl_server_name = "redis.example.com",
    ssl_verify_name = "redis.example.com",
    no_slaves = true,
})

Replace the paths, hostnames, and password variable with your configuration. Allow PING and CLUSTER SLOTS for connection setup and discovery, HELLO for RESP3, and READONLY for replica reads. Incorrect configured credentials fail connect().

Back to TOC

set_timeout

syntax: red:set_timeout(time)

Sets the timeout (in ms) protection for subsequent operations, including the connect method.

In the new implementation, the value must be an integer from 0 through 2147483647; 0 disables the threshold. Invalid numeric values return nil, "invalid timeout" without changing existing settings. A non-number raises an argument error. Success returns no values; do not assert a truthy return.

Prefer set_timeouts when different phases need different thresholds.

Back to TOC

set_timeouts

syntax: red:set_timeouts(connect_timeout, send_timeout, read_timeout)

Respectively sets the connect, send, and read timeout thresholds (in ms), for subsequent socket operations. Setting timeout thresholds with this method offers more granularity than set_timeout. As such, it is preferred to use set_timeouts over set_timeout.

In the new implementation, these thresholds are independent inactivity timeouts. Connect and send time do not consume the following phase’s budget. Progress renews the corresponding I/O deadline; progress by an earlier writer on a shared connection does not renew another request’s send deadline. Zero disables that threshold. All three default to 60000 milliseconds.

Topology discovery uses send/read timeouts within the overall seed connection budget. Reusing an idle connection restores the borrowing client’s timeout settings. Each borrower has independent operation timeouts and cancellation state.

All values must be integer milliseconds from 0 through 2147483647. Invalid numeric values return nil with "invalid connect_timeout", "invalid send_timeout", or "invalid read_timeout", leaving all existing settings unchanged. Non-numbers raise argument errors. Success returns no values.

Back to TOC

set_keepalive

syntax: ok, err = red:set_keepalive()

In the legacy implementation, puts the current Redis connection immediately into the ngx_lua cosocket connection pool.

In the new implementation, releases this client’s references to connections for all visited nodes and returns 1. Other clients can continue using a shared physical connection. Idle retention uses pool_size and max_idle_timeout from connect(). The compatibility arguments set_keepalive(max_idle_timeout, pool_size) are accepted but ignored.

Clients in Pub/Sub, MONITOR, WATCH, or a transaction cannot be returned to the idle pool. Finish the stateful operation or use close() instead.

Only call this method in the place you would have called the close method instead. Calling this method will immediately turn the current redis object into the closed state. Any subsequent operations other than connect() on the current object will return the closed error.

Back to TOC

get_reused_times

syntax: times, err = red:get_reused_times()

This method returns the (successfully) reused times for the current connection. In case of error, it returns nil and a string describing the error.

If the current connection does not come from the built-in connection pool, then this method always returns 0, that is, the connection has never been reused (yet). If the connection comes from the connection pool, then the return value is always non-zero. So this method can also be used to determine if the current connection comes from the pool.

In the new implementation, the count refers to the currently selected physical connection, including shared active connections. It can change when routing selects another node or round-robin selection chooses another connection on the same node. After set_keepalive() or close(), this method returns nil, "closed".

Back to TOC

close

syntax: ok, err = red:close()

Closes the current redis connection and returns the status.

In the new implementation, releases all of the client’s node references and closes the Lua object. A shared physical connection closes only when no other client uses it. A successful connect() reopens the object; a failed reconnect leaves it closed.

In case of success, returns 1. In case of errors, returns nil with a string describing the error.

Back to TOC

init_pipeline

Supported by the new 1.0.2 implementation. Pipelines are not supported by the legacy implementation.

syntax: red:init_pipeline()

syntax: red:init_pipeline(n)

Enable the redis pipelining mode. All subsequent calls to Redis command methods will automatically get cached and will send to the server in one run when the commit_pipeline method is called or get cancelled by calling the cancel_pipeline method.

On a closed object, this method returns nil, "closed".

If the redis object is already in the Redis pipelining mode, then calling this method will discard existing cached Redis queries.

The optional n argument is accepted for compatibility and ignored.

Back to TOC

commit_pipeline

Supported by the new implementation; not supported by the legacy implementation.

syntax: results, err = red:commit_pipeline()

Quits the pipelining mode by committing all the cached Redis queries to the remote server in a single run. All the replies for these queries will be collected automatically and are returned as if a big multi-bulk reply at the highest level.

This method returns nil and a Lua string describing the error upon failures.

Results preserve command order even across nodes. Redis errors become {false, err} entries. MGET, MSET, DEL, EXISTS, UNLINK, and TOUCH support keys on different nodes, without cross-node atomicity. A transport error can occur after some commands have executed; uncertain writes and partially executed write pipelines are not automatically replayed. Do not retry them unconditionally.

Same-slot multi-key requests are sent as single Redis commands, preserving their atomicity and validation before writes. Cross-slot requests may partly succeed. Reply order does not imply total execution order across nodes or atomicity of the entire pipeline. Committing without initialization returns nil, "no pipeline"; an initialized empty pipeline returns an empty table. Queuing commands returns no values.

Pipelines containing writes use masters so subsequent reads observe preceding writes on the same node. Pure read pipelines can use replicas. Cancellation and transport failures release operations and collected replies for every participating node while preserving other clients sharing the physical connections.

Subscription commands and starting a transaction are not allowed inside ordinary pipelines. AUTH, HELLO (with or without AUTH), RESET, WAIT, and WAITAOF act as ordered boundaries. WAIT and WAITAOF apply to the preceding command’s physical connection, not writes on other round-robin connections or across the cluster; sharing that connection can also include preceding writes by other borrowers.

Back to TOC

cancel_pipeline

Supported by the new implementation; not supported by the legacy implementation.

syntax: red:cancel_pipeline()

Quits the pipelining mode by discarding all existing cached Redis commands since the last call to the init_pipeline method.

On a closed object, this method returns nil, "closed".

If the redis object is not in the Redis pipelining mode, then this method is a no-op.

Back to TOC

hmset

syntax: res, err = red:hmset(myhash, field1, value1, field2, value2, ...)

syntax: res, err = red:hmset(myhash, { field1 = value1, field2 = value2, ... })

Special wrapper for the Redis “hmset” command.

When there are only three arguments (including the “red” object itself), then the last argument must be a Lua table holding all the field/value pairs.

Back to TOC

array_to_hash

syntax: hash = red:array_to_hash(array)

Auxiliary function that converts an array-like Lua table into a hash-like table.

This method was first introduced in the v0.11 release.

Back to TOC

read_reply

syntax: res, err = red:read_reply()

Reads a reply from the Redis server. This method is mostly useful for the Redis Pub/Sub API.

In the new implementation, subscriptions use a dedicated connection, and read timeouts preserve the subscription. Exceeding pubsub_max_messages or pubsub_max_bytes closes that connection, clears queued replies, and returns nil, "pub/sub buffer limit exceeded" until reconnect. Reconnect and subscribe again to resume receiving messages. monitor() also uses a dedicated stream read through this method; stop it with reset() or close().

Redis 7 sharded Pub/Sub is supported through ssubscribe, sunsubscribe, and spublish. Shard channels route by hash slot. An active subscriber stays on its selected server; use separate clients for subscriptions on different nodes.

Redis confirms every unsubscribe argument, including unknown and repeated names. Confirmations keep their received order with messages and remain readable after the last known subscription disappears. Read timeouts preserve confirmations still in flight.

Shard channels are independent of global channels and patterns, including when their names match. Subscription errors, including MOVED, CROSSSLOT, and invalid arguments, preserve the existing stream. The initial subscription follows redirects while retaining an isolated connection.

The subscription byte budget includes reply containers, strings, and queue entries, excluding allocator overhead and the reply currently being parsed. Consuming a reply releases its queue budget; an overflow error persists until reconnect rather than silently skipping messages.

A MONITOR connection cannot execute other commands or return to the idle pool; MONITOR cannot be queued in pipelines or transactions. Read timeouts preserve the stream. Failed RESET and HELLO commands preserve the existing stream too.

Ordinary commands and ordinary command pipelines on a subscribed client retain its subscription socket and Redis’s protocol-specific command restrictions. Ordinary command replies go to their callers; subscription messages remain available through read_reply(). Canceling or timing out an ordinary command discards its late reply without ending the subscription or letting that reply satisfy another command. A MONITOR stream rejects ordinary pipelines without closing its connection.

For example:

    local cjson = require "cjson"
    local redis = require "resty.redis.cluster.fast"

    local red = redis:new()
    local red2 = redis:new()

    red:set_timeouts(1000, 1000, 1000) -- 1 sec
    red2:set_timeouts(1000, 1000, 1000) -- 1 sec

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

    ok, err = red2:connect("127.0.0.1", 6379, { cluster = "cluster" })
    if not ok then
        ngx.say("2: failed to connect: ", err)
        return
    end

    local res, err = red:subscribe("dog")
    if not res then
        ngx.say("1: failed to subscribe: ", err)
        return
    end

    ngx.say("1: subscribe: ", cjson.encode(res))

    res, err = red2:publish("dog", "Hello")
    if not res then
        ngx.say("2: failed to publish: ", err)
        return
    end

    ngx.say("2: publish: ", cjson.encode(res))

    res, err = red:read_reply()
    if not res then
        ngx.say("1: failed to read reply: ", err)
        return
    end

    ngx.say("1: receive: ", cjson.encode(res))

    red:close()
    red2:close()

Running this example gives the output like this:

1: subscribe: ["subscribe","dog",1]
2: publish: 1
1: receive: ["message","dog","Hello"]

The following class methods are provieded:

Back to TOC

add_commands

syntax: hash = redis.add_commands(cmd_name1, cmd_name2, ...)

WARNING this method is now deprecated since we already do automatic Lua method generation for any redis commands the user attempts to use and thus we no longer need this.

Adds new redis commands to the resty.redis class. Here is an example:

    local redis = require "resty.redis.cluster.fast"

    redis.add_commands("foo", "bar")

    local red = redis:new()

    red:set_timeouts(1000, 1000, 1000) -- 1 sec

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

    local res, err = red:foo("a")
    if not res then
        ngx.say("failed to foo: ", err)
    end

    res, err = red:bar()
    if not res then
        ngx.say("failed to bar: ", err)
    end

Back to TOC

Connection Lifecycle

The following behavior applies to the new implementation. Ordinary commands multiplex over physical connections shared within one Nginx worker. connection_count defaults to 3 per node and pool identity; connections open on demand. Successive commands rotate through connections, including commands issued by the same Lua client. Each pipeline batch selects one connection per destination node and advances that node’s rotation once. Failed or expired shared connections are replaced on their next turn. Set connection_count = 1 for single-connection multiplexing.

Pool identity includes the cluster name, credentials, protocol, TLS configuration, connection_count, max_pending, and subscription buffer limits. Timeouts, seeds, and routing policies do not create separate physical connection pools. pool_size defaults to 8 and bounds retained idle connections per node and identity; backlog is ignored. max_pending limits outstanding operations on each physical connection; a full queue waits within the read timeout, returning nil, "pending queue full" on expiry. A zero read timeout permits an unbounded wait.

Blocking commands, WATCH, transactions, subscriptions, and connection-state commands use isolated connections. Settings such as CLIENT SETNAME and HELLO persist for subsequent commands and pipelines on the same node. Releasing that client discards the changed connection so later borrowers receive a clean session. Keepalive, close, and garbage collection release references for all visited nodes.

WAIT and WAITAOF are exceptions to connection isolation: they use the preceding command’s physical connection because acknowledgements refer to writes sent on that connection. They never reconnect or switch to an idle socket to obtain an acknowledgement. They cover that connection, not earlier writes on other round-robin connections or across the cluster. Sharing a connection can also make them wait for preceding writes from other clients, and a pending WAIT can delay other commands sharing that connection. In pipelines, these commands wait for preceding commands to finish before executing.

Use a separate Lua client object for each concurrent operation. Shared physical connections do not make a single Lua client object safe for concurrent use.

Back to TOC

Redis Authentication

The legacy implementation does not support the AUTH command. The new implementation supports both credentials in connect() and red:auth(password) or red:auth(username, password) for Redis 6+ ACL users.

Successful AUTH returns "OK", isolates the session from shared borrowers, and applies the credentials to later node connections. Redis authentication errors return nil, err and preserve previous credentials; transport and initialization failures also return nil, err. When connecting without credentials to a protected server, authentication completes deferred protocol setup and topology discovery.

Authentication applies to discovered masters, replicas, subscription connections, redirects, and newly opened connections. Empty and binary passwords are supported. Different users sharing a password retain separate connections and permissions. AUTH on an existing exclusive session retains its physical connection and connection name. Without authentication, protected commands return Redis’s NOAUTH errors.

AUTH and HELLO AUTH in pipelines form ordered boundaries: preceding requests finish before authentication, and following requests use the resulting credentials. AUTH queued inside MULTI changes credentials only when EXEC executes it successfully. DISCARD, canceled pipelines, and aborted transactions preserve previous credentials. Redis’s RESP2 subscription restrictions still apply; RESP3 permits AUTH while subscribed. Authentication and protocol controls receive their own replies; pending subscription messages remain readable through read_reply().

HELLO without AUTH also forms an ordered pipeline boundary. Successful protocol changes, including HELLO executed by EXEC, apply to future node connections. Failed HELLO commands, canceled pipelines, and aborted or discarded transactions preserve the previous protocol setting.

Successful RESET ends subscriptions, MONITOR, and WATCH, clears stored credentials, and restores RESP2. RESET also forms an ordered pipeline boundary, including when mixed with AUTH. Authenticate again before protected commands; use hello(3, "AUTH", username, password) to authenticate and select RESP3 together. Failed RESET or HELLO preserves the existing stream.

Redis uses the AUTH command to do authentication: http://redis.io/commands/auth

There is nothing special for this command as compared to other Redis commands like GET and SET. So one can just invoke the auth method on your resty.redis instance. Here is an example:

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

    red:set_timeouts(1000, 1000, 1000) -- 1 sec

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

    local res, err = red:auth("foobared")
    if not res then
        ngx.say("failed to authenticate: ", err)
        return
    end

where we assume that the Redis server is configured with the password foobared in the redis.conf file:

requirepass foobared

If the password specified is wrong, then the sample above will output the following to the HTTP client:

failed to authenticate: ERR invalid password

Back to TOC

Redis Transactions

In the new implementation, MULTI and WATCH use exclusive connections. multi() sends MULTI immediately; queued commands return Redis queue acknowledgements or immediate command errors. All transaction keys must belong to one hash slot. Use matching hash tags for different keys, such as order:{42}:state and order:{42}:version. Transactions cannot start inside an ordinary command pipeline.

Execution uses the slot owner. Keyless commands before the first keyed command are retained. If that key selects another node, the client discards the unexecuted queue and restores it on the owner, preserving queue-time errors. A WATCH connection stays on its selected node. WATCH reuses an idle connection when available; UNWATCH, EXEC, and DISCARD permit reuse after Redis confirms the stateful operation has ended. An unfinished transaction cannot enter the idle pool.

The initial WATCH follows MOVED and ASK on an exclusive master connection; later WATCH calls retain it after success. Canceling an unconfirmed WATCH retires the connection even if its reply arrives later. Transaction control through uppercase or mixed-case methods, add_commands(), or send_req() retains the same isolation and lifecycle rules. Raw subscription and MONITOR requests obey the same pipeline and transaction guards.

This library supports the Redis transactions. Here is an example:

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

    red:set_timeouts(1000, 1000, 1000) -- 1 sec

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

    local ok, err = red:multi()
    if not ok then
        ngx.say("failed to run multi: ", err)
        return
    end
    ngx.say("multi ans: ", cjson.encode(ok))

    local ans, err = red:set("a", "abc")
    if not ans then
        ngx.say("failed to run sort: ", err)
        return
    end
    ngx.say("set ans: ", cjson.encode(ans))

    local ans, err = red:lpop("a")
    if not ans then
        ngx.say("failed to run sort: ", err)
        return
    end
    ngx.say("set ans: ", cjson.encode(ans))

    ans, err = red:exec()
    ngx.say("exec ans: ", cjson.encode(ans))

    red:close()

Then the output will be

multi ans: "OK"
set ans: "QUEUED"
set ans: "QUEUED"
exec ans: ["OK",[false,"ERR Operation against a key holding the wrong kind of value"]]

Back to TOC

Redis Module

This library supports the Redis module. Here is an example with RedisBloom module:

    local cjson = require "cjson"
    local redis = require "resty.redis.cluster.fast"
    -- register the module prefix "bf" for RedisBloom
    redis.register_module_prefix("bf")

    local red = redis:new()

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

    -- call BF.ADD command with the prefix 'bf'
    res, err = red:bf():add("dog", 1)
    if not res then
        ngx.say(err)
        return
    end
    ngx.say("receive: ", cjson.encode(res))

    -- call BF.EXISTS command
    res, err = red:bf():exists("dog")
    if not res then
        ngx.say(err)
        return
    end
    ngx.say("receive: ", cjson.encode(res))

Migrating from lua-resty-redis-cluster

Migration requires changing construction, configuration, and connection ownership as well as the import. The private client uses require "resty.redis.cluster.fast", creates an unconnected object with new(), and requires an explicit connect(). It does not provide a resty.redis.cluster alias. Passing the upstream configuration table to new(config) does not apply those settings.

Upstream configurationPrivate client configuration
nameRequired cluster in connect().
serv_listInitial connect(host, port, ...) endpoint and other_nodes = {{host, port}, ...}; serv_list is also accepted for additional seeds.
connect_timeout, send_timeout, read_timeoutCall set_timeouts() before connecting.
keepalive_timeoutmax_idle_timeout in connect().
keepalive_conspool_size for idle retention; choose connection_count separately for ordinary-command connections.
enable_slave_read = falseSet no_slaves = true explicitly; the private client defaults to allowing replica reads for recognized read-only commands.
authpassword, optionally with username.
connect_optsTranslate TLS and pool options individually into top-level connect() options.

Release the logical client with set_keepalive() or close() when finished. Ordinary errors return nil, err, while nested array, pipeline, and EXEC errors remain {false, err}. Check err ~= nil so successful RESP3 boolean false replies are preserved.

Cross-slot multi-key requests can partly succeed. WAIT and WAITAOF acknowledge only their current physical connection. Review these behaviors before treating the new client as interchangeable with an upstream version; matching ordinary command arguments does not imply identical connection, retry, or session behavior.

Back to TOC

Load Balancing and Failover

The new implementation discovers slot ownership and follows MOVED/ASK redirects. Known read-only commands can use replicas unless no_slaves = true. After an ordinary connection failure, it attempts to refresh topology through surviving seeds or known peers. A read or command known not to have been sent can retry once if discovery selects another node. Uncertain writes are returned as errors without replay. Transactions, subscriptions, and other stateful operations are not migrated this way.

Refreshes are shared within a worker and try at most three peers. The refresh budget derives from the configured connect timeout, capped at three seconds; disabling the connect timeout still uses a three-second budget. Candidate connect/send/read timeouts are allocated from the remaining budget. These are inactivity timeouts, so continuing I/O can extend elapsed time. Refreshes are rate limited to one attempt per 100 milliseconds after completion. Peer selection rotates across attempts so dead seeds cannot permanently hide other reachable nodes. Partially executed write pipelines return their original error without replay; refreshed routes apply to later operations.

The following suggestions are retained for applications using the legacy implementation:

You can trivially implement your own Redis load balancing logic yourself in Lua. Just keep a Lua table of all available Redis backend information (like host name and port numbers) and pick one server according to some rule (like round-robin or key-based hashing) from the Lua table at every request. You can keep track of the current rule state in your own Lua module’s data, see https://github.com/openresty/lua-nginx-module/#data-sharing-within-an-nginx-worker

Similarly, you can implement automatic failover logic in Lua at great flexibility.

Back to TOC

Debugging

It is usually convenient to use the lua-cjson library to encode the return values of the redis command methods to JSON. For example,

    local cjson = require "cjson"
    ...
    local res, err = red:mget("h1234", "h5678")
    if res then
        print("res: ", cjson.encode(res))
    end

Back to TOC

Automatic Error Logging

For the new implementation, check returned errors in application code and inspect the Nginx error log for connection diagnostics. lua_socket_log_errors does not control the new module’s own logging. The following cosocket logging advice applies to the legacy implementation.

By default the underlying ngx_lua module does error logging when socket errors happen. If you are already doing proper error handling in your own Lua code, then you are recommended to disable this automatic error logging by turning off ngx_lua’s lua_socket_log_errors directive, that is,

    lua_socket_log_errors off;

Back to TOC

Check List for Issues

  1. Configure pool_size in connect, not set_keepalive(). For the new implementation, choose connection_count separately from idle retention and count physical connections across workers, Nginx instances, nodes, pool identities, and exclusive sessions. pool_size limits retained idle connections; max_pending limits outstanding operations on each physical connection. backlog does not limit concurrency. The legacy implementation’s backlog behavior is described in connect.
  2. Ensure the backlog setting on the Redis side is large enough. For Redis 2.8+, you can directly tune the tcp-backlog parameter in the redis.conf file (and also tune the kernel parameter SOMAXCONN accordingly at least on Linux). You may also want to tune the maxclients parameter in redis.conf.
  3. Use appropriate thresholds in set_timeout or set_timeouts. A timeout does not prove a write was not executed. Retry only when the operation is safe to repeat.
  4. If your NGINX worker processes’ CPU usage is very high under load, then the NGINX event loop might be blocked by the CPU computation too much. Try sampling a C-land on-CPU Flame Graph and Lua-land on-CPU Flame Graph for a typical NGINX worker process. You can optimize the CPU-bound things according to these Flame Graphs.
  5. If your NGINX worker processes’ CPU usage is very low under load, then the NGINX event loop might be blocked by some blocking system calls (like file IO system calls). You can confirm the issue by running the epoll-loop-blocking-distr tool against a typical NGINX worker process. If it is indeed the case, then you can further sample a C-land off-CPU Flame Graph for a NGINX worker process to analyze the actual blockers.
  6. If your redis-server process is running near 100% CPU usage, then you should consider scale your Redis backend by multiple nodes or use the C-land on-CPU Flame Graph tool to analyze the internal bottlenecks within the Redis server process.

Back to TOC

Limitations

  • This library cannot be used in code contexts like init_by_lua*, set_by_lua*, log_by_lua*, and header_filter_by_lua* where the ngx_lua cosocket API is not available.
  • The resty.redis object instance cannot be stored in a Lua variable at the Lua module level, because it will then be shared by all the concurrent requests handled by the same nginx worker process (see https://github.com/openresty/lua-nginx-module/#data-sharing-within-an-nginx-worker ) and result in bad race conditions when concurrent requests are trying to use the same resty.redis instance (you would see the “bad request” or “socket busy” error to be returned from the method calls). You should always initiate resty.redis objects in function local variables or in the ngx.ctx table. These places all have their own data copies for each request.

Back to TOC

Installation

For the new implementation, install lua-resty-redis-cluster-fast-1.31.8 version 1.0.2 with matching OpenResty 1.31.8.x. RPM and DEB packages provide the dynamic module and Lua bytecode modules and use the same configuration. Follow the installation guide for your package manager and load:

load_module /usr/local/openresty/nginx/modules/ngx_redis_cluster_module.so;

Ensure Nginx workers can read the installed bytecode files and any TLS certificate/key files they need.

The following installation instructions are retained for the legacy implementation:

Before using this library, you need to install lua-resty-redis-cluster-fast and openresty-coro-hiredis-nginx-module packages from our OpenResty XRay private repository. And the nginx.conf file should be configured as follows, load the corresponding nginx dynamic modules and hiredis modules.

    # nginx.conf
    load_module "/usr/local/openresty-coro-nginx-module/lib/ngx_http_coro_module.so";
    load_module "/usr/local/openresty-coro-nginx-module/lib/ngx_http_coro_hiredis_module.so";

    http {
        # specify the location of the hiredis
        coro_preload /usr/local/openresty-plus/hiredis/lib/libhiredis.so;
        coro_stack_size  32768;

        # you do not need the following line if you are using
        # the OpenResty bundle:
        lua_package_path "/path/to/lua-resty-redis-cluster-fast/lib/?.lua;;";
        ...
    }

Ensure that the system account running your Nginx ‘‘worker’’ proceses have enough permission to read the .lua file.

Back to TOC

TODO

Back to TOC

Known Issues

  • The legacy implementation does not support SSL connections. The new 1.0.2 implementation supports TLS, including server verification and client certificates; see connect.

Back to TOC

Community

Back to TOC

English Mailing List

The openresty-en mailing list is for English speakers.

Back to TOC

Chinese Mailing List

The openresty mailing list is for Chinese speakers.

Back to TOC

Bugs and Patches

Please report bugs or submit patches by

  1. creating a ticket on the GitHub Issue Tracker,
  2. or posting to the OpenResty community.

Back to TOC

Author

Yichun “agentzh” Zhang (章亦春) agentzh@gmail.com, OpenResty Inc.

Back to TOC

This module is licensed under the BSD license.

Copyright (C) 2012-2017, by Yichun Zhang (agentzh) agentzh@gmail.com, OpenResty Inc.

All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  • Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

  • Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Back to TOC

See Also

Back to TOC