Introduction
Without uniqueness controls, the same job can run multiple times concurrently or be enqueued repeatedly. Solid Queue provides built-in concurrency controls, and you can implement custom deduplication for any backend.
Key Concepts
- Unique Job: A job that prevents duplicate concurrent execution.
- limits_concurrency: Solid Queue's built-in uniqueness control.
- Lock Pattern: Using Redis or database locks to prevent duplicate execution.
Real World Context
A user clicks 'Send Email' five times rapidly. Without uniqueness, they receive five emails.
Deep Dive
Solid Queue Concurrency Controls
rubyclass ProcessOrderJob < ApplicationJob limits_concurrency to: 1, key: ->(order_id) { order_id } def perform(order_id) # Only one instance runs per order_id at a time end end
DIY Unique Jobs with Redis
rubyclass UniqueJob < ApplicationJob before_perform :acquire_lock after_perform :release_lock discard_on DuplicateJobError def unique_key [self.class.name, arguments].join(':') end private def acquire_lock acquired = Redis.current.set("job_lock:#{unique_key}", job_id, nx: true, ex: 1.hour.to_i) raise DuplicateJobError unless acquired end def release_lock Redis.current.del("job_lock:#{unique_key}") if Redis.current.get("job_lock:#{unique_key}") == job_id end end
Common Pitfalls
- Not releasing locks on failure — Use
after_performandensureblocks. - Lock timeout too short — If the job takes longer than the lock TTL, another instance starts.
Best Practices
- Use Solid Queue's limits_concurrency when possible — Built-in, no Redis needed.
- Set lock TTL longer than the job's expected duration.
Summary
- Solid Queue provides
limits_concurrencyfor built-in uniqueness. - For other backends, use Redis-based locking patterns.
- Always set appropriate lock timeouts.
- Use
after_performto release locks.
Code Examples
ruby
class ProcessOrderJob < ApplicationJob
limits_concurrency to: 1, key: ->(order_id) { order_id }
def perform(order_id)
order = Order.find(order_id)
OrderProcessor.process(order)
end
end