Dom
10/20/2025, 6:54 AMMarvin
10/20/2025, 6:54 AMMarvin
10/20/2025, 6:57 AM@flow and @task functions locally like normal Python
- Concurrency via task runners (threads by default, or processes)
- Retries, timeouts, caching, and result persistence to local storage
- Logging via get_run_logger() (to your console)
- Composing “pipelines” by calling tasks from flows and using subflows
What you won’t get without a backend
- No UI/dashboard, run history, or central log storage
- No scheduled runs, workers, or deployments
- No global concurrency limits, automations, or event processing
- Retries only occur in the same process/session (not across restarts)
A note on “pipeline”
- In Prefect 3 there’s no separate “pipeline” object. You model pipelines with a @flow (optionally calling other flows as subflows) composed of `@task`s.
About REST APIs
- If you aren’t running Prefect Cloud/Server, Prefect’s REST API is not available/needed.
- To integrate with your own REST services, just call them from tasks using `httpx`/`requests`. If you want to expose your flow as an HTTP endpoint, wrap it with a small FastAPI/Flask app that calls the flow.
Basic local-only setup
- Make sure no API is configured by leaving PREFECT_API_URL unset or setting it to an empty string.
- Optionally enable local result persistence and tweak logging.
Example using your own functions/private packages as tasks
python
import os
# Ensure local-only (no API calls)
os.environ["PREFECT_API_URL"] = ""
from prefect import flow, task, get_run_logger
from prefect.task_runners import ThreadPoolTaskRunner
# Imagine this comes from your private package
from my_private_pkg.etl import fetch_raw, transform, load
@task(retries=2, retry_delay_seconds=1, log_prints=True, persist_result=True)
def fetch(url: str):
return fetch_raw(url)
@task(persist_result=True)
def transform_records(data):
return transform(data)
@task
def load_records(rows):
load(rows)
@flow(task_runner=ThreadPoolTaskRunner(max_workers=4), log_prints=True)
def etl_pipeline(urls: list[str]):
logger = get_run_logger()
<http://logger.info|logger.info>(f"Starting ETL for {len(urls)} urls")
raw_futs = [fetch.submit(u) for u in urls]
xformed = [transform_records.submit(f) for f in raw_futs]
# Resolve futures before load
rows = [x.result() for x in xformed]
load_records(rows)
return len(rows)
if __name__ == "__main__":
count = etl_pipeline(["<https://api.example.com/a>", "<https://api.example.com/b>"]).result()
print(f"Processed {count} batches")
Calling an external REST API from a task
python
from prefect import task
@task(retries=3, retry_delay_seconds=2)
def call_service(url: str):
import httpx
with httpx.Client(timeout=30) as client:
r = client.get(url)
r.raise_for_status()
return r.json()
Optional environment tweaks for local mode
bash
# Disable Cloud/Server communication
export PREFECT_API_URL=""
# Persist results locally (defaults to ~/.prefect/storage)
export PREFECT_RESULTS_PERSIST_BY_DEFAULT=true
export PREFECT_TASKS_DEFAULT_PERSIST_RESULT=true
# Optional custom path
# export PREFECT_LOCAL_STORAGE_PATH=/tmp/prefect-storage
Common gotchas
- Scheduling/deployments/workers require a server/Cloud; purely local runs are “you run the script when you want it to run.”
- With ProcessPoolTaskRunner, your tasks must be importable/pickleable.
- Without a backend, logs and states are ephemeral to the current process; if you need history, persist results to files/object storage yourself or adopt Prefect Cloud/Server later.
Docs to dig deeper
- Quickstart: Quickstart
- Logging: Add loggingMarvin
10/20/2025, 6:57 AMDom
10/20/2025, 7:45 AMDom
10/20/2025, 7:45 AMMarvin
10/20/2025, 7:45 AMMarvin
10/20/2025, 7:47 AMpython
from prefect import task
@task(
retries=0, # no retries
retry_delay_seconds=None, # no retry schedule
timeout_seconds=None, # no timeout
cache_policy=None, # disable cross-call caching
cache_key_fn=None, # ensure no custom cache key
cache_expiration=None, # no cache expiration since caching is off
cache_result_in_memory=False,# no in-memory result cache
persist_result=False, # do not write results to storage
result_storage=None # ensure no storage is configured
)
def my_task(x):
return x
Per-flow
python
from prefect import flow
@flow(
retries=0, # no retries
retry_delay_seconds=None,
timeout_seconds=None, # no timeout
cache_policy=None, # disable flow-level caching
cache_key_fn=None,
cache_expiration=None,
persist_result=False, # do not persist flow result
result_storage=None
)
def my_flow(x):
return my_task(x)
Notes and gotchas
- Retries: default is effectively “no retries.” Setting retries=0 makes that explicit.
- Timeouts: default is None (disabled). Only set this if you’ve previously enabled a timeout or want to be explicit.
- Caching:
- Disable cross-call caching with cache_policy=None.
- Also set cache_result_in_memory=False on tasks to avoid in-memory result caching during a run.
- Result persistence: set persist_result=False and leave result_storage=None to prevent writing to local storage.
- If you’ve enabled any global defaults via settings or a profile, make sure those are not forcing persistence or caching. For example:
bash
# Make sure these are unset or not set to "true"
unset PREFECT_RESULTS_PERSIST_BY_DEFAULT
unset PREFECT_TASKS_DEFAULT_PERSIST_RESULT
Docs for reference
- Caching: Caching
- Results/persistence: Results
- Settings: Settings reference
If you share a small snippet of your current decorators, I can suggest the minimal changes needed to disable each behavior.Dom
10/20/2025, 7:49 AMMarvin
10/20/2025, 7:49 AMMarvin
10/20/2025, 7:50 AM@task(retries=..., retry_delay_seconds=...).
- Flow retries are set on the flow with @flow(retries=..., retry_delay_seconds=...) and only control retrying the entire flow run.
- If a task ultimately fails (after its own retries, if any), the flow fails. If the flow has retries, Prefect re-runs the whole flow from the beginning.
- Subflows behave the same: each subflow’s retries are independent. Re-running the parent flow will call the subflow again.
Caching/persistence note
- On a flow retry, tasks will re-run unless you’ve enabled a cache policy and result persistence. With caching + persisted results, successful work can be skipped on the next attempt. Without it, everything re-executes.
Tiny example
python
from prefect import flow, task
@task(retries=0) # task will not retry
def flaky():
raise RuntimeError("boom")
@flow(retries=2, retry_delay_seconds=3) # flow will retry the entire run up to 2 times
def my_flow():
flaky()
if __name__ == "__main__":
my_flow()
Docs:
- Flows and tasks overview: Quickstart
- Caching: Caching
- Results/persistence: Results
If you want to fully disable retries/timeouts/caching/persistence, set them explicitly on each decorator as in my prior message.Dom
10/20/2025, 7:50 AMretries=0, # no retries
retry_delay_seconds=None, # no retry schedule
timeout_seconds=None, # no timeout
cache_policy=None, # disable cross-call caching
cache_key_fn=None, # ensure no custom cache key
cache_expiration=None, # no cache expiration since caching is off
cache_result_in_memory=False,# no in-memory result cache
persist_result=False, # do not write results to storage
result_storage=None # ensure no storage is configuredMarvin
10/20/2025, 7:50 AMMarvin
10/20/2025, 7:54 AMDom
10/20/2025, 7:57 AMMarvin
10/20/2025, 7:57 AMMarvin
10/20/2025, 7:58 AMMarvin
10/20/2025, 7:58 AMcache_result_in_memory=False and persist_result=False, Prefect will not keep an additional in-memory cache or write results to disk. There’s no extra duplication beyond your returned objects and any references you keep.
How to avoid “memory explosion”
- Don’t return large objects from tasks. Write big intermediates to disk/object storage/database and return a small handle (e.g., path/URL/ID).
- Stream instead of accumulating. Avoid building big lists of futures or results in the flow.
- Process items in a for-loop and let references go as soon as possible.
- Don’t store all results in a list unless they’re small.
- Turn off in-run caching for tasks:
- cache_policy=None
- cache_result_in_memory=False
- persist_result=False
- Cap concurrency to match your memory budget:
- @flow(task_runner=ThreadPoolTaskRunner(max_workers=N))
- Avoid logging huge payloads and keep log_prints=False for large data paths.
- Optionally del large Python objects once you’re done with them to help GC.
Minimal example pattern
python
from prefect import flow, task
from prefect.task_runners import ThreadPoolTaskRunner
@task(
retries=0,
timeout_seconds=None,
cache_policy=None,
cache_result_in_memory=False,
persist_result=False,
)
def preprocess_and_write(chunk) -> str:
# process then write big data to disk/object store
path = f"/tmp/out/{chunk['id']}.parquet"
# ... write the data ...
return path # small handle
@task
def consume(path: str) -> int:
# read small metadata or pass path downstream
# ... small work ...
return 1
@flow(task_runner=ThreadPoolTaskRunner(max_workers=4))
def pipeline(chunks):
for ch in chunks:
p = preprocess_and_write.submit(ch)
c = consume.submit(p)
# do not accumulate in lists; let references go per-iteration
_ = c.result()
If memory pressure is still tight, the most effective technique is to keep large data out of return values and pass handles instead. If you later want to push even more memory off the process, enabling result persistence to local disk for just the large steps (and returning references) is also a safe option, even without any server.
Docs:
- Caching: Caching
- Results/persistence: Results
- Task runners and concurrency: Task runnersDom
10/23/2025, 3:18 AMDom
10/23/2025, 3:18 AMMarvin
10/23/2025, 3:19 AMMarvin
10/23/2025, 3:19 AMDom
10/23/2025, 3:20 AM03:16:29.957 | INFO | prefect - Starting temporary server on <http://127.0.0.1:8954>
See <https://docs.prefect.io/v3/concepts/server#how-to-guides> for more information on running a dedicated Prefect server.
03:16:36.338 | INFO | Flow run 'purple-bear' - Beginning flow run 'purple-bear' for flow 'msme_credit_score_flow'
03:16:36.388 | INFO | Task run '_load_config-5b3' - Finished in state Completed()
I have not set prefect api and api-key for my service. Is that consume server resources massively?Marvin
10/23/2025, 3:20 AMMarvin
10/23/2025, 3:24 AMPREFECT_API_URL (and thus no API key) is configured, Prefect starts a local API on a random localhost port to track your flow/task state. Key points:
- It runs as a single local API subprocess with a small SQLite database in your Prefect home directory; CPU/memory usage is minimal and it shuts down when your flow/process ends.
- It’s intended for local dev and ad-hoc runs — not production scale.
- There’s no network exposure beyond localhost unless you explicitly configure it.
If you want to change this behavior:
- Use a persistent local server: run prefect server start and point your runs at it by setting PREFECT_API_URL to the URL the command prints.
- Use Prefect Cloud: set PREFECT_API_URL and PREFECT_API_KEY for your workspace.
- If you really don’t want Prefect to auto-start an ephemeral server, you can disable it via settings and run purely in “unconfigured” mode (you’ll lose orchestration features).
Docs:
- Overview and options: Prefect Server concepts
- Running a dedicated local server: Server CLI guideDom
10/23/2025, 3:29 AMMarvin
10/23/2025, 3:29 AMMarvin
10/23/2025, 3:29 AMPREFECT_LOGGING_TO_API_ENABLED=false
- Lower log volume: PREFECT_LOGGING_LEVEL=WARNING (or prune logging in tasks)
For heavier usage:
- Start a local persistent server: prefect server start, then set PREFECT_API_URL to the URL it prints.
- For high concurrency/throughput, point the server at PostgreSQL via PREFECT_SERVER_DATABASE_CONNECTION_URL for better write concurrency than SQLite.
Docs:
- Server overview: Prefect Server concepts
- Running a dedicated server: Server CLI guide
If you can share what “significant” means (lots of parallel tasks? very verbose logging? many scheduled runs?), I can recommend the best setup.Dom
10/23/2025, 3:41 AMMarvin
10/23/2025, 3:41 AMMarvin
10/23/2025, 3:43 AMprefect server start --host 0.0.0.0 --port 4200
- From the machine where you run flows and CLI:
- Test the API directly:
curl -s http://<your-host>:4200/api/hello
You should see a friendly hello response.
2) Point your CLI and flows to that API URL
- Set PREFECT_API_URL in the same environment where you run CLI and flows:
prefect config set PREFECT_API_URL=http://<your-host>:4200/api
- Verify:
prefect config view | grep PREFECT_API_URL
echo $PREFECT_API_URL
- When you run a flow, the logs should show “Connecting to Prefect server at http//<your host>4200/api”. If you see “Starting temporary server on http://127.0.0.1:…”, it’s still connecting to an ephemeral server.
3) Create a work pool on that server and verify it shows in the UI
- Example:
prefect work-pool create my-pool --type process
prefect work-pool ls
You should see my-pool in both the CLI and the UI. If you created it while pointed at an ephemeral server, it won’t persist or appear in your UI.
4) Start a worker for that pool
prefect worker start -p my-pool
5) Minimal flow run test
- In the same shell (so it uses the same PREFECT_API_URL):
from prefect import flow
@flow
def hello():
print("hi")
if __name__ == "__main__":
hello()
- You should see the flow run appear in the UI immediately.
If that still doesn’t work, could you share:
- The URL you use for the UI (e.g., http//<your host>4200)
- The output of:
prefect version
prefect config view | grep PREFECT_API_URL
- Any error you see when you run:
prefect work-pool create my-pool --type process
This will let me pinpoint whether it’s an API URL/profile mismatch or a server/reverse-proxy connectivity issue.
Docs:
- Server concepts: Self-hosted Prefect Server
- Work pools/workers: Work pools and workers
- Server CLI: Server CLI guideDom
10/23/2025, 7:19 AMMarvin
10/23/2025, 7:19 AMMarvin
10/23/2025, 7:20 AMprefect server start --host 0.0.0.0 --port 4200
- From the machine where you run flows/CLI:
curl -s http://<your-host>:4200/api/hello
You should get a hello response.
2) Point Prefect at your server (so it won’t try ephemeral)
- In the same shell where you run flows and CLI:
prefect config set PREFECT_API_URL=http://<your-host>:4200/api
- Verify:
prefect config view | grep PREFECT_API_URL
- Now create a work pool and start a worker:
prefect work-pool create my-pool --type process
prefect worker start -p my-pool
- Run a simple flow from the same shell and confirm it appears in the UI:
from prefect import flow
@flow
def hello():
print("hi")
if __name__ == "__main__":
hello()
If you really want to use the ephemeral server, try:
- Increase the startup timeout:
prefect config set PREFECT_SERVER_EPHEMERAL_STARTUP_TIMEOUT_SECONDS=60
- Or disable ephemeral entirely (runs will be “unconfigured” and not show in a UI):
prefect config set PREFECT_SERVER_EPHEMERAL_ENABLED=false
Common causes of ephemeral timeout:
- Constrained environments (Docker/CI) where the subprocess can’t bind to 127.0.0.1 quickly
- Very slow startup or high CPU pressure
- Security policies blocking local sockets
If it still fails, please share:
- The UI URL you’re using
- Output of:
prefect version
prefect config view | grep -E "PREFECT_API_URL|PREFECT_SERVER_EPHEMERAL"
curl -s http://<your-host>:4200/api/hello
- How you’re running flows (local, Docker, remote server)
Docs:
- Server overview: Self-hosted Prefect Server
- Server CLI: Server CLI guide
- Ephemeral server settings: Server how-to guides