Skip to content

Proposal: deduplicate pending jobs with coalesce #834

Open

Description

@dtcristo

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:

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 (:created or :discarded) and provider_job_id tell the caller what happened.
  • Semaphores from limits_concurrency are 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_concurrency to prevent that.

Questions

  1. Would you want pending-job coalescing in Solid Queue?
  2. Should it be its own coalesce key: declaration, or an option on limits_concurrency?
  3. 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

  1. changed the title [-]Coalescing - Keep one pending job per key[/-] [+]Coalesce - Pending job deduplication[/+] on Nov 7, 2026
  2. changed the title [-]Coalesce - Pending job deduplication[/-] [+]Deduplicate pending jobs with coalesce[/+] on Nov 7, 2026
  3. changed the title [-]Deduplicate pending jobs with coalesce[/-] [+]Proposal: deduplicate pending jobs with coalesce[/+] on Nov 7, 2026
  4. dtcristo commented on Nov 7, 2026

    @dtcristo
    Author

    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:

    • Coalescing at enqueue time
    • Concurrency at enqueue or dispatch time
    • Rate limiting at claim time

    It's still experimental and not fully optimised, but thought you might find it interesting!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions