Xinglin Qiang
11/30/2025, 3:16 AMMarvin
11/30/2025, 3:16 AMMarvin
11/30/2025, 3:22 AMserve(my_task) — not in your FastAPI process/thread
What you get back
- A PrefectDistributedFuture that exposes:
- `task_run_id`: UUID for tracking
- .state / .wait() / .result() for status and results
How to implement with FastAPI
1) Define and serve the task
- Run this worker as a separate process/container. Ensure it shares result storage with your API (e.g., same mounted volume or remote storage like S3).
from prefect import task
from prefect.task_worker import serve
@task(log_prints=True)
def long_job(payload: dict) -> dict:
import time
print("Starting job")
time.sleep(10)
return {"ok": True, "size": len(payload)}
if __name__ == "__main__":
# Concurrency optional via limit=
serve(long_job)
2) Submit from FastAPI and poll status/results
from fastapi import FastAPI
from uuid import UUID
from prefect.client.orchestration import get_client
from app.tasks import long_job # import the task above
app = FastAPI()
@app.post("/jobs", status_code=202)
async def start_job(payload: dict):
f = long_job.delay(payload)
return {"task_run_id": str(f.task_run_id)}
@app.get("/jobs/{task_run_id}")
async def get_job(task_run_id: UUID):
async with get_client() as client:
tr = await client.read_task_run(task_run_id)
state = tr.state
resp = {
"id": str(task_run_id),
"state": (state.type.value if state else "PENDING"),
"name": (state.name if state else None),
"message": (state.message if state else None),
}
if state and state.is_completed():
try:
resp["result"] = state.result(_sync=True)
except Exception as e:
resp["result_error"] = str(e)
elif state and state.is_failed():
try:
resp["error"] = str(state.result(_sync=True))
except Exception:
resp["error"] = state.message
return resp
3) Fetch logs for a task run
- Logs are visible in the Prefect UI and also programmatically via the client. Make sure you use get_run_logger() or set log_prints=True to capture prints.
from uuid import UUID
from prefect.client.orchestration import get_client
from prefect.client.schemas.filters import FilterSet, LogFilter, LogFilterTaskRunId
@app.get("/jobs/{task_run_id}/logs")
async def get_logs(task_run_id: UUID, limit: int = 200, offset: int = 0):
async with get_client() as client:
filters = FilterSet(
logs=LogFilter(task_run_id=LogFilterTaskRunId(any_=[task_run_id]))
)
logs = await client.read_logs(filter=filters, limit=limit, offset=offset)
return [
{
"timestamp": str(log.timestamp),
"level": log.level,
"message": log.message,
}
for log in logs
]
When to choose a flow instead
- Use a flow (and typically a deployment) when you have:
- Multiple steps/tasks with dependenciesMarvin
11/30/2025, 3:22 AMrun_deployment(...) and track the returned flow_run_id; you can then inspect both the flow run and its underlying task runs.
- See run_deployment API
Notes for production
- Ensure your API and Task Worker share result storage so results/parameters can be retrieved:
- For containers, mount the same PREFECT_LOCAL_STORAGE_PATH
- Or configure remote result storage (e.g., S3/GCS)
- Control worker concurrency via serve(..., limit=N)
- Prefer get_run_logger() inside tasks for structured logs
Docs and references
- Background tasks quickstart: Run background tasks
- Full FastAPI + background tasks example: Deploy a web app powered by background tasks
- Task Worker API reference: prefect.task_worker
- Futures API: prefect.futures
- Client orchestration API: prefect.client.orchestration
- REST logs endpoint: Read logs
Recommendation for your case
- For “invoke long-running logic from FastAPI and query state/logs/result,” use task.delay() + a Task Worker. It’s the simplest, most direct pattern for API-driven background jobs with full observability.
If you share a bit about your deployment environment (single container vs multiple, Cloud vs local, where you want to store results), I can tailor the storage and worker setup exactly to your needs.Xinglin Qiang
11/30/2025, 3:29 AMflow + tasks or just tasks? Furthermore, I'd like to track of the progress, like how many entities have beed processed, how many success and failed. @MarvinMarvin
11/30/2025, 3:30 AMMarvin
11/30/2025, 3:35 AMfrom prefect import flow, task
from prefect.futures import wait
from prefect.artifacts import create_progress_artifact, update_progress_artifact, create_table_artifact
from prefect.task_runners import ThreadPoolTaskRunner
@task(retries=2, retry_delay_seconds=2, log_prints=True)
def process_entity(e):
# your chained API calls here
return {"id": e["id"], "status": "success"}
@flow(task_runner=ThreadPoolTaskRunner(max_workers=20))
def process_all(entities: list[dict]):
total = len(entities)
progress_id = create_progress_artifact(
progress=0.0,
key="batch-progress",
description=f"Processing {total} entities"
)
futures = process_entity.map(entities)
# As they complete, compute counts
done, _ = wait(futures)
successes = [f for f in done if f.state.is_completed()]
failures = [f for f in done if f.state.is_failed()]
update_progress_artifact(
artifact_id=progress_id,
progress=100.0,
description=f"Completed: {len(successes)} success, {len(failures)} failed"
)
# Optional: detailed results table
create_table_artifact(
table=[{"id": f.result()["id"], "status": "success"} for f in successes]
+ [{"id": entities[i].get("id"), "status": "failed"} for i, f in enumerate(done) if f.state.is_failed()],
key="batch-results",
description="Per-entity outcomes"
)
return {
"total": total,
"successful": len(successes),
"failed": len(failures),
}
Pattern B: Single background task that iterates and emits progress (simplest FastAPI integration)
```
from prefect import task
from prefect.task_worker import serve
from prefect.artifacts import create_progress_artifact, update_progress_artifact, create_table_artifact
@task(log_prints=True)
def process_batch(entities: list[dict], job_id: str):
total = len(entities)
progress_id = create_progress_artifact(
progress=0.0,
key=f"job-{job_id}-progress",
description=f"Starting job {job_id} with {total} entities"
)
successes, failures = [], []
for i, e in enumerate(entities, start=1):
try:
# your chained API calls here
successes.append({"id": e["id"], "status": "success"})
except Exception as exc:
failures.append({"id": e["id"], "status": "failed", "error": str(exc)})
update_progress_artifact(
artifact_id=progress_id,
progress=(i / total) * 100.0,
description=f"{i}/{total} processed | {len(successes)} success, {len(failures)} failed"
)
create_table_artifact(
table=successes + failures,
key=f"job-{job_id}-results",Marvin
11/30/2025, 3:35 AMFastAPI endpoints for submission and status
- With Pattern B, submit via `process_batch.delay(...)` and track with the returned task_run_id. Poll state and artifacts for progress and final counts.
- With Pattern A, you can either call the flow directly from the API (if acceptable) or deploy it and trigger with `run_deployment`, then track by flow_run_id.
Helpful client snippets
- Read task run or flow run state/result in your API:
from uuid import UUID
from prefect.client.orchestration import get_client
async def read_task_status(task_run_id: UUID):
async with get_client() as client:
tr = await client.read_task_run(task_run_id)
st = tr.state
if not st:
return {"state": "PENDING"}
if st.is_completed():
return {"state": "COMPLETED", "result": st.result(_sync=True)}
if st.is_failed():
return {"state": "FAILED", "message": st.message}
return {"state": st.type.value}
- Read logs for a given task run:
from prefect.client.schemas.filters import FilterSet, LogFilter, LogFilterTaskRunId
async def read_task_logs(task_run_id: UUID, limit=200, offset=0):
async with get_client() as client:
fs = FilterSet(logs=LogFilter(task_run_id=LogFilterTaskRunId(any_=[task_run_id])))
return await client.read_logs(filter=fs, limit=limit, offset=offset)
```
Docs to dive deeper
- Run work concurrently (map/submit): Run work concurrently
- Background tasks with FastAPI and artifacts: Deploy a web app powered by background tasks
- Progress artifacts API: prefect.artifacts
- Client orchestration API: prefect.client.orchestration
If you share your constraints (need for parallelism, desired throughput, infra limits), I can recommend a concrete pattern and wire up the exact FastAPI + Prefect code you’ll need.Xinglin Qiang
11/30/2025, 3:40 AMMarvin
11/30/2025, 3:40 AMMarvin
11/30/2025, 3:41 AMfrom prefect import flow, task
from prefect.futures import as_completed
from prefect.artifacts import create_progress_artifact, update_progress_artifact
from prefect.task_runners import ThreadPoolTaskRunner
from prefect.runtime import flow_run
@task(retries=2, retry_delay_seconds=2, log_prints=True)
def process_entity(e):
# your chained API calls here
return {"id": e["id"], "status": "success"}
@flow(task_runner=ThreadPoolTaskRunner(max_workers=20))
def process_all(entities: list[dict]):
total = len(entities)
prog_key = f"batch-{flow_run.id}-progress"
# Create a single progress artifact we update as tasks finish
prog_id = create_progress_artifact(
progress=0.0,
key=prog_key,
description=f"Processing {total} entities"
)
# Fan out
futures = process_entity.map(entities)
# Incremental progress as futures complete
completed = 0
success = 0
failed = 0
for fut in as_completed(futures):
completed += 1
if fut.state.is_completed():
success += 1
else:
failed += 1
pct = (completed / total) * 100.0
update_progress_artifact(
artifact_id=prog_id,
progress=pct,
description=f"{completed}/{total} processed | {success} succeeded, {failed} failed"
)
# Optionally return final counts
return {"total": total, "successful": success, "failed": failed, "progress_artifact_key": prog_key}
Notes
- This keeps progress accurate without each task needing to know the total or fight over updates.
- Your frontend can poll the artifact by its key (prog_key) to render a progress bar and counts.
- If you want more frequent updates (e.g., partial stages inside each entity), you can also create per-entity artifacts or push intermediate logs, but the above is the simplest aggregate progress.
If you prefer a single background task (Pattern B), you can still increment progress for every entity inside that task; but you’ll lose per-entity fault isolation and parallelism.
Helpful docs
- Futures (as_completed): prefect.futures
- Progress artifacts: prefect.artifacts
If you want, share how you’ll poll progress (Prefect API vs your own DB) and I’ll show the exact read_artifacts filter to match your keys and return counts efficiently.Xinglin Qiang
11/30/2025, 3:47 AMMarvin
11/30/2025, 3:47 AMMarvin
11/30/2025, 3:48 AMfrom prefect.client.orchestration import get_client
from prefect.client.schemas.filters import FilterSet, LogFilter, LogFilterFlowRunId
async with get_client() as client:
filters = FilterSet(
logs=LogFilter(flow_run_id=LogFilterFlowRunId(any_=[flow_run_id]))
)
logs = await client.read_logs(filter=filters, limit=1000, offset=0)
- Only one task’s logs:
from prefect.client.schemas.filters import FilterSet, LogFilter, LogFilterTaskRunId
filters = FilterSet(
logs=LogFilter(task_run_id=LogFilterTaskRunId(any_=[task_run_id]))
)
logs = await client.read_logs(filter=filters, limit=1000, offset=0)
Notes
- Task logs include both flow_run_id and task_run_id; flow-only logs include only flow_run_id.
- To see prints in logs, set log_prints=True on your tasks/flow or use get_run_logger().
- For background tasks (task.delay without a flow), logs will have a task_run_id but no flow_run_id.
Docs
- Read logs via API: REST: read logs
- Client orchestration API: Python clientXinglin Qiang
11/30/2025, 3:57 AMMarvin
11/30/2025, 3:57 AMMarvin
11/30/2025, 3:59 AMhttpx.Client from the stored cookies.
- On 401, acquire a distributed lock so only one task refreshes the session and writes new cookies. Other tasks wait, then retry with the new cookies.
Sketch
# Pseudocode – use your own Redis/DB client
SESSION_KEY = "api_session_cookies"
LOCK_KEY = "api_session_refresh_lock"
def load_cookies():
raw = redis.get(SESSION_KEY)
return httpx.Cookies() if not raw else httpx.Cookies( cookies=json.loads(raw) )
def save_cookies(cookies: httpx.Cookies):
redis.set(SESSION_KEY, json.dumps(list(cookies.jar)), ex=3600)
def build_client_from_store() -> httpx.Client:
cookies = load_cookies()
return httpx.Client(base_url=BASE_URL, cookies=cookies, timeout=30)
def perform_login() -> httpx.Client:
c = httpx.Client(base_url=BASE_URL, timeout=30)
# ... do login, set cookies ...
return c
@task(retries=0)
def process_entity(e):
def do_call(client):
return client.get(f"/entities/{e['id']}")
client = build_client_from_store()
r = do_call(client)
if r.status_code != 401:
return r.json()
# 401 → attempt coordinated refresh
with redis.lock(LOCK_KEY, timeout=60): # ensure only one task refreshes
# Re-check in case another task already refreshed
client = build_client_from_store()
r = do_call(client)
if r.status_code == 401:
# Still unauthorized → we refresh
client = perform_login()
save_cookies(client.cookies)
# Retry original call with refreshed cookies
client = build_client_from_store()
r = do_call(client)
r.raise_for_status()
return r.json()
Notes
- Represent cookies in a portable format (e.g., a dict/list or a “Cookie” header string) when storing.
- Use a distributed lock (e.g., Redis lock). Prefect’s built-in memory/filesystem locks won’t help across machines.
2) Chunked fan‑out to reduce contention
- Split the 200 entities into N chunks.
- Each task processes its chunk with its own client/session and manages its own refresh on 401.
- You still get parallelism, but you don’t need global cookie sharing.
3) Single owner of the session inside one long-running background task
- Run Pattern B (a single background task via .delay()), create one httpx.Client, and iterate all entities inside that task.
- You can still do internal concurrency with httpx.AsyncClient + a semaphore, but it’s all within the same task/process, so one session is easy to manage.
- Trade-off: you lose per-entity task states.
What not to do
- Don’t pass httpx.Client as a task parameter or rely on global variables for it; it will break when you change runners or introduce distribution.
If you share how you’re running (single machine vs multiple workers, desired parallelism), I can tailor one of the patterns with concrete code (e.g., Redis-backed cookie store + lock, or an async single-owner task).Xinglin Qiang
11/30/2025, 4:11 AMMarvin
11/30/2025, 4:11 AMMarvin
11/30/2025, 4:13 AMpip install prefect httpx redis
- Environment: REDIS_URL, API_BASE_URL, API_USERNAME, API_PASSWORD
Code
```
import os
import json
import httpx
import redis
from typing import Any
from prefect import flow, task, get_run_logger
from prefect.futures import as_completed
from prefect.task_runners import ThreadPoolTaskRunner
from prefect.artifacts import create_progress_artifact, update_progress_artifact
from prefect.runtime import flow_run
# ------------------------------------------------------------------------------
# Configuration
# ------------------------------------------------------------------------------
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")
BASE_URL = os.getenv("API_BASE_URL", "https://api.example.com")
LOGIN_USERNAME = os.getenv("API_USERNAME")
LOGIN_PASSWORD = os.getenv("API_PASSWORD")
SESSION_KEY = "api_session_cookies" # Redis key to store cookies JSON
LOCK_KEY = "api_session_refresh_lock" # Redis lock key for session refresh
LOCK_TIMEOUT = 60 # Seconds the lock is held
LOCK_BLOCKING_TIMEOUT = 30 # Seconds tasks will wait to acquire lock
# ------------------------------------------------------------------------------
# Redis helpers
# ------------------------------------------------------------------------------
def get_redis() -> redis.Redis:
# decode_responses=True so we get strings back and can json.dumps/loads easily
return redis.Redis.from_url(REDIS_URL, decode_responses=True)
# ------------------------------------------------------------------------------
# Cookie (de)serialization helpers
# ------------------------------------------------------------------------------
def serialize_cookies(cookies: httpx.Cookies) -> str:
# Convert CookieJar to a JSON-serializable list of dicts
data = []
for c in cookies.jar:
data.append(
{
"name": c.name,
"value": c.value,
"domain": c.domain,
"path": c.path,
"expires": c.expires,
"secure": bool(c.secure),
}
)
return json.dumps(data)
def deserialize_cookies(raw: str | None) -> httpx.Cookies:
jar = httpx.Cookies()
if not raw:
return jar
for c in json.loads(raw):
# domain/path are optional for client-side cookies; include if present
jar.set(
c["name"],
c["value"],
domain=c.get("domain"),
path=c.get("path"),
)
return jar
def load_cookies() -> httpx.Cookies:
r = get_redis()
raw = r.get(SESSION_KEY)
return deserialize_cookies(raw)
def save_cookies(cookies: httpx.Cookies, ttl_seconds: int | None = None) -> None:
r = get_redis()
payload = serialize_cookies(cookies)
if ttl_seconds:
r.setex(SESSION_KEY, ttl_seconds, payload)
else:
r.set(SESSION_KEY, payload)
# ------------------------------------------------------------------------------
# HTTP client + login
# ------------------------------------------------------------------------------
def build_client_from_store() -> httpx.Client:
return httpx.Client(base_url=BASE_URL, timeout=30, cookies=load_cookies())
def login_and_get_client() -> httpx.Client:
"""
Perform a fresh login and persist the resulting cookies to Redis.
Replace the login endpoint/payload with your real auth flow.
"""
if not LOGIN_USERNAME or not LOGIN_PASSWORD:Marvin
11/30/2025, 4:13 AMMarvin
11/30/2025, 4:13 AMprocess_all_entities(entities) where entities is a list of dicts like {"id": "123", ...}.
- Your frontend can poll the single progress artifact by key batch-{flow_run_id}-progress and show counts.
- Concurrency is capped at 10 via ThreadPoolTaskRunner(max_workers=10).
Notes
- Each task uses its own httpx.Client instance; no clients are shared across tasks.
- Only one task performs a login refresh on 401, guarded by a Redis lock; others wait and then reuse the refreshed cookies.
- You can adjust `LOCK_TIMEOUT`/`LOCK_BLOCKING_TIMEOUT` based on how long a login may take.
Docs
- Run work concurrently (map/as_completed): Run work concurrently
- Artifacts (progress, tables, markdown): prefect.artifacts
If you want to add a pre-check to keep the session fresh before a burst (e.g., ensure_session()), or switch to async with httpx.AsyncClient, I can adapt the template accordingly.Xinglin Qiang
11/30/2025, 4:17 AMMarvin
11/30/2025, 4:17 AMMarvin
11/30/2025, 4:18 AMasync with prefect.concurrency.asyncio.concurrency("entity-processing").
Code
```
import os
import json
import httpx
import redis.asyncio as aioredis
from typing import Any
from prefect import flow, task, get_run_logger
from prefect.futures import as_completed
from prefect.task_runners import PrefectTaskRunner
from prefect.artifacts import acreate_progress_artifact, aupdate_progress_artifact
from prefect.runtime import flow_run
from prefect.concurrency.asyncio import concurrency # requires a configured concurrency limit
# ------------------------------------------------------------------------------
# Configuration
# ------------------------------------------------------------------------------
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")
BASE_URL = os.getenv("API_BASE_URL", "https://api.example.com")
LOGIN_USERNAME = os.getenv("API_USERNAME")
LOGIN_PASSWORD = os.getenv("API_PASSWORD")
SESSION_KEY = "api_session_cookies" # Redis key to store cookies JSON
LOCK_KEY = "api_session_refresh_lock" # Redis lock key for session refresh
LOCK_TIMEOUT = 60 # seconds the lock is held server-side
LOCK_BLOCKING_TIMEOUT = 30 # seconds tasks will wait to acquire lock
CONCURRENCY_LIMIT_NAME = "entity-processing" # define this in Prefect UI, limit=10
# ------------------------------------------------------------------------------
# Redis helpers (async)
# ------------------------------------------------------------------------------
def get_redis() -> aioredis.Redis:
# decode_responses=True to read/write strings
return aioredis.from_url(REDIS_URL, decode_responses=True)
# ------------------------------------------------------------------------------
# Cookie (de)serialization helpers
# ------------------------------------------------------------------------------
def serialize_cookies(cookies: httpx.Cookies) -> str:
data = []
for c in cookies.jar:
data.append(
{
"name": c.name,
"value": c.value,
"domain": c.domain,
"path": c.path,
"expires": c.expires,
"secure": bool(c.secure),
}
)
return json.dumps(data)
def deserialize_cookies(raw: str | None) -> httpx.Cookies:
jar = httpx.Cookies()
if not raw:
return jar
for c in json.loads(raw):
jar.set(c["name"], c["value"], domain=c.get("domain"), path=c.get("path"))
return jar
async def load_cookies() -> httpx.Cookies:
r = get_redis()
raw = await r.get(SESSION_KEY)
return deserialize_cookies(raw)
async def save_cookies(cookies: httpx.Cookies, ttl_seconds: int | None = None) -> None:
r = get_redis()
payload = serialize_cookies(cookies)
if ttl_seconds:
await r.setex(SESSION_KEY, ttl_seconds, payload)
else:
await r.set(SESSION_KEY, payload)
# ------------------------------------------------------------------------------
# HTTP client + login (async)
# ------------------------------------------------------------------------------
async def build_client_from_store() -> httpx.AsyncClient:
return httpx.AsyncClient(base_url=BASE_URL, timeout=30, cookies=await load_cookies())
async def login_and_get_client() -> httpx.AsyncClient:
"""
Perform a fresh login and persist cookies to Redis.Marvin
11/30/2025, 4:18 AMMarvin
11/30/2025, 4:18 AMasync with concurrency("entity-processing") manager. Be sure you’ve created a concurrency limit named entity-processing with a value of 10 in the Prefect UI.
- This approach works regardless of task runner and guarantees no more than 10 concurrent tasks will execute the body at the same time.
Why not use a shared AsyncClient?
- Prefect tasks can run in different threads/contexts; sharing a live client is brittle. Rebuilding a client from a shared cookie store keeps things safe and deterministic, and the Redis lock ensures only one refresh occurs on 401s.
Related docs
- Run work concurrently: Run work concurrently
- Artifacts (async functions like acreate_progress_artifact): prefect.artifacts
If you want me to add a minimal endpoint that triggers this flow and a progress endpoint that polls the progress artifact by key, I can drop that in too.Xinglin Qiang
11/30/2025, 8:02 AMMarvin
11/30/2025, 8:02 AMMarvin
11/30/2025, 8:05 AM# serve_flows.py
from prefect import flow, serve
# import your flows here
from my_project.flows import flow_a, flow_b, flow_c
if __name__ == "__main__":
dep_a = flow_a.to_deployment(name="flow-a")
dep_b = flow_b.to_deployment(name="flow-b")
dep_c = flow_c.to_deployment(name="flow-c")
# Limit = max concurrent flow run subprocesses on this machine
serve(dep_a, dep_b, dep_c, limit=8, pause_on_shutdown=False)
FastAPI endpoint to trigger a flow
# app.py
from fastapi import FastAPI, HTTPException
from prefect.deployments import run_deployment
from prefect.client.orchestration import get_client
from uuid import UUID
app = FastAPI()
@app.post("/flows/flow-a/trigger")
async def trigger_flow_a(payload: dict):
try:
fr = await run_deployment("flow_a/flow-a", parameters=payload, timeout=0)
return {"flow_run_id": str(fr.id)}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/flows/status/{flow_run_id}")
async def flow_status(flow_run_id: str):
async with get_client() as client:
fr = await client.read_flow_run(UUID(flow_run_id))
state = fr.state
return {
"flow_run_id": str(fr.id),
"state": state.type if state else None,
"name": state.name if state else None,
"message": state.message if state else None,
"completed": state.is_completed() if state else False,
}
Containerization notes
- Run the serve script and your API in the same container, or separate containers, both pointed at the same Prefect API.
- Example CMD to run both in one container:
CMD python serve_flows.py & uvicorn app:app --host 0.0.0.0 --port 8000
- Set PREFECT_API_URL in the container env to your Prefect Server/Cloud.
When to pick this
- You’re on one machine, prefer simplicity, and don’t need dynamic infra. Each flow run is isolated in its own subprocess; limit=8 is a good starting point for a 12‑core host.
Option 2: Deploy to a work pool and run a worker (more scalable)
- Use flow.deploy(..., work_pool_name="...") to register deployments that run on a worker you start.
- Start a worker: prefect worker start --pool my-process-pool
- Your FastAPI endpoint still uses run_deployment(...). This makes it easier to scale later or move to Docker/K8s workers, but it’s more setup than serve().
Which should you use?
- For your current setup (single host, Dockerized API), Option 1 with serve(...) is the quickest and most reliable. You’ll get:
- Non-blocking trigger from your API via run_deployment
- Observability in the UI
- Concurrency control via serve(limit=...)
- Subprocess isolation for each flow run
Tips
- Align serve(limit=...) with your workload. If flows are CPU-heavy, try 6–8. If mostly I/O-bound, you can go higher.
- If your flows themselves fan out to tasks with map/submit, their own task_runner concurrency will further consume cores/threads. Monitor CPU and adjust.
- Add tags on to_deployment for routing and filtering.
- You can serve multiple flows together; they share the same limit across the single Runner.
Docs
- Serve flows locally: Serve flowsMarvin
11/30/2025, 8:05 AM