Repository navigation
Description
Hi Rosa and team,
David (human) here. We've used Solid Queue in production for nearly a year and love it.
We found that limits_concurrency on_conflict: :discard doesn't deduplicate scheduled jobs. After causing us a production incident, we also discovered that it can discard important work on a retry. I feel that limits_concurrency is not the right tool for deduplication.
Below is my AI-authored issue that describes the problem and my proposed solution, introducing the concept I'm calling coalesce.
I've written a gem solid_queue-coalesce (that composes well with limits_concurrency) exploring these ideas further, including replacing arguments, collecting calls into batches, debouncing and throttling. For now, I'm only proposing basic deduplication, but check out the gem if you're curious about the rest.
Motivation
Issue #105 described one pending job per key, with another allowed as soon as the first starts. limits_concurrency on_conflict: :discard (#176, #594) controls execution, not pending jobs:
- Scheduled jobs can accumulate, and more than one can run (Concurrency controls on_conflict: :discard still enqueues and runs multiple jobs #597).
- Calls made during execution can be dropped, even if the running job has already read the state being updated.
- Callers can't identify which existing job covers their request.
For example, repeated product changes should share one pending reindex. A change during that reindex should schedule the next one.
on_conflict: :discard remains useful for shedding load; coalescing would complement it.
Proposal
class ReindexProductJob < ApplicationJob
coalesce key: ->(product_id) { product_id }
def perform(product_id)
Product.find(product_id).reindex!
end
end- Calls with the same key, including scheduled ones, share one pending job.
- The key is released when that job starts, so a call during execution queues the next one.
- Keys are scoped to the job class and batch; retries never coalesce.
coalesce_status(:createdor:discarded) andprovider_job_idtell the caller what happened.- Semaphores from
limits_concurrencyare untouched.
The first job's arguments and scheduled time are retained. Collecting calls, replacing arguments and debouncing are outside this proposal.
Implementation
The standalone gem works but patches private Solid Queue methods, making upgrades fragile. Native support could avoid that. #835 illustrates a minimal implementation using a solid_queue_coalesce_keys table and row locks; it's intended to support discussion, not represent a complete solution. The gem also has benchmarks.
Trade-offs
- A new table requires a migration.
- Row locking introduces contention and potential deadlocks; Add coalesce to deduplicate pending jobs #835 also enqueues coalesced bulk jobs individually, allowing partial batches on failure.
- Two jobs with the same key can run at once. Add
limits_concurrencyto prevent that.
Questions
- Would you want pending-job coalescing in Solid Queue?
- Should it be its own
coalesce key:declaration, or an option onlimits_concurrency? - If it stays a gem, would you take small public extension points so it doesn't have to patch private methods?
🤖 Written by @dtcristo's agent (Claude Opus 5.5)
Activity
[-]Coalescing - Keep one pending job per key[/-][+]Coalesce - Pending job deduplication[/+][-]Coalesce - Pending job deduplication[/-][+]Deduplicate pending jobs with coalesce[/+][-]Deduplicate pending jobs with coalesce[/-][+]Proposal: deduplicate pending jobs with coalesce[/+]dtcristo commentedon Nov 7, 2026
Not to distract from this issue, but I also wanted to share another gem I've been working on: solid_queue-rate_limit.
Unlike
limits_concurrency, which limits how many jobs run simultaneously, rate limiting controls how frequently jobs can start.I think all three complement each other and compose nicely at different stages of a job's lifecycle:
It's still experimental and not fully optimised, but thought you might find it interesting!