Gautam Menon
11/06/2025, 2:43 PMMarvin
11/06/2025, 2:43 PMMarvin
11/06/2025, 2:53 PMDockerHost block is just connection/configuration for a Docker daemon (e.g., base URL, timeouts). It doesn’t create, hold, or reuse containers for runs. The “max_pool_size” on the block is an HTTP connection pool to the Docker API, not a pool of containers.
- Prefect workers (including Cloud Run v2, Docker, Kubernetes) generally start fresh infrastructure per flow run. There’s no built-in prewarming or container reuse for flows in Prefect 3.x.
What you can do to reduce cold starts:
1) If you can run your own infra: use a Docker work pool + Docker worker on a long-lived VM
- The worker is always running; it will launch local containers for each run, which can be much faster if the image is already present.
- Set image_pull_policy to avoid re-pulling:
from prefect import flow
@flow
def my_flow():
...
my_flow.from_source(
"<https://github.com/<org>/<repo>>",
entrypoint="flows.py:my_flow",
).deploy(
name="prod",
work_pool_name="docker-pool",
image="<http://gcr.io/<project>/<image>:<tag>|gcr.io/<project>/<image>:<tag>>",
job_variables={"image_pull_policy": "IfNotPresent"} # or "Never" if image is guaranteed locally
)
- Docs: Run flows with Docker and Workers concept
2) If you must stay on Cloud Run Jobs (v2):
- Cloud Run Jobs spin up new job executions; there isn’t a “min instances” concept like services. Cold start is inherent.
- You can still improve startup by:
- Keeping images small and pre-built (no runtime pip installs).
- Hosting images in Artifact Registry in the same region.
- Avoiding unnecessary network init at start.
- Cloud Run worker docs: GCP worker guide
3) Consider Process or “serve” for warm, long-lived processes (if acceptable)
- A process work pool or flow.serve() keeps a Python process hot on a VM and avoids container spin-up entirely.
- Docs: Run flows in local processes
4) Kubernetes alternative
- Still one pod per run, but you can reduce startup by keeping nodes warm and images pre-pulled on nodes (imagePullPolicy: IfNotPresent), which can be faster than serverless job provisioning.
On subflows:
- If you call another flow as a function from within a flow, it typically runs in the same infrastructure as the parent (no extra container). If you schedule a separate flow run (e.g., via APIs that create a new run), that will incur a new worker/container startup.
If you share a bit more about your constraints (must use Cloud Run Jobs vs. OK to run a VM, typical image size, where your image is hosted), I can suggest a concrete setup to minimize your startup time.Gautam Menon
11/10/2025, 3:04 PMrun_deployment . Currently the cold start of the containers per subflow causes very long time spans for flow completion. The obvious optimization would be to have warm containers available to pickup jobs from the prefect workpool queue. I am using GCP as my infra provider, suggest some options to tackle this situation.Marvin
11/10/2025, 3:04 PMMarvin
11/10/2025, 3:15 PMrun_deployment subflow pays the cold start cost.
Good options on GCP to reduce the impact:
Option A — Stay on Cloud Run (push pool) and mitigate cold starts
- Make the container fast to start
- Use a slim base image and multi-stage builds; keep layers small and shared across images.
- Lazy-import heavy libs inside tasks instead of on module import.
- If you’re using .from_source(...) deployments, avoid repo clones at runtime. Package your code instead:
- Use Prefect’s GCS build/pull steps so your code is fetched quickly from GCS instead of cloning Git on every run:
# prefect.yaml
deployments:
- name: my-deployment
entrypoint: flows/my_flow.py:my_flow
work_pool:
name: my-cloud-run-v2-pool
build:
- prefect_gcp.deployments.steps.push_to_gcs:
bucket: my-bucket
folder: bundles/my-flow
pull:
- prefect_gcp.deployments.steps.pull_from_gcs:
bucket: my-bucket
folder: bundles/my-flow
- Keep your container image in Artifact Registry in the same region as your Cloud Run job.
- “Warm” the image cache (best-effort)
- Schedule a tiny warm-up deployment every few minutes that uses the same image to help keep it cached regionally. This is not guaranteed (no min-instances for jobs), but it can shave off image pull time.
- Tune job variables where helpful (though they won’t remove cold starts)
- Region, service account, VPC connector, CPU/memory, env vars, etc. can be set via job variables on your deployment or pool.
- Links:
- Prefect Cloud Run worker guide
- Cloud Run Jobs (GCP docs)
- Min instances (Services only)
Option B — Switch to a pull pool to keep capacity “warm”
If you need isolated containers per subflow with much faster startup, moving from a push pool (Cloud Run) to a pull pool is the most effective fix. A pull worker is a long-running process that polls Prefect and spins containers/pods immediately, avoiding serverless provisioning overhead.
- Docker worker on a small GCE VM
- Pros: Very fast per-run startup on a warm VM since the image is already pulled; simple to operate; low cost with a small always-on VM.
- Steps:
- Create a Docker work pool and start a worker on a GCE VM. Pre-pull your images on that VM.
- Control concurrency at the pool level and via worker settings.
- Docs: Docker worker guide
- Kubernetes worker on GKE
- Pros: Good isolation per run (K8s Jobs), fast scheduling when nodes are warm; lots of configurability.
- Tips for low latency:
- Keep a small node pool always on (min nodes > 0) to avoid node spin-up.
- Use Artifact Registry in the same region; pre-pull images (DaemonSet) and set imagePullPolicy: IfNotPresent.
- Run the Prefect Kubernetes worker as a Deployment in the cluster.
- Docs: Kubernetes worker guide
Option C — Revisit orchestration boundaries
- If strict isolation per subflow is not always necessary, consider:
- Running some subflows inline (call the subflow directly) within one container run to amortize startup cost.
- Parallelizing inside a single run (e.g., Dask/Coiled) to keep one container alive while doing many units of work.Marvin
11/10/2025, 3:15 PMrun_deployment subflows are you firing per parent flow and how frequently?
- Are your deployments currently using .from_source(...) (Git clone at runtime) or are you packaging code into your image already?
- What’s your typical image size?
If you want to stick with Cloud Run, I can help you set up the GCS build/pull steps and review your Dockerfile for faster startup. If you’re open to Docker or Kubernetes workers, I can outline the exact steps to get a GCE VM or GKE-based worker running and tuned for low-latency starts.Gautam Menon
11/10/2025, 3:26 PM.from_source() but the prefect worker is from a pre-build docker image, while the server is hosted on a separate long-standing VM.
4. My typical image size ranges from ~ 2 - 3 Gbs
I cannot provide you with my Dockerfile so assume a representative example for discussion.Marvin
11/10/2025, 4:10 PMMarvin
11/10/2025, 4:12 PMrun_deployment, the key is to minimize the cold-start penalty of each Cloud Run Job. Because Jobs don’t support min-instances or provisioned concurrency, we’ll focus on what you can control: image size, code retrieval, regional placement, and execution strategy.
What you can do right away
1) Stop cloning source at runtime
- If your deployments use .from_source(...), each job clones/pulls your repo at start — that’s pure startup overhead.
- Switch to packaging code into the image or use Prefect’s GCS build/pull steps. This removes git operations during every subflow start.
- Example prefect.yaml using GCS steps:
deployments:
- name: my-deployment
entrypoint: flows/my_flow.py:my_flow
work_pool:
name: my-cloud-run-v2-pool
build:
- prefect_gcp.deployments.steps.push_to_gcs:
bucket: my-bucket
folder: bundles/my-flow
pull:
- prefect_gcp.deployments.steps.pull_from_gcs:
bucket: my-bucket
folder: bundles/my-flow
- Docs: GCP worker guide
2) Split the child subflow into its own minimal image
- 2–3 GB images are a major cold start driver. Create a separate, slim image for the subflow with only the deps it needs (not the full parent stack).
- Tips:
- Use python:3.x-slim (or distroless if appropriate).
- Multi-stage builds; build wheels in a builder stage, copy only wheels to the final image.
- Remove build tools and caches from the final image.
- Keep layers stable and shared to maximize regional caching.
- Store the image in an Artifact Registry repository in the same region as the Cloud Run Job.
- Representative Dockerfile pattern:
# syntax=docker/dockerfile:1.6
FROM python:3.11-slim AS builder
WORKDIR /wheels
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip wheel -r requirements.txt
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1 PIP_NO_CACHE_DIR=1
# Only runtime OS deps
RUN apt-get update && apt-get install -y --no-install-recommends \
libstdc++6 libgomp1 \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
COPY --from=builder /wheels /wheels
RUN pip install --no-index --find-links=/wheels /wheels/*
WORKDIR /app
# Copy only what’s needed to run the child flow
COPY src/ ./src
# No need to set a CMD for Prefect jobs; Prefect handles the entrypoint
- If you’re using heavy stacks (e.g., PyTorch, full JDK), consider optionalizing them or splitting to separate images.
3) Keep everything regional and cache-friendly
- Push images to Artifact Registry in the same region as your Cloud Run Job.
- Use immutable tags/digests to leverage caching but avoid churn on base layers.
- If parent and child share a big base layer, keep that layer stable so the child benefits from a cached pull.
4) “Warm up” the image cache (best-effort)
- Schedule a tiny Cloud Run Job (via a simple Prefect deployment) every few minutes using the same child image. This can keep layer caches warm in the region. It’s not guaranteed but can help.
5) Overlap the cold start with parent work
- If the subflow doesn’t depend on the parent’s outputs, dispatch it earlier and wait later.
- Kick off run_deployment(...) near the start of the parent flow with a short timeout so the parent doesn’t block.
- Poll for completion later in the flow. This overlaps the container spin-up with parent execution.
- If the child depends on the parent’s result, you might still do partial early-dispatch (e.g., have the child fetch data directly from a durable location rather than being passed as a parameter).
6) Give the job enough CPU during startupMarvin
11/10/2025, 4:12 PMjob_variables for the child job can reduce import time and overall runtime, even if it costs a bit more per minute.
- You can set region, image, cpu/memory, service account, VPC connector, env, timeout in the Cloud Run V2 work pool variables or per deployment.
- Docs for tuning job variables: Work pool management and GCP worker guide
7) If you must continue with .from_source(...)
- Prefer small, shallow repos; ensure the source is in the same region if you’re hosting it yourself; minimize submodules; avoid fetching non-essential assets at runtime.
- But whenever possible, prefer the GCS steps or bake code into the image — they’re almost always faster for Cloud Run Jobs.
Why Cloud Run Services aren’t a drop-in fix here
- Services support min-instances (true warming), but Prefect’s Cloud Run integration executes flows as Jobs, not Services. You can’t keep Job instances warm with min-instances.
- Running a pull worker as a Service with min-instances would remove cold starts, but then you lose per-subflow container isolation (process worker) and can’t use Docker-in-Docker on Cloud Run. That conflicts with your isolation requirement.
Helpful references
- Prefect Cloud Run worker guide
- Customize job variables
- Work pools overview
- Cloud Run Jobs (GCP)
If you share the rough dependency stack for the child flow (even just a list of heavy libs), I can propose a concrete slimming plan to try to bring the child image closer to sub-1 GB and outline the exact job_variables to set on the deployment. Also, do you have the parent and child images stored in the same regional Artifact Registry as your Cloud Run Jobs?