Library / SDK
leandromoreira/redlock-rb avatar
leandromoreira/redlock-rb

redlock-rb: a Redis distributed lock with a validity time, not a boolean

Redlock is a redis-based distributed lock implementation in Ruby. More than 40 Millions of downloads.

730 stars84 forksRubyBSD-2-Clause

At a glance

What is it?
redlock-rb implements the Redlock distributed lock algorithm on top of Redis for Ruby, and its distinguishing feature is that a lock is a value object carrying a validity time rather than a true or false, so callers can reason about how long the lock still protects them. It requires Redis 6.0 or later, and version 2.0 and above work only with the redis-client gem.
Who is it for?
redlock-rb is worth considering if you need mutual exclusion across processes and already run Redis, because the lock value object with its validity time and the separate TTL query methods are what stop a caller from treating a lock as a boolean it can trust, and the retry configuration with jitter is the kind of detail that matters under contention.
Can I use it commercially?
Yes. BSD-2-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Activity is slowing. The repository last received commits 6 months ago.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A lock is a value with a validity time, not a boolean

redlock-rb describes itself as a distributed lock using Redis, and it is an implementation of a proposed distributed lock algorithm with Redis, started as a fork of antirez's implementation.

The distinguishing design choice is visible in the return value. Locking a resource does not give you true or false, it gives you a hash carrying a validity, a resource name and a generated uuid value:

ruby
  # Locking
  lock_manager = Redlock::Client.new([ "redis://127.0.0.1:7777", "redis://127.0.0.1:7778", "redis://127.0.0.1:7779" ])
  first_try_lock_info = lock_manager.lock("resource_key", 2000)
  second_try_lock_info = lock_manager.lock("resource_key", 2000)

  p first_try_lock_info
  # => {validity: 1987, resource: "resource_key", value: "generated_uuid4"}

  p second_try_lock_info
  # => false

The validity figure is the number of milliseconds for which the lock is still considered held. It is lower than the 2000 you asked for, because the elapsed time between the call and the answer is already subtracted. That single number is why the library refuses to be boolean: a lock with 20 milliseconds of validity left is not the same thing as a lock with two seconds left, and code that treats both as true will overrun its critical section.

A second attempt on the same resource returns false, which is the ordinary contention case. The lock is taken by one caller and refused for the other.

All expiration durations in this library are in milliseconds, which the README states as a note before the example, and that convention is worth internalising before you read any of the following code.

Three ways to take a lock, and the one that raises

There are three call shapes for acquiring a lock, and they differ in what happens when the lock is already held.

The plain form returns the lock info or false, and you are responsible for unlocking it. The block version takes a block and unlocks automatically:

ruby
lock_manager.lock("resource_key", 2000) do |locked|
  if locked
    # critical code
  else
    # error handling
  end
end

Notice that even the block form does not raise on contention, it just hands you false inside the block and lets you branch. That is a deliberate design for callers who want to wait, retry or degrade rather than crash.

The bang form is the exception-raising variant. It only executes the block if the lock was acquired, returns the block's value as the result, and raises otherwise. Passing a block is mandatory:

ruby
begin
  block_result = lock_manager.lock!("resource_key", 2000) do
    # critical code
  end
rescue Redlock::LockError
  # error handling
end

The bang form is the one to use when proceeding without the lock is a bug rather than a branch. The error class is Redlock::LockError, and rescuing it is the whole error handling story, which keeps call sites short.

The same pattern appears in the extension code further down, where the block is wrapped in a begin and rescue Redlock::LockError, so the exception class is the single integration point for failure handling across the library.

Extending a lock is where the algorithm gets subtle

The extension mechanism has a trap in it, and the README is direct about the trap, which is the most useful thing the documentation does here.

Extending works by locking again with the extend parameter set to the lock you already hold:

ruby
begin
  lock_info = lock_manager.lock("resource_key", 2000)
  while lock_info
    # Critical code

    # Time up and more work to do? Extend the lock.
    lock_info = lock_manager.lock("resource key", 3000, extend: lock_info)
  end
rescue Redlock::LockError
  # error handling
end

The behaviour to be careful about: the above code will also acquire the lock if the previous lock has expired and the lock is currently free. That means the lock could have been acquired and released by someone else in the meantime, and your critical section is now running without any protection, without an exception and without a warning.

To only extend the life of the lock if it is currently locked by yourself, there is the extend_only_if_locked parameter:

ruby
lock_manager.lock("resource key", 3000, extend: lock_info, extend_only_if_locked: true)

With that flag the extension fails rather than silently taking a lock someone else has released. If your critical section can run longer than the initial TTL, this flag is the difference between correct mutual exclusion and a race that only shows up under load.

One more subtlety in the sample: the extension call uses the resource key "resource key" with a space, while the original lock used "resource_key" with an underscore. That difference in the sample is worth noticing, because extending a lock on a different resource name is not extending the same lock, and the sample as written is illustrating the loop shape rather than a resource name you should copy.

Two ways to ask about a lock, and the one that is not safe for timing

There is a family of query methods, and the README draws a clear line between the ones that are convenient and the one that is correct for timing critical code.

The convenience methods are locked? and valid_lock?. The first asks whether a resource is currently locked by anyone. The second asks whether the specific lock you hold is still valid:

ruby
lock_info = lock_manager.lock("resource_key", 2000)
lock_manager.valid_lock?(lock_info)
#=> true

lock_manager.unlock(lock_info)
lock_manager.valid_lock?(lock_info)
#=> false

The README states plainly that these methods are not safe if you are using them to time critical code, because they return true if the lock has not expired even if there is only, for example, 1ms left on it. That is a race in the caller's code: a true answer followed by a scheduling delay can mean the critical section starts after the lock has lapsed.

The safe methods are get_remaining_ttl_for_lock and get_remaining_ttl_for_resource. The first gives the remaining TTL for the specific lock you hold:

ruby
resource = "resource_key"
lock_info = lock_manager.lock(resource, 2000)
sleep 1

lock_manager.get_remaining_ttl_for_lock(lock_info)
#=> 986

lock_manager.unlock(lock_info)
lock_manager.get_remaining_ttl_for_lock(lock_info)
#=> nil

The second gives the remaining TTL on a resource without requiring you to hold the lock, which is the version you use from a different part of the code that only cares whether the resource is contended. It returns nil once the lock has gone, exactly like the lock-specific variant after unlocking.

The difference between the two families is the difference between asking whether a lock exists and asking how long it will keep existing, and the second question is the one your critical section actually needs answered.

Sentinels, client objects and the retry knobs

The client accepts URLs, configuration hashes or Redis objects, and the README explains why you would pass a Redis object: to configure the connection in more detail, for example setting a username and a password.

ruby
servers = [ 'redis://localhost:6379', RedisClient.new(:url => 'redis://someotherhost:6379') ]
redlock = Redlock::Client.new(servers)

Sentinel setups are supported by passing a RedisClient instance or just a configuration hash as part of the servers array during initialization. The configuration hash names the master, lists the sentinels with their host and port, and sets a role of :master, so a sentinel deployment is described in the same shape a Redis client configuration would use.

The README notes that Redlock supports the same configuration hash as RedisClient, so the sentinel configuration is not a Redlock-specific dialect and a Redis client configuration transfers directly.

The retry logic is configurable on the client, and the defaults are visible in the documented options:

ruby
  lock_manager = Redlock::Client.new(
                  servers, {
                  retry_count:   3,
                  retry_delay:   200, # milliseconds
                  retry_jitter:  50,  # milliseconds
                  redis_timeout: 0.1  # seconds
   

The jitter is the interesting option. Without a random component, two clients that fail at the same moment retry at the same moment, and a retry_count of 3 with a 200 millisecond delay is exactly the kind of synchronised retry that turns a brief Redis hiccup into a thundering herd. A 50 millisecond jitter spread breaks that up, and the separate redis_timeout of 0.1 seconds keeps a single unresponsive node from eating the whole retry budget.

On compatibility, the README states two constraints plainly: it works with Redis server versions 6.0 or later, and Redlock 2.0 and above only works with a RedisClient instance. The second point matters for anyone upgrading, since it means the older redis gem is no longer an option.

Testing runs against three Redis instances in Docker

The repository ships a Docker Compose file for the test suite, and it is worth reading because it explains the shape of the environment the library expects.

The test service runs the plain ruby image with the repository mounted at /redlock, and its command installs the bundle and runs rspec. Three Redis services, redis1, redis2 and redis3, are linked to it under the hostnames redis1.local.com, redis2.local.com and redis3.local.com, and the corresponding host and port values are passed as environment variables. There is also a DEFAULT_REDIS_HOST and DEFAULT_REDIS_PORT pair for the single-instance case.

Three instances rather than one is not incidental. The Redlock algorithm takes a lock across multiple independent Redis nodes so that a single node failing does not release the lock, and a test suite that only ever ran against one Redis would not exercise the quorum behaviour at all. The library's own compatibility note that it needs Redis 6.0 or later is a separate constraint, about the server features the implementation relies on.

The Makefile wraps the Compose commands in three targets. The default target runs the test service, the build target builds the gem inside the container, and the publish target pushes the most recently built gem file, selecting it with an ls piped through awk. The Rakefile and the .rspec file sit alongside it, and the gemspec is redlock.gemspec with the library code in lib/.

For the maintenance picture, the repository is not archived and the last push was on 2026-04-14. The release list shows 2.1.0 on 2026-01-27, described as support for redis-client and redis transparently, 2.0.6 in October 2023 described as bubbling up errors, and 2.0.5 the same day described as stopping the monkey patching of Rails 7. The licence is BSD-2-Clause, with the terms in the LICENSE file, and the project carries a CHANGELOG, a CONTRIBUTORS file and a Makefile-driven release path, so the change history is documented even though the release cadence is irregular.

Editorial conclusion

redlock-rb is worth considering if you need mutual exclusion across processes and already run Redis, because the lock value object with its validity time and the separate TTL query methods are what stop a caller from treating a lock as a boolean it can trust, and the retry configuration with jitter is the kind of detail that matters under contention. It is the wrong tool if you need strict consistency, since a distributed lock over independent Redis instances is a best-effort primitive rather than a consensus system, and the library's own upstream reference is a proposed algorithm rather than a guarantee. Before you adopt it, verify three things yourself: that your Redis deployment is 6.0 or later with a non-clustered or otherwise suitable topology, that your Redis client is redis-client rather than the older redis gem, since version 2.0 dropped support for it, and that the lock TTL you choose is longer than your worst case critical section, because the validity time counts down from the moment of acquisition and the code cannot run past it.

Frequently asked questions

What is redlock-rb and what does it implement?

It is a Ruby distributed lock built on Redis, implementing a proposed distributed lock algorithm with Redis. The project started as a fork of antirez's implementation and works with Redis server versions 6.0 or later.

Why does redlock-rb return a lock value object instead of a boolean?

The returned hash carries a validity in milliseconds, the resource name and a generated uuid value, so a caller can see how much time the lock still protects it. The README warns that the convenience query methods return true even when only 1ms of the lock remains, and points to the remaining TTL methods for timing critical code.

How do I extend a lock in redlock-rb safely?

Call lock again with the extend parameter set to the lock you already hold. Note that the plain form will also acquire the lock if your previous lock expired and the resource is now free, so pass extend_only_if_locked: true if you only want to extend a lock that is still yours.

Which Redis client does redlock-rb require?

Redlock 2.0 and above works only with a RedisClient instance from the redis-client gem, so the older redis gem is no longer supported. The client accepts URLs, configuration hashes or Redis objects, and supports sentinels by passing a RedisClient instance or a configuration hash in the servers array.

How do I install and use redlock-rb?

Add gem 'redlock' to your Gemfile and run bundle, or install it with gem install redlock. Create a Redlock::Client with your server list, then call lock with a resource name and an expiration in milliseconds, and unlock with the returned lock info, or use the block and bang variants.

Official sources

  1. Issues
  2. leandromoreira/redlock-rb on GitHub
  3. License: BSD-2-Clause
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/leandromoreira-redlock-rb.svg)](https://hysenlabs.com/projects/leandromoreira-redlock-rb)

Was this useful?