Tenant-Isolated Document Indexing Queue#
Dify's document indexing pipeline uses a Redis-backed, per-tenant FIFO queue to prevent one tenant's heavy indexing workload from starving others. The core abstraction is TenantIsolatedTaskQueue, which manages two Redis keys per tenant:
| Redis Key | Purpose |
|---|---|
tenant_self_{key}_task_queue:{tenant_id} | List of waiting DocumentTask payloads (LPUSH / RPOP = FIFO) |
tenant_{key}_task:{tenant_id} | Presence flag β set with a 1-hour TTL while a task is in-flight |
The presence flag acts as a soft mutex: if it exists, new submissions are queued; if absent, the submission sets the flag and dispatches immediately .
TENANT_ISOLATED_TASK_CONCURRENCY (default 1) controls how many tasks are dequeued per handoff .
Dispatch Routing#
The entry point for callers is DocumentTaskProxyBase.delay(), which routes based on deployment edition :
| Deployment Edition | Plan | Queue | Tenant Isolation |
|---|---|---|---|
| Cloud | Sandbox | dataset (normal) | β Yes |
| Cloud | Paid | priority_dataset | β Yes |
| Self-hosted / Enterprise | β | priority_dataset | β No |
Subclass BatchDocumentIndexingProxy implements the actual queue check-and-dispatch for batch submissions. Tasks are serialized as DocumentTask dataclasses (fields: tenant_id, dataset_id, document_ids) and wrapped with TaskWrapper for JSON serialization .
The two concrete Celery tasks that run under the queue system are:
normal_document_indexing_taskβdatasetCelery queuepriority_document_indexing_taskβpriority_datasetCelery queue
Duplicate-document (re-index) flows mirror the same structure via normal_duplicate_document_indexing_task and priority_duplicate_document_indexing_task, using a separate queue key "duplicate_document_indexing" .
Queue Handoff Mechanism#
Tenant isolation is maintained through a finally-block chain: regardless of whether indexing succeeds or raises, the completing task always checks the queue for its tenant and hands off to the next waiting task.
The handoff logic in _document_indexing_with_tenant_queue():
- Run
_document_indexing()inside atryblock. - In
finally: callpull_tasks(count=TENANT_ISOLATED_TASK_CONCURRENCY)β a RedisRPOPfrom the tenant's list . - If tasks exist: refresh the TTL via
set_task_waiting_time()and dispatch each viatask_func.apply_async(). - If queue is empty: call
delete_task_key()to clear the presence flag .
The duplicate-document variant (_duplicate_document_indexing_task_with_tenant_queue()) follows the same pattern but uses task_func.delay() instead of apply_async with a shared producer.
TTL-Based Cleanup and Error Recovery#
The presence flag key is set via Redis SETEX with a 1-hour TTL . This is a safety net: if a Celery worker crashes mid-task before the finally block fires, the flag auto-expires and the next submission for that tenant will correctly re-enter the dispatch path rather than being silently queued forever.
Error handling within tasks:
- Billing limit violations β documents are marked
IndexingStatus.ERRORimmediately before any indexing begins . DocumentIsPausedErrorβ logged at INFO level; the queue handoff still runs .- All other exceptions β caught, logged, then the queue handoff still runs in
finallyto avoid blocking the tenant's queue . - Sync task errors β
document_indexing_sync_taskwritesIndexingStatus.ERRORwith the exception message directly to the document record .
Bypass Paths (No Tenant Isolation)#
Four paths bypass the TenantIsolatedTaskQueue entirely and run indexing directly:
1. Event Handler (create_document_index.py)#
handle() listens on the document_index_created Blinker signal and runs IndexingRunner.run() synchronously in-process β no Redis, no Celery, no per-tenant queuing. This is typically triggered in contexts where the calling thread already manages concurrency.
2. Legacy Tasks (to be deprecated)#
document_indexing_task and duplicate_document_indexing_task accept only (dataset_id, document_ids) β no tenant_id parameter β and call the shared _document_indexing / _duplicate_document_indexing_task functions directly without any queue coordination. Both are marked TO BE DEPRECATED in their docstrings.
3. Notion Sync Task#
document_indexing_sync_task is a standalone Celery task (queue: dataset) that re-indexes a single Notion-sourced document. It accepts only (dataset_id, document_id), has no tenant_id routing argument, and performs no queue check. It includes its own early-exit if the document is already in PARSING status and if Notion content is unchanged since last sync .
4. Self-Hosted / Enterprise Direct Dispatch#
As noted in Dispatch Routing, non-Cloud deployments call _send_to_priority_direct_queue() , which skips TenantIsolatedTaskQueue and sends directly to Celery.
Key Files Reference#
| File | Role |
|---|---|
api/core/rag/pipeline/queue.py | TenantIsolatedTaskQueue and TaskWrapper β core Redis primitives |
api/services/document_indexing_proxy/base.py | DocumentTaskProxyBase β deployment-edition-aware dispatch routing |
api/services/document_indexing_proxy/batch_indexing_base.py | BatchDocumentIndexingProxy β queue check-and-push logic |
api/core/entities/document_task.py | DocumentTask dataclass β serialized queue payload |
api/tasks/document_indexing_task.py | Normal & priority Celery tasks + handoff logic |
api/tasks/duplicate_document_indexing_task.py | Duplicate-doc variants of the same |
api/tasks/document_indexing_sync_task.py | Notion sync β bypasses tenant isolation |
api/events/event_handlers/create_document_index.py | In-process event handler β bypasses tenant isolation |
api/configs/feature/__init__.py | TENANT_ISOLATED_TASK_CONCURRENCY config (default: 1) |