# Retriable, a small Ruby DSL for retrying blocks with exponential backoff

> Retriable wraps a block of Ruby in a retry policy with randomized exponential backoff, three tries by default and a fifteen minute ceiling. Version 5.0 froze the global configuration object, which breaks code that writes to it directly, and the gem still supports very old Ruby through separate branches.

**kamui/retriable** — Retriable is an simple DSL to retry failed code blocks with randomized exponential backoff.

- Repository: https://github.com/kamui/retriable
- Website: kamui.github.io/retriable
- Stars: 909 · Forks: 47
- Language: Ruby
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/kamui-retriable

## A block wrapper rather than a client library

Retriable is described as a simple DSL to retry failed code blocks with randomized exponential backoff, and the README says it is especially useful when interacting with external APIs, remote services or filesystem calls.

The API is one method and a block:

```ruby
require 'retriable'

class Api
  # Use it in methods that interact with unreliable services
  def get
    Retriable.retriable do
      # code here...
    end
  end
end
```

That is the entire idea. Whatever raises inside the block is caught and the block is run again, so the same policy applies to an HTTP call, an S3 read and a file write without any of them knowing about it.

The value is consistency. Without a helper like this, every call site ends up with its own sleep-and-retry loop, its own number of attempts and its own idea of what should be retried, and the inconsistencies are invisible until production. The gem is MIT licensed, published as the retriable gem, and the version series has moved to 5.0, with v5.0.0 tagged on 2026-08-25 and v5.0.1 on 2026-08-28. The last push to the repository was on 2026-10-18.

The repository carries a spec/ directory, a Rakefile, a .rspec file, rubocop configuration, a sig/ directory for type signatures, a benchmark/ directory, a bin/ directory, and a CHANGELOG.md, which is the shape of a small library that takes its own testing seriously.

## Three tries, StandardError, and no surprises

The defaults are stated in three lines, and they are worth reading carefully because every one of them is a decision.

Retriable rescues any exception inherited from StandardError. It makes 3 tries, including the initial attempt, before raising the last exception. And it uses randomized exponential backoff to calculate each succeeding try interval.

Rescuing StandardError and its descendants rather than Exception is the sensible default, since it leaves interrupts and signals alone. Three tries including the initial attempt is a modest default that covers a single transient blip without turning a persistent failure into a long stall, and re-raising the last exception means the caller still sees the original error rather than a wrapper.

The randomized element is the part that differs from a textbook backoff. Instead of a fixed interval, each interval is drawn from a range, which spreads the retries of many clients that failed at the same moment instead of having them all come back at once.

The README gives the default interval table for ten tries, in seconds, with a minimum, an average and a maximum for each attempt. The first retry has a minimum of 0.25, an average of 0.5 and a maximum of 0.75, and each subsequent row grows by the same factor, up to the ninth retry at a minimum of 6.407 and a maximum of 19.222. The tenth row is marked stop, which corresponds to the default of three tries ending well before that.

## The options that decide the policy

The option table is where the library earns its keep, and each option has a default that reflects a real opinion.

tries defaults to 3, and passing Float::INFINITY keeps retrying until success or until max_elapsed_time is reached. on defaults to [StandardError] and is the type of exception to retry. retry_if defaults to nil and is a callable, a Proc or a lambda, that receives the rescued exception and returns true or false to decide whether to retry. base_interval defaults to 0.5 seconds, multiplier defaults to 1.5 so each successive interval grows by that factor, and max_interval defaults to 60 and caps the interval before rand_factor is applied, with a randomized interval able to reach max_interval multiplied by one plus rand_factor.

The two that shape behaviour most are max_elapsed_time, which defaults to 900 seconds, fifteen minutes, and which can be set to nil to disable the time limit and rely on tries alone, and sleep_disabled, which defaults to false and, when true, turns off the backoff entirely and retries immediately.

That last option is not a testing convenience only. It exists for the case where the delay is the problem rather than the failure, for instance when a local resource is briefly locked, and setting it in a global configuration would be a mistake, so it is one of the options you override per call.

Beyond the table, the README documents a custom interval array for a hand-written schedule, and an opt-in mode for unbounded retries.

## Choosing what gets retried, and what does not

Two options decide whether a rescued exception should be retried at all, and they cover different needs.

The on option takes a list of exception types, so you can narrow the default [StandardError] to the errors that are actually transient, a Timeout::Error and a Net::ReadTimeout for example, and let everything else propagate on the first failure.

The retry_if option is the finer-grained one. It receives the rescued exception and returns a boolean, so you can decide based on the content of the error rather than its class, distinguishing a rate limit from a permission error even when both arrive as the same exception type.

The distinction matters because retrying the wrong exception is the most common way a retry policy turns into an outage amplifier. A 403 that will never succeed, retried three times with backoff, is three times the latency for the same result. A connection reset that would have succeeded immediately is now delayed by a second. The on option handles the coarse case and the retry_if callable handles the case where the class is not enough to tell.

There is a third consideration that neither option covers, which is that not every failure is safe to repeat. Retriable will happily run a block that charges a card twice. The library cannot know that, so the decision about which blocks are wrapped remains the caller's.

## Version 5.0 froze the global configuration

Retriable 5.0 changes one thing for existing users, and it is the kind of change that shows up as a crash in a test suite rather than in application code. Retriable.config is now a frozen snapshot, so direct writes raise FrozenError.

```ruby
Retriable.config.sleep_disabled = true              # => FrozenError
Retriable.configure { |c| c.sleep_disabled = true } # do this instead
```

The configuration has to go through the configure block, which is the form that was always the documented way to set global options. What the freeze does is stop the config object from being mutated behind the library's back after it has been read, so that a policy is the same for everyone who reads it.

The README points out where this bites: test setup files such as spec_helper and rails_helper are the likeliest place. That is a fair prediction, because a spec helper is exactly where a global tweak like disabling the sleep is most tempting, and exactly where it is most likely to have been written against the old mutable object.

There are full 4.x to 5.x and 3.x to 4.0 guides in docs/migration.md, and the options for disabling a configured callback per call, for contexts, for ensure and else handling, for the kernel extension and for testing are all documented in the README's own table of contents.

## Ruby 3.2 and a set of branches for everything older

The requirements section is unusual in a good way. The current gem needs Ruby 3.2 or newer, and the README then lists what to do for older interpreters, with a branch for each range.

Ruby 2.3.0 to 3.1.x is served by the 3.8.x branch, selected by specifying ~> 3.8 in the Gemfile. Ruby 2.0.0 to 2.2.x is served by the 3.1 branch with ~3.1. Ruby 1.9.3 is served by the 2.x branch with ~2.1. And Ruby 1.8.x to 1.9.2 is served by the 1.x branch with ~1.4.

That is four maintained lines for a library whose entire feature set is a retry loop, and it is a reasonable thing for a gem that has been in use since the 1.x series to keep the old interpreters working. It is also a maintenance commitment, and it is the reason the README's upgrading section had to be explicit about the one breaking change in 5.0.

For a new project the answer is simple: Ruby 3.2 or newer and the ~> 5.0 line. For an existing project on an older interpreter, the branch table tells you exactly which version constraint to write, which is more than most gems bother to document.

Installation is the usual three routes: gem install retriable from the command line, require 'retriable' in the script, or gem 'retriable', '~> 5.0' in the Gemfile.

## Conclusion

Retriable suits a Ruby codebase that calls flaky services and filesystem APIs from many places and wants one consistent retry policy with per-call overrides, callbacks and a hard time limit. It does not suit code where a retry could repeat a non-idempotent side effect, and it does not replace a client library's own retry handling. Before you upgrade, check for direct writes to Retriable.config, since version 5.0 turns those into a FrozenError, and note that the gem's floor is now Ruby 3.2, with older Ruby served by the 3.8.x, 3.1.x, 2.x and 1.x branches.

## FAQ

### What does Retriable do by default?

It rescues any exception inherited from StandardError, makes 3 tries including the initial attempt before raising the last exception, and uses randomized exponential backoff for each succeeding try interval.

### How do I install Retriable and which Ruby versions are supported?

Install it with gem install retriable, require 'retriable' in your script, or add gem 'retriable', '~> 5.0' to your Gemfile. The current gem requires Ruby 3.2 or newer, with older interpreters served by the 3.8.x, 3.1.x, 2.x and 1.x branches.

### How do I retry only specific exceptions with Retriable?

Use the on option to narrow the retried exception types, or the retry_if option with a callable such as a Proc or lambda that receives the rescued exception and returns true or false to decide whether to retry that particular error.

### What changed in Retriable 5.0?

Retriable.config is now a frozen snapshot, so writing to it directly raises FrozenError. Global settings have to go through Retriable.configure { |c| ... } instead, and the README warns that spec_helper and rails_helper files are the likeliest place this breaks.

### How long will Retriable keep retrying?

The max_elapsed_time option defaults to 900 seconds, fifteen minutes, and can be set to nil to disable the time limit and retry based on tries alone. The tries option defaults to 3 and can be set to Float::INFINITY to keep retrying until success or until that time limit is reached.

## Sources

- [Issues](https://github.com/kamui/retriable/issues)
- [kamui/retriable on GitHub](https://github.com/kamui/retriable)
- [License: MIT](https://github.com/kamui/retriable/blob/main/LICENSE)
- [README](https://github.com/kamui/retriable/blob/main/README.md)
- [Releases](https://github.com/kamui/retriable/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/kamui-retriable
