<@ULVA73B9P> I'd like to limit concurrent executio...
# marvin-ai
j
@Marvin I'd like to limit concurrent executions of a specific task on each worker instance but without sharing a global concurrency limiter that affects other workers. I'm looking for something that functions similar to the
--limit
flag but at a task level rather than a flow level. Is there any open issues / discussions around this? I'd prefer not to have to implement my own local semaphore in the task.
m
thought for 683.8 seconds
Short answer: there isn’t a built‑in “per‑worker, per‑task” concurrency limit in Prefect 3.x today. Task concurrency limits are enforced via tags (or via global concurrency limits) and are global across all workers. The
worker --limit
flag is flow-run concurrency per worker, not task-level. What exists today - Tag-based task limits (global): Docs - Global concurrency limits: Docs - CLI to manage tag limits (verified):
Copy code
prefect concurrency-limit create <tag> <limit>
  prefect concurrency-limit ls
  prefect concurrency-limit inspect <tag>
  prefect concurrency-limit delete <tag>
Open issues / discussions - I couldn’t find an open issue/discussion specifically requesting “per-worker task concurrency.” If you’d like, I can open a feature request on your behalf, or you can use New issue. Practical workarounds (no local semaphore required) 1) Per-worker tags + tag limits - Give each worker a unique identifier (env var) and add a worker-specific tag to the task at submit time. Then create a concurrency limit for each worker’s tag. - Example:
Copy code
# Set per-worker ID via environment on each worker, e.g.
  # PREFECT_WORKER_ID=worker-a

  from prefect import flow, task
  from prefect.context import tags
  import os

  @task
  def critical_op(i: int):
      ...

  @flow
  def main():
      worker_id = os.getenv("PREFECT_WORKER_ID", "default")
      # Add a worker-scoped tag
      with tags(f"db-crit:{worker_id}"):
          for i in range(10):
              critical_op.submit(i)
Then set limits per worker:
Copy code
prefect concurrency-limit create db-crit:worker-a 2
  prefect concurrency-limit create db-crit:worker-b 2
Notes: - You can also do
critical_op.with_options(tags=[f"db-crit:{worker_id}"]).submit(...)
. - How to set the env var depends on worker type: - Process worker: set it in the shell/systemd before
prefect worker start
. - Kubernetes worker: add an env var in the work pool’s base job template. See work pools: Docs. 2) Global Concurrency Limits with a worker-specific key (still Prefect-managed, not a “local” semaphore) - If you want the limiting inside the task body while still using Prefect’s central leases:
Copy code
import os
  from prefect import task
  from prefect.concurrency.sync import concurrency

  @task
  def critical_op():
      key = f"db-crit:{os.getenv('PREFECT_WORKER_ID', 'default')}"
      with concurrency(key, occupy=1):
          # do restricted work
          ...
Then create per-worker limits for those keys:
Copy code
prefect concurrency-limit create db-crit:worker-a 2
  prefect concurrency-limit create db-crit:worker-b 2
Caveats - Both approaches still use Prefect’s orchestration for limits, but the scoping is achieved by using different tag/limit names per worker. There’s no native “scope to this worker instance” flag on concurrency limits today. If you share which worker type you’re on (process/Kubernetes/ECS), I can give you a ready-to-paste snippet to inject the worker ID env var. Also, happy to file a feature request for true per‑worker task limits if you’d like.
n
i'd suggest @Marvin’s first answer should use global concurrency limits and that'd be a first-class intended use of concurrency limits and not a workaround let me know if that doesn't work for some reason (you can use tags but they're just there for backwards compat and there's no strong reason to use them instead of GCLs)
m
thought for 65.9 seconds
Totally agree: use Global Concurrency Limits (GCLs) for this. They’re the first‑class mechanism in Prefect 3.x and not a workaround. For per‑worker, per‑task limits, the recommended pattern is: - Use a GCL key that includes the worker’s identity (so each worker gets its own independent limit) - Acquire the lease inside the task using Prefect’s server‑backed concurrency context manager (this is not a local semaphore) Example
Copy code
# Set on each worker (env var or job template)
# PREFECT_WORKER_ID=worker-a | worker-b | ...

from prefect import flow, task
from prefect.concurrency.sync import concurrency
import os, time

@task
def my_task(i: int):
    key = f"my-task:{os.getenv('PREFECT_WORKER_ID', 'default')}"
    # Server-backed lease; scoped by worker via key naming
    with concurrency(key, occupy=1):
        time.sleep(2)
        return i

@flow
def run():
    for i in range(10):
        my_task.submit(i)
Then create independent limits per worker:
Copy code
prefect gcl create my-task:worker-a 2
prefect gcl create my-task:worker-b 2
prefect gcl ls
prefect gcl inspect my-task:worker-a
Notes - This is fully orchestrated by Prefect (leases stored/managed server‑side), so you’re not implementing a local semaphore. - If you can’t inject a worker ID as an env var, tell me your worker type (process/k8s/ECS) and I’ll share a snippet to set it in your pool template. Docs - Global concurrency limits: https://docs.prefect.io/v3/concepts/global-concurrency-limits - CLI (verified):
prefect gcl --help
If you need a zero‑code alternative, tag-based limits still exist but are mainly for compatibility; GCLs are preferred going forward.
j
Heyy, ah yeah thanks I did consider that approach before coming here, just felt a little dirty for my use case as I've got a few pinch points in my flow where I'd want to drop to a limit of 1 to execute specific bins while keeping all other tasks as parallel as possible. I was hoping to be able to co-locate the limits for each task within the annotation for clarity and to avoid naming things but tbh maybe the global approach isn't as dirty as I originally thought. I'll give it a proper go when I get a chance. In theory a worker-level limit could be implemented in the task annotation right? The worker process would just need to keep track of a map of task def -> limiter instances which work in a similar way to the existing --limit flag. I'm guessing this is just something that hasn't really got enough use-case to justify the added complexity / effort yeah?
n
since • workers subscribe to work pools and optionally specific work queues • work pools and/or work queues can have their own concurrency limits im not sure what you'd be adding here, unless im missing something
i guess i didn't read as closely into "per worker" in your original ask as i could have
maybe im also confused by this
but at a task level
is this prefect "Task" or something else
💯 1
map of task def -> limiter instances
ah yea i need to read better
something that hasn't really got enough use-case to justify the added complexity / effort
i think short answer, yes
👍 1
j
So to give a little more context, this particular flow has to invoke some licensed binaries which means that we have a small pool of workers each with a licence. On each worker the binary can only have one process running at a time without it crashing. To navigate this currently we run the workers with
--limit=1
which is great but it does mean we're missing out on some possible concurrency on the workers for all the other setup and finalisation tasks. Ideally those 2 tasks in flow where we can't have multiple running at the same time are the only bits with a concurrency limit applied (individually) so we can run as many flows as we want across our worker pool instead of 1 per worker.
Appreciate its a fairly niche case, our other flows are much more sensible and use ECS push pools with global limits etc but unfortunately this one needs to use windows ;(
n
interesting! thanks for the context, i think even though its on the advanced side, the selective application of concurrency is pretty common in lifelike use cases and is likely worth an example (sourced from here) i'll try to back out a general example that maps onto this or would be happy to review one if you beat me to it
❤️ 1
j
Even more context, its a render farm. The rough flow of tasks is: 1. Download assets 2. Pre process / normalise assets 3. Business logic 4. Generate render project - There cannot be more than one of this running on a worker 5. Render the project - Multiple of these can run on a worker, but we want to limit to 2-3 to get good CPU utilisation while allowing other work to happen 6. Upload output As mentioned before we're currently limited to 1 flow run execution per worker to avoid problems during steps 4 & 5 but we could be also preparing / completing steps 1,2,3,6 for other flow runs while waiting on limits during those steps. So yeah, it's task-level concurrency limits but where the workers resources are the concern rather than external resources like a shared DB etc. Once again niche but could be an valid example for others.
n
what do we think? feel free to drop comments on the PR
❤️ 1
j
Ah cool, I think you've captured our problem / use-case perfectly there. Great job.
catjam 1
n
the GPU use case (where each worker has physical access to its own instance of some vertically scaled infra) has come up several times, will be good to have a thing to link! thanks for explaining your situation!
❤️ 1
this now exists in the real ™️ docs
👍 1