Tenancy
How a tenant identity flows through every request, query, cache key, and capacity counter to keep tenants isolated.
Ductor is multi-tenant from the ground up. A tenant identity isn't bolted on at the API edge — it flows through every layer, so isolation is a property of the whole system rather than a check in one place. This page explains where tenancy lives; Entitlements covers what a tenant is allowed to do once it's isolated.
Tenancy is an end-to-end invariant
Every object Ductor persists is tenant-scoped: pools, rules, connections, workflow definitions, and every workflow run. The tenant identity travels with:
- Every request — carried from the transport boundary through the application layer.
- Every workflow tick — a run's tenant is loaded with its state and stays attached through the whole tick.
- Every storage query — reads and writes filter on the tenant.
- Every cache key — cached pools and rules are keyed by tenant, so one tenant's cache can't serve another's data.
- Every capacity counter — the atomic Redis counters that enforce concurrency and rate limits are per-tenant.
Because the scoping is uniform, there's no single "did we remember to filter by tenant here?" gap. Isolation holds at storage, at cache, and at the hot path simultaneously.
Why isolation comes first
Tenant isolation is the precondition for everything in Entitlements. A quota only means something if one tenant's usage can't be charged to another; a plan ceiling only holds if load from tenant A can't consume tenant B's capacity. So the ordering is deliberate: isolate first, then enforce limits on top of clean boundaries.
The fair-scheduling connection
Tenancy also drives fairness, not just isolation. The tiered fair queue schedules work tenant-first: it picks the earliest-due tenant, then that tenant's earliest work, so a tenant with thousands of partitions can't starve a tenant with a handful. Fairness and isolation are two faces of the same tenant scoping.
System tenant
Not all work belongs to a customer. Internal machinery — most visibly coordinator wakeups — runs under a reserved system tenant on a dedicated partition. This keeps engine-internal traffic from competing with customer work in the fair scheduler and keeps the "every item has a tenant" invariant intact even for infrastructure tasks.
Run lineage within a tenant
Sub-workflows and ContinueAsNew create related runs, and that lineage is
tracked without ever crossing a tenant boundary. A run carries its
parent_run_id, root_run_id, and nesting_depth, so a tree of child runs and
a chain of ContinueAsNew successors all stay attributable to one tenant and one
originating request. See
The DAG Workflow Model.
One identity, everywhere
The practical rule: if a piece of data or a unit of work exists in Ductor, it has a tenant. Storage rows, cache entries, capacity counters, and queue items all carry one — which is what makes "tenant isolation" a structural guarantee rather than a review checklist.
Where to go next
- Entitlements — plan and quota enforcement on top of tenant isolation.
- The Tiered Fair Queue — tenant-fair scheduling.
- The Routing Pipeline — where tenant-scoped decisions are made.