Is your feature request related to a problem? Please describe.
Currently, Hatchet concurrency keys appear to be scoped to a specific task/workflow concurrency strategy. This works well when the concurrency limit only needs to apply to a single task or workflow definition. However, there are cases where multiple independent tasks or workflows need to coordinate access to the same external mutable resource.
Example:
- ImportOrdersWorkflow mutates data for account_id=123
- SyncCustomerWorkflow also mutates data for account_id=123
- ReconcileAccountWorkflow also mutates data for account_id=123
These workflows may be independent from a DAG perspective, but they should not run the critical mutation steps concurrently for the same account_id.
Today, if each workflow defines:
ConcurrencyExpression(
expression="input.account_id",
max_runs=1,
limit_strategy=ConcurrencyLimitStrategy.GROUP_ROUND_ROBIN,
)
the concurrency limit is still tied to each workflow/task concurrency definition rather than acting as a shared lock across all workflows using the same rendered key.
Describe the solution you'd like
I would like Hatchet to support shared concurrency groups across different tasks and workflows. The proposed API could extend the existing concurrency expression model with an optional scope and namespace:
ConcurrencyExpression(
expression="input.account_id",
max_runs=1,
limit_strategy=ConcurrencyLimitStrategy.GROUP_ROUND_ROBIN,
scope=ConcurrencyScope.SHARED,
namespace="account-mutations",
)
Proposed semantics:
LOCAL, current/default behavior:
tenant_id + strategy_id + rendered_key
SHARED, proposed behavior:
tenant_id + namespace + rendered_key
This would allow different task/workflow definitions to share the same concurrency pool when they intentionally use the same namespace and rendered key. Example:
ImportOrdersWorkflow:
namespace = "account-mutations"
key = input.account_id
SyncCustomerWorkflow:
namespace = "account-mutations"
key = input.account_id
Expected behavior:
ImportOrdersWorkflow(account_id=123) starts
SyncCustomerWorkflow(account_id=123) waits
ImportOrdersWorkflow(account_id=456) can run independently
SyncCustomerWorkflow(account_id=456) can run independently
The max_runs value should apply per rendered key, preserving the existing mental model of ConcurrencyExpression.
If a user wants a global limit across the entire namespace, they can use a constant expression:
ConcurrencyExpression(
expression="'*'",
max_runs=10,
limit_strategy=ConcurrencyLimitStrategy.GROUP_ROUND_ROBIN,
scope=ConcurrencyScope.SHARED,
namespace="heavy-jobs",
)
For an initial version, I think it would be reasonable to support only GROUP_ROUND_ROBIN limit strategy for SHARED scope and leave CANCEL_IN_PROGRESS and CANCEL_NEW semantics for a later design, since cancelling runs across different workflow definitions may be surprising.
It would also be useful to validate shared namespace configuration. For example, if two tasks use the same shared namespace but different max_runs or different strategies, registration should probably fail with a clear error.
Describe alternatives you've considered
- Use an external lock. Use PostgreSQL advisory locks, Redis locks, or a custom locks table inside the task body. This works, but it moves concurrency control outside Hatchet. The task may already be running while waiting for the external lock, which consumes worker capacity and makes queueing/fairness less visible in Hatchet.
- Use a wrapper workflow. Put all conflicting operations behind one shared workflow/task definition and apply standard Hatchet concurrency there. This works when the architecture can be centralized, but it is less convenient when multiple independent workflows naturally need to coordinate around the same resource.
- Precompute a key and pass it into input. This helps when the key cannot be represented directly as a CEL expression, but it does not solve the cross-workflow/task scoping problem if concurrency pools remain tied to a specific strategy definition.
- Rely on idempotency and conflict handling at the application layer. This is still necessary in many systems, but it does not replace the need to prevent concurrent mutations of the same external resource where sequential execution is required.
Additional context
This feature would be useful for workloads where several independent workflows mutate or synchronize the same external resource, such as:
- account/customer synchronization;
- importing and reconciling data for the same tenant/account/project;
- avoiding concurrent writes to an external API resource;
- serializing per-resource mutation jobs across multiple workflow definitions;
- coordinating critical sections without moving all logic into a single wrapper workflow.
🤖 AI Disclosure
Details
GPT-5.5 was used for codebase investigation, as well as for translating and refining the content of this issue.
Is your feature request related to a problem? Please describe.
Currently, Hatchet concurrency keys appear to be scoped to a specific task/workflow concurrency strategy. This works well when the concurrency limit only needs to apply to a single task or workflow definition. However, there are cases where multiple independent tasks or workflows need to coordinate access to the same external mutable resource.
Example:
These workflows may be independent from a DAG perspective, but they should not run the critical mutation steps concurrently for the same account_id.
Today, if each workflow defines:
the concurrency limit is still tied to each workflow/task concurrency definition rather than acting as a shared lock across all workflows using the same rendered key.
Describe the solution you'd like
I would like Hatchet to support shared concurrency groups across different tasks and workflows. The proposed API could extend the existing concurrency expression model with an optional scope and namespace:
Proposed semantics:
This would allow different task/workflow definitions to share the same concurrency pool when they intentionally use the same namespace and rendered key. Example:
Expected behavior:
The max_runs value should apply per rendered key, preserving the existing mental model of ConcurrencyExpression.
If a user wants a global limit across the entire namespace, they can use a constant expression:
For an initial version, I think it would be reasonable to support only
GROUP_ROUND_ROBINlimit strategy forSHAREDscope and leaveCANCEL_IN_PROGRESSandCANCEL_NEWsemantics for a later design, since cancelling runs across different workflow definitions may be surprising.It would also be useful to validate shared namespace configuration. For example, if two tasks use the same shared namespace but different max_runs or different strategies, registration should probably fail with a clear error.
Describe alternatives you've considered
Additional context
This feature would be useful for workloads where several independent workflows mutate or synchronize the same external resource, such as:
🤖 AI Disclosure
Details
GPT-5.5 was used for codebase investigation, as well as for translating and refining the content of this issue.