django-ox, a database-backed worker for the new Tasks framework

Hi all,

I’ve released django-ox (open source), a worker backend for django.tasks that stores the queue in your existing database instead of a broker.

The docs for the Tasks framework say production systems should rely on backends that supply a
worker process and a durable queue, so that’s what this is. The part I care most about
is that enqueue() is a plain INSERT on your default connection. If you enqueue inside
transaction.atomic(), the task commits or rolls back together with your business data. No
on_commit boilerplate, and no window where an order exists without its confirmation email task
or the other way round.

What’s in the box right now:

  • at-least-once execution, claimed with SELECT FOR UPDATE SKIP LOCKED on databases that have it,
    and an atomic compare-and-set everywhere else (SQLite included, which is handy for small
    deployments)
  • retries with exponential backoff, keeping the traceback of every attempt in the row
  • a reaper that returns tasks from dead workers to the queue
  • graceful shutdown: SIGTERM stops claiming and drains in-flight tasks
  • deferred tasks (run_after), priorities and multiple queues, per the Tasks API
  • recurring tasks: cron schedules declared in settings next to the backend, dispatched by the
    workers themselves, so there is no separate scheduler process to keep alive. A unique
    constraint on (schedule, tick) makes each tick fire once however many workers are polling.
    If every worker was down when a tick passed, the latest missed tick fires once on recovery
    and older ones are skipped, so a nightly job survives a bad deploy window without a
    thundering herd afterwards
  • monitoring: a queue-stats API, an ox_health command for probes and cron alerting, and
    structured log events on the django_ox logger
  • an ox_prune command for cleaning up finished rows

Nothing to learn on the producer side, it’s just the standard @task decorator and .enqueue().

Free, BSD-3. The suite is 191 tests, green on SQLite and Postgres 16 across
Django 6.0 and 6.1, Python 3.12 to 3.14.

There are benchmarks against django-tasks-db in the repo, with the methodology, the raw
per-sample data, plus a soak run: 37,802 tasks under sustained
load while workers were repeatedly SIGKILLed, with zero tasks lost.

Repo: GitHub - oxpull/django-ox: Database-backed worker for Django's Tasks framework. Transactional enqueue, retries, recurring schedules, no broker to run. · GitHub
Docs: django-ox

I’d genuinely like to hear your pov, and issues are welcome!
peace:)

Congratulations - you vibe-coded something. You even created a linting tool to try catch AI slop before it gets published so that people don’t realise it’s AI slop. Not sure why you removed the linter from the repo - I found that the most interesting!

    # --- Build-assistant attribution ----------------------------------------
    # This corpus was swept clean by hand once. A gate keeps it clean.
    Rule(
        "AI_DISCLOSURE",
        re.compile(
            r"(?:"
            + _words(
                "claude", "chatgpt", "gpt-4", "gpt-5", "copilot", "anthropic", "openai"
            )
            + r"|\bAI[- ](?:generated|written|assisted|authored)\b"
            + r"|\bgenerated (?:by|with) (?:an? )?(?:AI|LLM|model|assistant)\b"
            + r"|\b(?:LLM|language model)[- ](?:generated|written)\b"
            + r"|\bco-authored-by:\s*claude"
            + r")",
            re.I,
        ),
        "Build-assistant attribution does not belong on a public surface.",
    ),
    # --- Assistant-prose tells ----------------------------------------------
    # The standard is that external text reads like a person wrote it. These are
    # the phrases that most reliably say otherwise.
    Rule(
        "AI_SLOP",
        re.compile(
            _words(
                "delve",
                "delves",
                "seamless",
                "seamlessly",
                "effortless",
                "effortlessly",
                "robust and",
                "cutting-edge",
                "state-of-the-art",
                "game-changer",
                "game-changing",
                "revolutionary",
                "supercharge",
                "unlock the power",
                "harness the power",
                "take it to the next level",
                "best-in-class",
                "world-class",
                "battle-tested",
                "blazing[- ]fast",
                "blazingly fast",
                "lightning[- ]fast",
                "rock[- ]solid",
                "bulletproof",
            ),
            re.I,
        ),
        "Marketing filler. Say the specific thing instead.",
    ),
    Rule(
        "AI_SLOP",
        re.compile(
            r"\bin today's [\w\s-]*(?:world|landscape|environment)\b"
            r"|\bwe(?:'re| are) (?:thrilled|excited|pleased) to (?:announce|share)\b"
            r"|\bit(?:'s| is) (?:important|worth) (?:to note|noting) that\b"
            r"|\bat the end of the day\b"
            r"|\bwhen it comes to\b"
            r"|\bthe world of\b",
            re.I,
        ),
        "Filler opener with no information in it.",
    ),
    Rule(
        "AI_SLOP",
        re.compile(r"\bnot (?:only|just) \w+[^.]{0,60}\bbut also\b", re.I),
        "Not-only-but-also construction reads as generated copy.",
    ),
    Rule(
        "AI_SLOP",
        # The standing house style is no em-dashes in external text. Written as
        # escapes (U+2014 em, U+2013 en) rather than literal characters so this
        # file does not trip the ambiguous-character lint that its own rule set
        # exists to enforce elsewhere.
        re.compile("\u2014|\u2013(?=\\s)|(?<=\\s)\u2013"),
        "Em or en dash. House style uses commas, colons or full stops.",
    ),
    Rule(
        "AI_SLOP",
        re.compile(r"[\U0001F300-\U0001FAFF✅❌✨⚠️]"),
        "Emoji. Not the register this product ships in.",
    ),

you have discovered a regex that bans delve, seamless,game-changing and we're thrilled to announce. It also bans emoji and em dashes. If that is a cover-up, it is the most boring one on this forum. It is a style gate, it lives with the rest of my editorial tooling , and the commit is still in the history if you want the other 400 lines.

One genuine question, since you spotted a file that had already been removed: did you diff the history yourself, or did you have something read the repo for
you :face_with_monocle: ?

You went through it carefully enough to catch a deleted file, which is more attention than most packages posted here get. I would have taken it as a compliment if you had pointed it at worker.py. At-least-once execution,
SELECT FOR UPDATE SKIP LOCKED where the database has it and compare-and-set where it does not, a reaper for tasks whose worker died. 191 tests, green on
SQLite and Postgres.

Break it and I will thank you properly :wink: