Goal: replace Celery with Taskiq across the Django runtime, scheduled jobs, deployment manifests, and tests without changing the user-facing behavior of ingestion, AI pipeline execution, newsletter drafting, trend jobs, or avatar processing. This migration should prefer async-native task implementations rather than keeping the existing synchronous task bodies and merely adapting the queue layer.
- Python runtime task modules now use Taskiq end to end. The old Celery bootstrap and Celery settings modules have been deleted.
- Telemetry and dependency manifests no longer carry Celery runtime wiring.
uv.lockand the Pants lockfile were regenerated after removing the Celery packages. - Local and Helm runtime manifests now use Taskiq worker and scheduler processes and provision RabbitMQ explicitly for the Taskiq broker.
- Test and env harnesses now use
TASKIQ_ALWAYS_EAGERinstead ofCELERY_TASK_ALWAYS_EAGERacross the executable repo surfaces. - A targeted repo grep across executable code, tests, deploy manifests, env examples, and scripts no longer finds Celery-specific runtime references.
- Public top-level docs now describe the Taskiq-based stack. Remaining Celery mentions are confined to historical planning text in this file.
- Regenerated dependency locks with
uv lockandpants generate-lockfiles --resolve=python-default. - Rendered local and deployed runtime config successfully with
docker compose configandhelm template digest-engine ./deploy/helm/digest-engine. - Re-ran
python manage.py checkafter the telemetry, dependency, and deployment edits. - Re-ran focused pytest slices covering newsletters, users profile API, core eager paths, trends, entities, ingestion, and pipeline retry behavior after replacing the Celery-era eager test harness.
- Optional documentation cleanup only: historical Celery terminology remains in this planning note and any external runbooks or dashboards that are not stored in this repo.
- No executable repo surfaces still depend on Celery.
- Taskiq bootstrap lives in digest_engine/taskiq.py.
- Taskiq settings live in digest_engine/settings/taskiq.py and are re-exported from digest_engine/settings/init.py.
- Task modules now migrated to Taskiq live in:
- Task dispatch from synchronous Django entrypoints now goes through the shared enqueue seam in digest_engine/taskiq.py.
- Local and deployed worker scheduling now depends on Taskiq worker and Taskiq scheduler in:
- Current entrypoint coverage assumes Taskiq broker and scheduler bootstrap in core/tests/test_entrypoints.py.
- RabbitMQ is now provisioned in local and Helm deployment surfaces alongside Redis result storage.
- Preserve the current eager-mode ergonomics used in tests and local control flow, where some orchestration tasks run inline when background execution is disabled.
- Preserve task names and call semantics long enough to migrate callers incrementally instead of forcing a big-bang rename.
- Preserve existing schedule cadence from digest_engine/settings/celery.py until behavior is intentionally changed.
- Keep the AI pipeline boundaries intact: persisted state in models, orchestration in task modules, pipeline logic in app-owned helpers.
- Treat async-first task execution as the default target. Only keep synchronous execution where a dependency is fundamentally blocking or CPU-bound.
- Be explicit about sync boundaries in Django and library code. Moving to Taskiq does not make Django ORM, Pillow/image manipulation, or CPU-heavy transforms automatically async.
- Adopt
taskiqas the core queue runtime. - Use
taskiq-aio-pikafor the primary broker and RabbitMQ as the production queue transport. - Use
taskiq-redisfor Redis-backed result storage and any Redis-backed scheduling helpers we later decide to add. - Start with core
TaskiqSchedulerplusLabelScheduleSourcefor the current fixed recurring schedules. Revisit dynamic Redis-backed schedules only if product requirements appear. - Keep one shared broker/bootstrap module under
digest_engine/, analogous to the current Celery entrypoint, but Taskiq-native. - Replace
CELERY_TASK_ALWAYS_EAGERwith a repo-owned Taskiq-era setting and test harness, rather than preserving Celery naming. - Plan for an async-first rewrite of task definitions. A sync-safe enqueue helper may still exist for synchronous Django call sites, but that helper should bridge caller context to Taskiq and not be treated as justification to keep task implementations mostly synchronous.
These boundaries are the agreed exceptions to the async-first direction. Async Taskiq handlers should treat these as explicit handoff points instead of letting blocking work leak through the task layer.
- Admin actions:
- projects/admin.py
ProjectConfigAdmin.recompute_selected_authority_models - content/admin.py
ContentAdmin.generate_newsletter_ideas - pipeline/admin.py review-queue retry admin action
- projects/admin.py
- Management commands:
- core/management/commands/bootstrap_live_sources.py source bootstrap ingestion dispatch
- Sync Ninja or Django request handlers:
- newsletters/ninja_api.py
generate_newsletter_draft_route - newsletters/ninja_api.py
regenerate_newsletter_draft_section_route - pipeline/ninja_api.py
retry_review_queue_item_route - trends/ninja_api.py original-content idea generation route
- projects/ninja_project_configs_api.py authority and source-quality recompute trigger
- users/ninja_api.py local task enqueue helper for avatar thumbnail generation
- newsletters/intake.py
queue_newsletter_intake
- newsletters/ninja_api.py
- Scope rule: these call sites should remain synchronous request or command code, but all queue submission should go through one repo-owned Taskiq enqueue helper rather than direct broker calls spread across the codebase.
- The following task modules are dominated by synchronous Django ORM work and should be treated as sync data-access units even when their Taskiq entrypoints become
async def: - The heaviest downstream ORM helpers that should stay behind explicit sync boundaries are:
- Scope rule: async Taskiq handlers may orchestrate these calls, but database-intensive units should be isolated with
sync_to_async, thread offloading, or dedicated synchronous helper functions rather than mixing raw ORM calls directly throughout async control flow.
- users/tasks.py
build_avatar_thumbnail- Uses Pillow image decoding, EXIF transpose, resize, WebP encoding, and storage writes.
- This should remain a sync helper invoked via thread or executor offload.
- newsletters/extraction.py
_extract_newsletter_items_heuristically- Uses
HTMLParserand regex-based extraction over raw newsletter bodies. - This is a reasonable sync parsing unit and does not need to be forced async.
- Uses
- entities/tasks.py candidate clustering and identity-enrichment helpers
- Includes string-similarity clustering and content normalization work alongside ORM access.
- Keep the compute-heavy portions behind explicit sync boundaries unless a clear async benefit appears.
- core/embeddings.py sentence-transformer embedding generation
- Local model inference is blocking compute work even if the surrounding task becomes async.
- HTTP clients currently used in synchronous mode:
- core/llm.py
httpx.postfor OpenRouter chat completions - core/embeddings.py
httpx.postfor Ollama and OpenRouter embeddings - content/deduplication.py
httpx.headfor short-link expansion - entities/tasks.py
httpx.getfor identity probing
- core/llm.py
- Sync SDKs and parsers that should be assumed blocking unless replaced:
- ingestion/plugins/linkedin.py
requests.post - ingestion/plugins/rss.py
feedparser.parse - ingestion/plugins/reddit.py
praw.Reddit - entities/tasks.py
atproto.Client - entities/tasks.py
mastodon.Mastodon - core/embeddings.py
QdrantClient
- ingestion/plugins/linkedin.py
- Scope rule:
- Convert
httpxusage tohttpx.AsyncClientwhere practical. - Keep obviously sync-only libraries behind thread offload unless we replace them with async-capable alternatives.
- Treat Qdrant and embedding provider access as explicit boundaries inside the embedding layer, not as direct async logic scattered across tasks.
- Convert
- messaging/signals.py and notifications/signals.py already use
async_to_syncto bridge sync Django signals into async channel-layer operations. - Scope rule: use the same style of narrow bridge for Taskiq enqueueing from sync Django entrypoints, but do not use that bridge as a reason to keep task implementations themselves predominantly synchronous.
The original phase plan below is preserved as a historical record of the migration approach that landed. The runtime, dependency, deploy, and test-harness work described there is now complete.
- Implement the selected Taskiq stack for this repo:
taskiqtaskiq-aio-pikataskiq-redis
- Define the Django bootstrap module and Taskiq broker wiring under
digest_engine/. - Define the initial scheduler shape using core
TaskiqSchedulerandLabelScheduleSourcefor code-owned recurring jobs. - Define the replacement for
CELERY_TASK_ALWAYS_EAGER, likely a Taskiq-specific eager/immediate execution setting plus a local helper seam for tests.
- Add Taskiq dependencies to pyproject.toml and regenerate 3rdparty/python/default.lock.
- Create a new broker/bootstrap module under
digest_engine/to initialize Taskiq against Django settings, RabbitMQ, and Redis result storage. - Add a small dispatch abstraction that replaces direct
.delay(...)knowledge with one repo-owned seam, so synchronous Django callers can enqueue Taskiq work safely while the internals move to async-first execution. - Add settings for broker URL, Redis result backend, eager mode, timeout/retry defaults, and scheduler configuration without removing Celery yet.
- Keep this first slice additive so the repo can compile and tests can run before task modules are ported.
- Migrate core/tasks.py first because it is the largest cross-cutting orchestration surface and defines helper patterns reused elsewhere.
- Port the remaining task modules in dependency order:
- Replace
@shared_task(...)with the Taskiq task decorator strategy selected in Phase 1. - Convert task entrypoints to
async defwhere the surrounding work can be made async with reasonable effort. - Replace synchronous external I/O inside tasks with async-capable clients where available.
- Move unavoidable synchronous work behind explicit boundaries such as
sync_to_async,asyncio.to_thread, or executor-based helpers rather than leaving whole task modules synchronous by default. - Replace local
DelayedTask/_enqueue_taskhelpers with a broker-agnostic enqueue helper that supports immediate execution where required. - Verify that task return values, ignored results, retries, logging behavior, and async error handling remain acceptable for each converted module.
- Replace
.delay(...)dispatch from runtime callers with the new enqueue seam in these high-signal surfaces first. Prefer async-native call sites when already in async contexts; use a narrow sync-to-async bridge only where Django entrypoints remain synchronous: - Recreate the current recurring schedules from digest_engine/settings/celery.py in the Taskiq scheduler layer.
- Keep schedule names and cadence recognizable so operational diffs are easy to review.
- Update digest_engine/settings/init.py to export Taskiq settings instead of Celery settings.
- Remove digest_engine/celery.py once all imports are gone and replace it with the Taskiq broker entrypoint.
- Update local development commands in justfile to start Taskiq worker and scheduler processes instead of Celery worker and beat.
- Update docker-compose.yml service names and commands for Taskiq runtime processes.
- Update Helm values, worker/scheduler deployments, KEDA config, and network policy references under deploy/helm/digest-engine.
- Replace Celery-specific OpenTelemetry instrumentation in pyproject.toml with the Taskiq equivalent if one is available; otherwise document the temporary observability gap.
- Delete Celery settings module and any remaining
CELERY_*environment variables once Taskiq config is live. - Remove Celery dependency and Celery instrumentation from pyproject.toml.
- Regenerate Pants lockfiles after dependency changes.
- Update tests that currently assert Celery app bootstrapping and beat schedule behavior, starting with core/tests/test_entrypoints.py.
- Search for remaining
celery,shared_task,.delay(,apply_async,celery-worker, andcelery-beatreferences and remove or rename them. - Update operator and contributor docs once the runtime commands and deployment terminology change.
- After Phase 2: run a narrow import/config check for the new broker bootstrap plus
python manage.py check. - After each task-module migration: run the nearest focused pytest module plus
python manage.py check. - After scheduler migration: add or update targeted tests for scheduled job registration and cadence.
- After compose/helm changes: run
just lintand the existing Helm lint target. - Before deleting Celery: run
just testand confirm no repo matches for Celery runtime imports remain outside historical notes.
- Taskiq scheduling semantics may not match Celery beat exactly, especially for mixed interval and cron jobs.
- Taskiq retry and result-handling behavior may differ from Celery for long-running AI or ingestion jobs.
- Existing eager-mode assumptions may be embedded in tests or orchestration helpers and need an explicit compatibility shim.
- A fully async rewrite will expose blocking dependencies that were previously hidden inside synchronous Celery workers, especially ORM-heavy code paths and CPU-bound helpers.
- Some code paths may only become partially async because Django ORM usage, image processing, or third-party SDKs still require thread or process offloading.
- Telemetry coverage may regress if Celery instrumentation is removed before a Taskiq replacement is wired.
- Deployment naming changes may require coordinated updates in dashboards, scaling policies, and operator expectations.
- No runtime migration work remains in-repo. If a future pass is needed, limit it to historical wording cleanup and external operator collateral.