Skip to main content
Rate limits cap how many times a task can consume a resource within a time window. They are distinct from concurrency limits:
ConcurrencyRate limit
ScopeHow many runs execute at onceHow many runs start within a time window
ResetSlot frees when the run finishesCounter resets on a fixed schedule (per second, minute, etc.)
Use caseProtect a shared resource from overloadComply with API quotas or fair-use policies
A task can carry multiple rate limits, and you can mix static (global) and dynamic (per-user / per-tenant) limits on the same task.

Define a rate limit key

Before a task can consume a static rate limit, you must register the key and its quota with Hatchet. Call hatchet.rate_limits.put() (Python) / hatchet.ratelimits.upsert() (TypeScript) / client.RateLimits().Upsert() (Go) at startup:
worker.py
from hatchet_sdk import Hatchet
from hatchet_sdk.rate_limit import RateLimitDuration

hatchet = Hatchet(debug=True)

RATE_LIMIT_KEY = "test-limit"

# Register: allow 2 runs per second globally
hatchet.rate_limits.put(RATE_LIMIT_KEY, 2, RateLimitDuration.SECOND)

Static (global) rate limit

A static rate limit uses a fixed string key. All runs of the task share the same quota bucket regardless of their input.
worker.py
from pydantic import BaseModel
from hatchet_sdk import Context, Hatchet
from hatchet_sdk.rate_limit import RateLimit, RateLimitDuration

hatchet = Hatchet(debug=True)

class RateLimitInput(BaseModel):
    user_id: str

rate_limit_workflow = hatchet.workflow(
    name="RateLimitWorkflow", input_validator=RateLimitInput
)

RATE_LIMIT_KEY = "test-limit"

@rate_limit_workflow.task(rate_limits=[RateLimit(static_key=RATE_LIMIT_KEY, units=1)])
def step_1(input: RateLimitInput, ctx: Context) -> None:
    print("executed step_1")

Dynamic rate limit

A dynamic rate limit uses a CEL expression for the key, evaluated against the task’s input at runtime. This lets you enforce a separate quota for each user, tenant, or any other dimension present in the input. When using a dynamic key, you must also supply limit so Hatchet knows the quota for keys it has never seen before.
worker.py
from pydantic import BaseModel
from hatchet_sdk import Context, Hatchet
from hatchet_sdk.rate_limit import RateLimit, RateLimitDuration

hatchet = Hatchet(debug=True)

class RateLimitInput(BaseModel):
    user_id: str

rate_limit_workflow = hatchet.workflow(
    name="RateLimitWorkflow", input_validator=RateLimitInput
)

@rate_limit_workflow.task(
    rate_limits=[
        RateLimit(
            dynamic_key="input.user_id",
            units=1,
            limit=10,
            duration=RateLimitDuration.MINUTE,
        )
    ]
)
def step_2(input: RateLimitInput, ctx: Context) -> None:
    print("executed step_2")
Both units and limit also accept CEL expressions when you need them to vary per run:
dynamic.py
from pydantic import BaseModel
from hatchet_sdk import Context, Hatchet
from hatchet_sdk.rate_limit import RateLimit

hatchet = Hatchet(debug=True)

class DynamicRateLimitInput(BaseModel):
    group: str
    units: int
    limit: int

dynamic_rate_limit_workflow = hatchet.workflow(
    name="DynamicRateLimitWorkflow", input_validator=DynamicRateLimitInput
)

@dynamic_rate_limit_workflow.task(
    rate_limits=[
        RateLimit(
            dynamic_key='"LIMIT:"+input.group',
            units="input.units",
            limit="input.limit",
        )
    ]
)
def step1(input: DynamicRateLimitInput, ctx: Context) -> None:
    print("executed step1")

Multiple rate limits on one task

You can stack several RateLimit entries on a single task. Hatchet checks all of them before dispatching the run — the task waits until every limit has capacity.
worker.py
from hatchet_sdk.rate_limit import RateLimit, RateLimitDuration

@workflow.task(
    rate_limits=[
        RateLimit(static_key="global-api-budget", units=1),
        RateLimit(
            dynamic_key="input.user_id",
            units=1,
            limit=10,
            duration=RateLimitDuration.MINUTE,
        ),
    ]
)
def call_api(input, ctx):
    ...

RateLimit parameter reference

static_key
string
A fixed string that identifies the rate limit bucket. All runs of this task share the same bucket. You must register the key and its quota with hatchet.rate_limits.put() before workers start. Mutually exclusive with dynamic_key.
dynamic_key
string
A CEL expression evaluated against the task input at runtime. Each unique result is treated as an independent bucket. For example, "input.user_id" creates a separate limit per user. Requires limit to also be set. Mutually exclusive with static_key.
units
int | string
default:"1"
How many units of quota this run consumes. Can also be a CEL expression string (e.g. "input.units") to derive the cost from the task input.
limit
int | string
The quota ceiling for a dynamic rate limit bucket. Required when dynamic_key is set. Can be an integer or a CEL expression string (e.g. "input.limit").
duration
RateLimitDuration
default:"MINUTE"
The length of the rate limit window. Available values: SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, YEAR.

Next steps

Concurrency limits

Cap how many runs execute simultaneously.

Scheduling

Run tasks on cron schedules or at a specific future time.

Build docs developers (and LLMs) love