{"slug": "django-tasks-prompt", "title": "Django tasks prompt", "summary": "A developer published a set of AI coding-agent prompts for migrating Django projects off Celery onto Django's built-in Tasks framework with Threadmill as the task backend and django-crontask replacing celery beat. The 21-step guide covers converting @app.task and @shared_task functions to @task with .enqueue(), restructuring chain/group/chord canvas workflows, mapping worker settings, and provisioning a dedicated non-evicting Redis instance, while noting Threadmill lacks a hard task timeout or rate limiter.", "body_md": "Upgrade to [Django's Task framework][django-tasks]\n\nPrompts\n\nUse the following prompts to instruct your AI coding agent to perform an update.\n\nCelery\n\n```\nReplace Celery with [Django's task framework][django-tasks].\n\n1. Use [Threadmill] as a task backend.\n2. Use [django-crontask] to replace celery beat. Recreate every `beat_schedule` entry and every django-celery-beat `PeriodicTask` row as a `@cron` decorated task, preserve the timezone, and decide how missed runs behave. Celery beat skips them, APScheduler applies `misfire_grace_time`.\n3. Convert `@app.task` and `@shared_task` functions to `@task` decorated functions, and replace `.delay()` and `.apply_async()` calls with `.enqueue()`. Reference task functions directly instead of the dotted strings passed to `send_task()`.\n4. Restructure Celery canvas workflows. `chain`, `group` and `chord` have no equivalent. Orchestrate follow-up tasks explicitly, or collect the `TaskResult` objects returned by `.enqueue()` and poll them.\n5. Replace `eta` and `countdown` with `task.using(run_after=...).enqueue()`.\n6. Use Threadmill's [retry] for `autoretry_for`, `max_retries` and broker re-delivery by retrying on the correct HTTP exceptions and status codes. Keep true rate limits in a lock based limiter, Threadmill has no rate limiter.\n7. Use `AcknowledgementTimeoutError` in `ExponentialBackoff(expected_exceptions=...)` for tasks which must be retried on lease expiration.\n8. Replace `task_always_eager` with `django.tasks.backends.immediate.ImmediateBackend` in tests.\n9. Replace Celery task signals and result hooks with receivers of `django.tasks.signals` (`task_enqueued`, `task_started`, `task_finished`).\n10. Replace `AsyncResult.get()` and `result_expires` with the `TaskResult` returned by `.enqueue()`. Use `.refresh()`, `.status`, `.return_value`, `.errors` and the backend's `result_ttl`.\n11. Drop `task_time_limit` and `task_soft_time_limit`. Threadmill has no hard task timeout, so enforce timeouts inside long-running tasks and keep `lease_ttl` above the worst-case runtime.\n12. Map the worker settings: `worker_concurrency` to `--workers` and `--threads`, `worker_max_tasks_per_child` to `--max-tasks`, `worker_prefetch_multiplier` to `--prefetch-count`. Remove `task_acks_late` and `task_reject_on_worker_lost`, Threadmill always acknowledges late.\n13. Add `threadmill[redis]` to the project dependencies and `threadmill[inspector]` to the dev dependency group only.\n14. Use `django.tasks.DEFAULT_TASK_QUEUE_NAME` and `django.tasks.DEFAULT_TASK_BACKEND_ALIAS` instead of `\"default\"` literals.\n15. Keep all current queue names and workers. Map every queue from `task_routes` and the `-Q` flags to a `TASKS` alias or `queue_name`, translate priorities from the 0 to 9 range to the -100 to 100 range, and run one `manage.py threadmill worker --queues ...` per queue.\n16. Pick thread/process worker configurations that meet available production RAM and CPU count, and validate the worker process/thread sizing. Note that `redis[hiredis]` re-enables the GIL and costs thread parallelism.\n17. Provision a dedicated, persistent Redis for the task backend. Threadmill does not support the RabbitMQ or SQS brokers, and the Redis instance must have no eviction policy set. Do not share an LRU cache instance.\n18. Replace the Celery worker and beat processes with `manage.py threadmill worker` and `manage.py crontask` in every deployment config, and drain the Celery queues before the cutover.\n19. Uninstall Celery, django-celery-beat and django-celery-results, remove the `CELERY_*` settings, and delete their database tables in a migration.\n20. Set `CRONTASK['REDIS_URL']` so the scheduler runs with a Redis lock, and document instructions how to connect the Threadmill inspector to the production redis. For example: `REDIS_URL=rediss://xxx uv run --group=dev python manage.py threadmill inspector`.\n21. If the project uses [django-health-check], add `health_check.contrib.crontask.Scheduler` and `health_check.contrib.threadmill.Threadmill`.\n\n[django-tasks]: https://docs.djangoproject.com/en/stable/topics/tasks/\n[Threadmill]: https://github.com/codingjoe/threadmill\n[django-crontask]: https://github.com/codingjoe/django-crontask\n[retry]: https://github.com/codingjoe/threadmill#retrying-failed-tasks\n[django-health-check]: https://codingjoe.dev/django-health-check\n```\n\nDramatiq\n\n```\nReplace Dramatiq and django-dramatiq with [Django's task framework][django-tasks].\n\n1. Use [Threadmill] as a task backend.\n2. Use [django-crontask] to replace dramatiq-crontab. Recreate every `@cron` and `@interval` periodic task as a `@cron` decorated task or an APScheduler `IntervalTrigger`, and move `DRAMATIQ_CRONTAB['REDIS_URL']` to `CRONTASK['REDIS_URL']`.\n3. Convert `@dramatiq.actor` functions to `@task` decorated functions, and replace `.send()` and `.send_with_options()` calls with `.enqueue()`.\n4. Replace the `delay` and `run_after` send options with `task.using(run_after=...).enqueue()`.\n5. Translate the broker middleware in `DRAMATIQ_BROKER`: `Retries` to retry callbacks, `Callbacks` to `django.tasks.signals`, `Prometheus` to your metrics pipeline, and `AdminMiddleware` to the Threadmill inspector. `AgeLimit` and `TimeLimit` have no equivalent, so drop stale work with `run_after` and enforce timeouts inside the task.\n6. Use Threadmill's [retry] for `max_retries` and broker re-delivery by retrying on the correct HTTP exceptions and status codes. Keep true rate limits in a lock based limiter, Threadmill has no replacement for `RateLimiters`.\n7. Use `AcknowledgementTimeoutError` in `ExponentialBackoff(expected_exceptions=...)` for tasks which must be retried on lease expiration.\n8. Replace the Dramatiq test mode with `django.tasks.backends.immediate.ImmediateBackend` in tests.\n9. Replace `after_process_message` callbacks with receivers of `django.tasks.signals` (`task_enqueued`, `task_started`, `task_finished`).\n10. Replace the Dramatiq results backend and the `django_dramatiq` admin task list with the `TaskResult` returned by `.enqueue()`. Use `.refresh()`, `.status`, `.return_value`, `.errors` and the backend's `result_ttl`.\n11. Drop `TimeLimit`. Threadmill has no hard task timeout, so enforce timeouts inside long-running tasks and keep `lease_ttl` above the worst-case runtime.\n12. Map the worker settings: `DRAMATIQ_NPROCS` and `--processes` to `--workers`, `DRAMATIQ_NTHREADS` and `--threads` to `--threads`, and worker restarts to `--max-tasks`.\n13. Add `threadmill[redis]` to the project dependencies and `threadmill[inspector]` to the dev dependency group only.\n14. Use `django.tasks.DEFAULT_TASK_QUEUE_NAME` and `django.tasks.DEFAULT_TASK_BACKEND_ALIAS` instead of `\"default\"` literals.\n15. Keep all current queue names and workers, map every Dramatiq queue to a `TASKS` alias or `queue_name`, and run one `manage.py threadmill worker --queues ...` per queue.\n16. Pick thread/process worker configurations that meet available production RAM and CPU count, and validate the worker process/thread sizing. Note that `redis[hiredis]` re-enables the GIL and costs thread parallelism.\n17. Provision a dedicated, persistent Redis for the task backend. Threadmill does not support the RabbitMQ broker, and the Redis instance must have no eviction policy set. Do not share an LRU cache instance.\n18. Replace the `rundramatiq` worker and the dramatiq-crontab scheduler with `manage.py threadmill worker` and `manage.py crontask` in every deployment config, and drain the Dramatiq queues before the cutover.\n19. Uninstall `dramatiq`, `django_dramatiq` and `dramatiq_crontab`, remove the `DRAMATIQ_*` settings, and delete the `django_dramatiq.models.Task` table in a migration.\n20. Set `CRONTASK['REDIS_URL']` so the scheduler runs with a Redis lock, and document instructions how to connect the Threadmill inspector to the production redis. For example: `REDIS_URL=rediss://xxx uv run --group=dev python manage.py threadmill inspector`.\n21. If the project uses [django-health-check], add `health_check.contrib.crontask.Scheduler` and `health_check.contrib.threadmill.Threadmill`.\n\n[django-tasks]: https://docs.djangoproject.com/en/stable/topics/tasks/\n[Threadmill]: https://github.com/codingjoe/threadmill\n[django-crontask]: https://github.com/codingjoe/django-crontask\n[retry]: https://github.com/codingjoe/threadmill#retrying-failed-tasks\n[django-health-check]: https://codingjoe.dev/django-health-check\n```\n\nDjango Q2\n\n```\nReplace Django Q2 with [Django's task framework][django-tasks].\n\n1. Use [Threadmill] as a task backend.\n2. Use [django-crontask] to replace the `Schedule` model and the cluster scheduler. Recreate every `Schedule` row as a `@cron` decorated task: translate MINUTES, HOURLY, DAILY, WEEKLY, BIWEEKLY, MONTHLY, BIMONTHLY, QUARTERLY and YEARLY to cron expressions or APScheduler triggers, preserve the timezone, and reproduce `repeats` with a trigger end date.\n3. Convert `async_task()` calls and `AsyncTask` instances to `@task` decorated functions. Pass the function directly instead of the dotted string, and queue delayed `ONCE` schedules with `task.using(run_after=...).enqueue()`.\n4. Replace `hook=` and the cluster signals with receivers of `django.tasks.signals` (`task_enqueued`, `task_started`, `task_finished`) or a Threadmill retry callback.\n5. Replace `result()`, `fetch()`, `result_group()`, `cached` and the group helpers with the `TaskResult` objects returned by `.enqueue()`: use `.status`, `.return_value`, `.errors` and `.refresh()`. A group becomes a list of results, and cached results map to the backend's `result_ttl`.\n6. Replace `sync=True` and the `Q_CLUSTER['sync']` test switch with `django.tasks.backends.immediate.ImmediateBackend`.\n7. Use Threadmill's [retry] for `max_attempts` and broker re-delivery by retrying based on the correct HTTP exceptions and status codes.\n8. Use `AcknowledgementTimeoutError` in `ExponentialBackoff(expected_exceptions=...)` for tasks which must be retried on lease expiration.\n9. Map the remaining `Q_CLUSTER` settings to Threadmill: `name` and `ALT_CLUSTERS` to backend aliases and queue names, `save_limit` and `save_limit_per` to `result_ttl`, `recycle` and `max_rss` to `--max-tasks`. There is no hard task timeout, so enforce timeouts inside long-running tasks and keep `lease_ttl` above the worst-case runtime. `ack_failures` disappears: failures are acknowledged and marked FAILED unless a retry callback re-queues them.\n10. Add `threadmill[redis]` to the project dependencies and `threadmill[inspector]` to the dev dependency group only.\n11. Use `django.tasks.DEFAULT_TASK_QUEUE_NAME` and `django.tasks.DEFAULT_TASK_BACKEND_ALIAS` instead of `\"default\"` literals.\n12. Keep all current queue names and workers, and run one `manage.py threadmill worker` per queue.\n13. Pick thread/process worker configurations that meet available production RAM and CPU count, and validate the worker process/thread sizing. Note that `redis[hiredis]` re-enables the GIL and costs thread parallelism.\n14. Provision a dedicated, persistent Redis for the task backend. Threadmill does not support the ORM, SQS, MongoDB or IronMQ brokers, and the Redis instance must have no eviction policy set. Do not share an LRU cache instance.\n15. Replace the `qcluster` process with `manage.py threadmill worker` and `manage.py crontask` in every deployment config. Drain the Django Q2 queues before the cutover.\n16. Uninstall Django Q2, remove the `Q_CLUSTER` setting and delete the `Task`, `Success`, `Failure`, `Schedule` and `OrmQ` models in a migration.\n17. Set `CRONTASK['REDIS_URL']` so the scheduler runs with a Redis lock, and document instructions how to connect the Threadmill inspector to the production redis. For example: `REDIS_URL=rediss://xxx uv run --group=dev python manage.py threadmill inspector`.\n18. If the project uses [django-health-check], add `health_check.contrib.crontask.Scheduler` and `health_check.contrib.threadmill.Threadmill`.\n\n[django-tasks]: https://docs.djangoproject.com/en/stable/topics/tasks/\n[Threadmill]: https://github.com/codingjoe/threadmill\n[django-crontask]: https://github.com/codingjoe/django-crontask\n[retry]: https://github.com/codingjoe/threadmill#retrying-failed-tasks\n[django-health-check]: https://codingjoe.dev/django-health-check\n```", "url": "https://wpnews.pro/news/django-tasks-prompt", "canonical_source": "https://gist.github.com/codingjoe/13c4fd7da3bed1546e344435ec5e32be", "published_at": "2026-10-11 09:42:03+00:00", "updated_at": "2026-10-15 14:54:49.958373+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "ai-tools"], "entities": ["Django", "Celery", "Threadmill", "django-crontask", "Redis", "RabbitMQ", "SQS", "django-health-check"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/django-tasks-prompt", "markdown": "https://wpnews.pro/news/django-tasks-prompt.md", "text": "https://wpnews.pro/news/django-tasks-prompt.txt", "jsonld": "https://wpnews.pro/news/django-tasks-prompt.jsonld"}}