A tale of mathematical misnomers, production outages, and why naming things correctly really does matter.
TL;DR
For 15 years, Rails (and nearly every major Ruby background-job library) shipped a retry strategy called :exponentially_longer that was never exponential — it was a quartic polynomial (executions**4).
This mismatch caused real queue floods and mis-planned capacity.
Rails 7.1 finally fixed the name:
wait: :polynomially_longer # the honest one
It Started With a SolidQueue Mystery
Our Rails 8 app processes retail audio files, forking SoX and LAME, hammering the database, and crashing with SIGSEGV under load. Deadlocks and connection-pool exhaustion were frequent.
We reached for ActiveJob’s retry mechanism and wrote:
class SpmProcessJob < ApplicationJob
retry_on ActiveRecord::Deadlocked, wait: :polynomially_longer, attempts: 5
retry_on PG::ConnectionBad, wait: :polynomially_longer, attempts: 3
end
Wait… polynomially_longer?
Wasn’t that always called exponentially_longer?
That single question unveiled one of the longest-running naming bugs in the Ruby ecosystem.
The Discovery (September 2023)
Victor Mours was investigating a partial production outage in a French social-services appointment system. Background jobs were timing out and retrying so fast they overflowed the queues.
During the post-mortem they discovered the supposed exponential backoff wasn’t exponential at all — it was polynomial. And the mistake was 15 years old.
Exponential vs Polynomial – The Numbers
Rails’ real formula (including jitter + base offset):
delay = executions**4 + (rand * executions**4 * 0.15) + 2
| Retry # | Polynomial (real observed) | True exponential 4ˣ |
|---|---|---|
| 1 | ~3 sec | 4 sec |
| 2 | ~18 sec | 16 sec |
| 3 | ~1.4 min | 1 min |
| 5 | ~10 min | 17 min |
| 7 | ~40 min | 5 hours |
| 10 | ~3 hours | 12 days |
It feels exponential for the first couple of retries… then diverges dramatically.
Where It All Began (2008)
Tobias Lütke’s original delayed_job:
# 2008 – the seed of the bug
time + (attempts ** 4) + 5
A year later the docs called it “exponential.” From there it spread unchanged to Sidekiq, Resque-retry, Sneakers, and finally ActiveJob in Rails 4.2 (2016) under the name :exponentially_longer.
People Noticed – Years Earlier
- Sidekiq #3569 (2017) – “This is quartic, not exponential.” Closed without fix.
- GoodJob users (2023) – Started seeing deprecation warnings after Rails 7.1 and realized the same thing.
Why This Actually Hurt Production
- Queues filled far faster than capacity plans predicted
- Retries happened “too soon” during DB or API outages
- Misleading mental models for incident response
- Secondary failures (connection-pool exhaustion, thundering herds despite jitter)
The Fix – Rails 7.1
# Rails 7.1+
retry_on MyError, wait: :polynomially_longer, attempts: 10
# Old name (still works, but warns)
retry_on MyError, wait: :exponentially_longer
No behavior change — just truth in labeling.
Rafael França (Rails core): “The intent is really to be a polynomial backoff.”
When Polynomial Backoff Is Perfect
Great for:
- ActiveRecord deadlocks
- Connection-pool exhaustion
- Transient network blips
- Rate-limiting responses
Avoid for prolonged third-party outages — use a custom exponential proc instead.
Custom Strategies (when you need something else)
# True exponential
retry_on ApiOutage, wait: ->(n) { (2 ** n).seconds }, attempts: 12
# Fixed interval
retry_on RateLimitError, wait: 30.seconds
# Time-aware (don’t retry at 3 a.m.)
retry_on BatchError, wait: ->(n) {
delay = n**4
delay += 8.hours if Time.current.hour.between?(2, 5)
delay.seconds
}
Takeaways
- Names matter — a bad one misled an entire ecosystem for a decade and a half.
- Polynomial ≠ exponential; the difference is enormous after ~5 retries.
- Jitter is your friend (Rails includes it by default).
- Audit your jobs when upgrading to Rails 7.1/8.0 — you’ll see deprecation warnings.
- If you ever thought Rails or Sidekiq gave you exponential backoff… you never actually had it.
Run this today and smile:
grep -r "exponentially_longer" app/jobs/
You now know exactly what you’re getting — and why it’s called polynomially_longer.
References
- Rails PR #49292 – https://github.com/rails/rails/pull/49292
- Sidekiq issue #3569 – https://github.com/sidekiq/sidekiq/issues/3569
- Original delayed_job history – https://github.com/collectiveidea/delayed_job/commits/master
- ActiveJob docs – https://edgeapi.rubyonrails.org/classes/ActiveJob/Exceptions/ClassMethods.html
Top comments (0)