Celery 5 on Redis with fakeredis-backed idempotency, tested offline in eager mode: working versions (tested 2026-10-15)
Configure a Celery app on a Redis broker and result backend for production, guard a task with a Redis SET NX idempotency lock, and test it offline with task_always_eager and fakeredis.
passed4 passed · 0 failed2026-10-15 11:52 UTC- Status
- passed
- Last passed
- 2026-10-15 11:52 UTC
- Tests
- 4 / 4
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38730348195
The code
Exactly the files that passed. Your agent can pull them with get_recipe("celery-redis-fakeredis-results") (install), or see them on GitHub.
celery==5.6.3 redis==8.1.0 fakeredis==2.40.0 pytest==9.1.1
import os
from celery import Celery
REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379/0")
app = Celery("app", broker=REDIS_URL, backend=REDIS_URL)
app.conf.update(
task_acks_late=True, # redeliver if a worker dies mid-task
worker_prefetch_multiplier=1, # don't hoard long tasks on one worker
result_expires=3600,
)
import os
import redis
class IdempotencyLock:
def __init__(self, client: redis.Redis, ttl: int = 3600, prefix: str = "idem:"):
self.client = client
self.ttl = ttl
self.prefix = prefix
def claim(self, key: str) -> bool:
# SET NX returns True on success and None when the key already exists
return bool(self.client.set(self.prefix + key, "1", nx=True, ex=self.ttl))
def release(self, key: str) -> None:
self.client.delete(self.prefix + key)
_default: IdempotencyLock | None = None
def get_lock() -> IdempotencyLock:
global _default
if _default is None:
url = os.environ.get("REDIS_URL", "redis://localhost:6379/1")
_default = IdempotencyLock(redis.Redis.from_url(url))
return _default
from celery_app import app
from dedupe import get_lock
def charge_gateway(order_id: str, cents: int) -> None:
raise NotImplementedError("wire your payment provider here")
@app.task
def record_payment(order_id: str, cents: int) -> str:
lock = get_lock()
key = f"payment:{order_id}"
if not lock.claim(key):
return "duplicate"
try:
charge_gateway(order_id, cents)
except Exception:
lock.release(key) # let a retry claim the key again
raise
return "charged"
import fakeredis, pytest
import tasks
from celery_app import app
from dedupe import IdempotencyLock
@pytest.fixture(autouse=True)
def eager():
app.conf.update(task_always_eager=True, task_eager_propagates=True)
yield
app.conf.update(task_always_eager=False)
@pytest.fixture
def fake_redis(monkeypatch):
fake = fakeredis.FakeRedis()
fake.flushall() # fakeredis instances may share data, so start clean
monkeypatch.setattr(tasks, "get_lock", lambda: IdempotencyLock(fake))
return fake
@pytest.fixture(autouse=True)
def gateway(monkeypatch):
calls = []
monkeypatch.setattr(tasks, "charge_gateway", lambda order_id, cents: calls.append(order_id))
return calls
def test_runs_inline_in_eager_mode(fake_redis, gateway):
assert tasks.record_payment.delay("o1", 500).get() == "charged"
assert gateway == ["o1"]
def test_duplicate_is_skipped(fake_redis, gateway):
# SET NX returns None (not False) on conflict, so the helper must coerce to bool
assert tasks.record_payment.delay("o2", 500).get() == "charged"
assert tasks.record_payment.delay("o2", 500).get() == "duplicate"
assert gateway == ["o2"]
def test_lock_has_ttl(fake_redis, gateway):
# without a TTL, a crashed worker would block this order forever
tasks.record_payment.delay("o4", 500).get()
assert 0 < fake_redis.ttl("idem:payment:o4") <= 3600
def test_failed_run_releases_claim(monkeypatch, fake_redis):
calls = []
def flaky(order_id, cents):
calls.append(order_id)
if len(calls) == 1:
raise RuntimeError("gateway down")
monkeypatch.setattr(tasks, "charge_gateway", flaky)
with pytest.raises(RuntimeError):
tasks.record_payment.delay("o3", 100).get()
assert tasks.record_payment.delay("o3", 100).get() == "charged"
assert len(calls) == 2
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
Celery 5 tasks, chains and groups tested in eager mode
Define Celery tasks, chain and fan them out with groups, then test them offline with task_always_eager and the in-memory broker.
Celery beat crontab schedules validated and run eagerly
Define beat_schedule crontab entries, validate task names and schedule types before deploy, and test each scheduled call offline with task_always_eager.
Django post_save signal enqueuing a Celery task, tested with eager mode and on_commit
A post_save signal queues a Celery task only after the row commits via transaction.on_commit, then tests run offline on in-memory SQLite with Celery eager mode and pytest-django's on_commit capture.
RQ background jobs tested with fakeredis
Enqueue Redis Queue (RQ) jobs and process them with a SimpleWorker against fakeredis, so queue tests run offline with no Redis server.