Skip to main content

Threadmill: Durable high-performance backend for Django's task framework.
Documentation | Issues | Changelog | Funding 💚

Threadmill PyPi Version Test Coverage GitHub License

Durable high-performance backend for Django's task framework.

Design Principles

  • Durability – Recover from any failures, even poorly written tasks.
  • Consistency – Never lose data, even if someone unplugs the power or network.
  • Utilization – Keep the CPU saturated with tasks, not with idle time or waiting for locks.

Setup

You need to have Django's Task framework set up properly.

uv add threadmill[redis]

Add threadmill to your INSTALLED_APPS in settings.py and configure the task backend:

# settings.py
import os

INSTALLED_APPS = [
    "threadmill",
    # ...
]

TASKS = {
    "default": {
        "BACKEND": "threadmill.backends.redis.RedisTaskBackend",
        "REDIS_URL": os.getenv("REDIS_URL", "redis://localhost:6379/0"),
    },
    # ...
}

Optionally, install the inspector dependency if you want the TUI:

uv add threadmill[inspector]

Then launch the worker pool:

uv run manage.py threadmill worker

Usage

Workers

The workers are inspired by Gunicorn, and the CLI is very similar.

Utilization

Depending on your workload, you can tweak the number of processes and threads. Processes allow for parallel compute (no GIL) while threads are great for low-memory concurrent IO.

uv run manage.py threadmill worker --processes 4 --threads 2

Health

If your tasks leak memory, you can recycle (restart) the workers after a certain number of tasks have been processed:

uv run manage.py threadmill worker --max-tasks 1000 --max-tasks-jitter 100

This will restart the workers after 1000 tasks have been processed, with a random jitter of up to 100 tasks to avoid all workers restarting at the same time.

Should a worker crash or be killed, the pool will automatically restart it.

Shutdown

A graceful shutdown is possible with the SIGTERM or a keyboard interrupt. All workers will finish the tasks they acquired and acknowledge them.

You can use --exit-empty to exit immediately after all tasks have been processed, which might be useful for draining a one-off queue.

Inspector

Inspector TUI screenshot

The optional TUI inspector lets you watch queues, tasks, and task details in real-time. Install it with the inspector extra and launch it from a separate terminal:

uv add threadmill[inspector]
uv run manage.py threadmill inspector

Redis Backend Options

The RedisTaskBackend accepts the following options under OPTIONS in your TASKS configuration:

Option Default Description
lease_ttl timedelta(hours=1) Max processing time before a started task is marked FAILED.
result_ttl timedelta(days=1) How long task results are retained before automatic removal.
broker_interval timedelta(seconds=1) Interval between background broker maintenance passes.
batch_size 100 Max tasks to move or requeue per broker pass.

A task that is started but never acknowledged (lease expired) is marked FAILED with an AcknowledgementTimeout error. Set lease_ttl comfortably above your worst-case task runtime.

All keys for one backend alias share a Redis Cluster hash tag ({alias}), so every multi-key operation — including the cross-queue acquire — runs on a single shard. Scale horizontally by running additional backend aliases, not by relying on cross-slot operations.

Retrying failed tasks

Pass a retry callback to @task() to retry failed tasks with a delay. The callback receives a TaskContext — use context.attempt for the current attempt count and context.task_result.errors[-1] for the latest error. Return a timedelta to schedule the next attempt, or None to stop retrying.

The worker re-queues the failed task, preserving its ID and error history; the broker promotes it back to the ready queue once the delay elapses.

Built-in ExponentialBackoff

threadmill.retry.ExponentialBackoff provides a serializable exponential backoff strategy out of the box. It caps the delay at max_delay, stops after max_retries attempts, and only retries exceptions listed in expected_exceptions.

import datetime

from django.tasks import task
from requests import HTTPError

from threadmill.retry import ExponentialBackoff


@task(
    retry=ExponentialBackoff(
        base_delay=datetime.timedelta(seconds=1),
        max_delay=datetime.timedelta(minutes=5),
        factor=2.0,
        max_retries=5,
        expected_exceptions=(HTTPError,),
    )
)
def fetch_github_api(url: str): ...

Custom retry callbacks

For cases that need logic beyond what ExponentialBackoff supports, write a callable that accepts a TaskContext and returns a timedelta or None. Use TaskError.exception_class to filter by exception type:

import datetime

from django.tasks import task
from django.tasks.base import TaskContext
from requests import HTTPError


def retry_on_rate_limit(context: TaskContext) -> datetime.timedelta | None:
    """Retry HTTP 429 responses with exponential backoff, up to 5 attempts."""
    if context.attempt >= 5:
        return None
    error = context.task_result.errors[-1]
    if not issubclass(error.exception_class, HTTPError):
        return None
    return min(
        datetime.timedelta(seconds=2**context.attempt),
        datetime.timedelta(seconds=60),
    )


@task(retry=retry_on_rate_limit)
def fetch_github_api(url: str): ...

Sponsors

Sponsors

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

threadmill-0.6.3.tar.gz (29.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

threadmill-0.6.3-py3-none-any.whl (33.6 kB view details)

Uploaded Python 3

File details

Details for the file threadmill-0.6.3.tar.gz.

File metadata

  • Download URL: threadmill-0.6.3.tar.gz
  • Upload date:
  • Size: 29.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for threadmill-0.6.3.tar.gz
Algorithm Hash digest
SHA256 1bdfde2c51b78b14167663fca8ed9a02108b926aa5fb61e5ada986ac6ca6c49a
MD5 68ba9f88b8f253ee54747cd067c7a4f2
BLAKE2b-256 6b8750bf32c20262a3b46050a3b531c4163ccfd95c0289a55056c56a92f26232

See more details on using hashes here.

Provenance

The following attestation bundles were made for threadmill-0.6.3.tar.gz:

Publisher: release.yml on codingjoe/threadmill

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file threadmill-0.6.3-py3-none-any.whl.

File metadata

  • Download URL: threadmill-0.6.3-py3-none-any.whl
  • Upload date:
  • Size: 33.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for threadmill-0.6.3-py3-none-any.whl
Algorithm Hash digest
SHA256 20abbcaa2648d01c71d8e67c09b24273fb257d5d771933079e3d59d98b4a57a2
MD5 832c1381ba1f159bfa93c7a3927bef41
BLAKE2b-256 60045c7236fae21e1d98731de91801443862b263a306071744757276d01682a5

See more details on using hashes here.

Provenance

The following attestation bundles were made for threadmill-0.6.3-py3-none-any.whl:

Publisher: release.yml on codingjoe/threadmill

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.7.1

2 files

0.7.0

2 files

This release

0.6.3 This release

2 files

0.6.2

2 files

0.6.1

2 files

0.6.0

2 files

0.5.1

2 files

0.5.0

2 files

0.4.0

2 files

0.3.1

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page