Celery vs RQ vs Dramatiq for Redaction Queues

Selecting a task queue for asynchronous redaction is a trade-off between feature breadth and operational cost, and the right answer depends on whether you need priority routing, multiple brokers, and how much delivery-guarantee plumbing you are willing to own. This guide compares Celery, RQ, and Dramatiq for exactly that workload.

Redaction jobs are long-running, PII-bearing, and audited, which raises the bar over a generic task queue. Delivery must be at-least-once, tasks must be idempotent so a redelivery never double-writes an audit record, and long OCR steps must not trip a broker’s visibility timeout. This companion to the Parsing Engine Selection Benchmarks framework covers the queue row of that matrix.

Prerequisites and version matrix Permalink to this section

Component Version Notes
Python 3.11 CPython
celery 5.3.x broadest broker support
rq 1.16.x Redis-only, minimal
dramatiq 1.17.x middleware-driven defaults
redis 7.x broker/result backend
Broker (Celery only) RabbitMQ 3.13 or Redis RabbitMQ for full routing

RQ and Dramatiq run on Redis alone. Celery adds RabbitMQ support and a richer result backend, at the cost of more moving parts to operate.

Trade-off comparison Permalink to this section

Capability Celery RQ Dramatiq
Broker support RabbitMQ, Redis, SQS Redis only Redis, RabbitMQ
Automatic retries yes (configurable) manual yes (default, with backoff)
Priority queues yes limited yes
Dead-letter handling via routing config not built-in built-in (dead queue)
Operational burden high low moderate
Best when complex routing, many brokers simple Redis-only jobs sane defaults, low ops

Step-by-step implementation Permalink to this section

1. Define the same redaction task in each framework. The body is identical — hash the artifact, redact, append to the audit log — only the decorator and retry wiring differ.

# Celery: explicit acks_late so a crashed worker redelivers the message.
from celery import Celery

app = Celery("redaction", broker="redis://localhost:6379/0")

@app.task(bind=True, acks_late=True, max_retries=3, default_retry_delay=30)
def redact_document_celery(self, doc_id: str, artifact_sha256: str):
    try:
        _redact(doc_id, artifact_sha256)   # idempotent on artifact_sha256
    except TransientError as exc:
        raise self.retry(exc=exc)
# RQ: retries are opt-in via the Retry helper at enqueue time.
from rq import Queue, Retry
from redis import Redis

queue = Queue("redaction", connection=Redis())

def redact_document_rq(doc_id: str, artifact_sha256: str):
    _redact(doc_id, artifact_sha256)

# queue.enqueue(redact_document_rq, doc_id, sha, retry=Retry(max=3, interval=30))
# Dramatiq: retries and time limits are middleware defaults, set per-actor.
import dramatiq
from dramatiq.brokers.redis import RedisBroker

dramatiq.set_broker(RedisBroker(url="redis://localhost:6379/0"))

@dramatiq.actor(max_retries=3, min_backoff=30_000, time_limit=1_800_000)
def redact_document_dramatiq(doc_id: str, artifact_sha256: str):
    _redact(doc_id, artifact_sha256)       # same idempotent body

2. Make the task idempotent. Key the redaction on the artifact’s SHA-256 so a redelivered message is a no-op rather than a duplicate audit entry.

def _redact(doc_id: str, artifact_sha256: str) -> None:
    if audit_store.exists(artifact_sha256):
        return                              # already processed; safe redelivery
    result = run_redaction(doc_id)          # produces redacted artifact
    audit_store.append(artifact_sha256, result.output_sha256)

Compliance checkpoint Permalink to this section

At-least-once delivery plus idempotency is what keeps the audit trail complete. Under NIST SP 800-53 Rev. 5 control AU-9 (Protection of Audit Information), audit records must be protected against loss and unauthorized modification; a task that is dropped on worker failure leaves a gap, while one that runs twice risks a conflicting record. Choosing at-least-once delivery (Celery’s acks_late, Dramatiq’s default redelivery) and enforcing idempotency on the artifact hash satisfies both halves — no lost record, no duplicate. Persist each task’s message id and artifact hash so redeliveries are auditable, consistent with the immutable logging described across the PDF and DOCX Parsing & Extraction Workflows guide.

Gotchas Permalink to this section

  • Visibility timeout versus long OCR jobs. Redis-backed brokers redeliver a message if it is not acknowledged within the visibility timeout; a 25-minute OCR job under a default 30-minute timeout is one slow page from being processed twice. Raise the timeout above your p99 job duration.
  • Duplicate delivery needs idempotent tasks. At-least-once means occasional redelivery by design. Without the hash guard above, a redraw appends a second audit record and can double-mask a document.
  • Dead-letter support differs. Dramatiq ships a built-in dead queue for exhausted retries; RQ has no native DLQ, so failed jobs need an explicit failed-job registry sweep; Celery needs routing configuration to emulate one. Wire escalation accordingly.
  • Result backend growth. Celery’s result backend accumulates state; set result_expires or it becomes an unbounded store of task metadata.

Verification and testing Permalink to this section

Assert that a failing task is retried and that a redelivery stays idempotent. Dramatiq’s stub broker makes retries deterministic in-process.

import dramatiq
from dramatiq.brokers.stub import StubBroker

def test_task_is_idempotent_and_retried(monkeypatch):
    broker = StubBroker()
    dramatiq.set_broker(broker)
    calls = {"n": 0}

    def fake_redact(doc_id, sha):
        calls["n"] += 1
        if calls["n"] == 1:
            raise TransientError("first attempt fails")
    monkeypatch.setattr("mymod._redact", fake_redact)

    redact_document_dramatiq.send("doc-1", "abc123")
    broker.join(redact_document_dramatiq.queue_name)
    # Retried once (2 invocations) but audit written exactly once.
    assert calls["n"] == 2
    assert audit_store.count("abc123") == 1

For CPU-bound OCR workers Dramatiq’s defaults usually win on ops burden; reach for Celery only when priority routing or a non-Redis broker is a hard requirement, and for a small Redis-only footprint RQ is the least to operate.

Size these queues against the throughput figures in Parsing Engine Selection Benchmarks and the fleet design in Async Batch Processing Pipelines. The sibling Tesseract vs EasyOCR for Scanned Legal Documents covers the long OCR jobs these workers run. Part of the PDF and DOCX Parsing & Extraction Workflows field guide.