Skip to content

[FEAT] shared concurrency groups across tasks and workflows #4237

Description

@pkozlov

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

  1. 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.
  2. 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.
  3. 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.
  4. 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
  • I acknowledge that an LLM was used in the creation of this Issue, in accordance with Hatchet's AI_POLICY.md.

Details
GPT-5.5 was used for codebase investigation, as well as for translating and refining the content of this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions