Shed

Cloud Run is a machine

Three handoffs, two files you have to write, a registry URL you have to derive, and a Kubernetes dialect you have to learn. Counting the cost of getting one container running.

Cloud Run is a good tool. It's also a complicated one, and the longer you run it the more it feels like what it technically is: a simplified Kubernetes, with the simplification enforced as limitations.

We think the agentic cloud is close: agents that write software, put it somewhere, keep it running, and fix it at 2am without waking anyone. For that to work, the deploy path has to be software an agent can operate, and most of it isn't. Mostly it's consoles, forms, and browser tabs with a CLI sprinkled between them.

This is the first post in a series where we count that. We start with Google Cloud Run because it's the strongest version of the current answer: a very good container runtime with a one-command deploy. The runtime is good. Getting to it is the expensive part.

What we're counting, and why it isn't handoffs

We started this series planning to count handoffs, meaning the moments where an agent has to stop, hand control back to a person, and wait while that person goes and clicks something in a browser. Handoffs are countable and they're what makes setup feel long. A person experiences one as a context switch. An agent just stops, and the session sits on a half-finished thought while somebody waits for a billing form to load.

Then we counted Cloud Run's and got nine, and the number fell apart under inspection. Seven of the nine steps in Google's quickstart are scriptable. We'll show that below. The honest count is three, and one handoff of difference is not a blog post.

The problem was the unit. A handoff is only the part of the cost you can see. What actually stands between an agent and a running container is a pile of separate things, and only some of them involve a browser. You have to write a Dockerfile. You have to construct a registry URL out of four variables, none of which appear in the deploy command. You have to know that three specific APIs need enabling and that nothing will tell you which. And if you want the configuration in a file, the file is a Kubernetes manifest in a dialect you did not choose.

Cloud Run is a machine, and this is what machines are like. It costs something to start it and it costs something to keep it running, and those are different costs paid at different times. Below is our attempt to put both of them on paper.

What Cloud Run actually is

Under the marketing name, Cloud Run is Google's managed serverless container platform. You hand it an OCI image, or source code that Cloud Build turns into an image with buildpacks and parks in Artifact Registry, and it runs that container with request-driven autoscaling that goes down to zero.

The specifics are what make it good. Concurrency is per instance rather than per request, up to 1000 of them on one container, which is the single biggest difference from Lambda-shaped platforms where one sandbox serves one request. An I/O-bound service that would occupy two hundred Lambda sandboxes occupies one Cloud Run instance. Scaling is queue-aware: when everything is at its concurrency limit, a pending request waits up to 3.5 times your average startup time, or 10 seconds, whichever is longer, before it fails.

Revisions are immutable and traffic is a separate concern. You can deploy with --no-traffic --tag canary, get a distinct URL, test against production infrastructure with zero production traffic, then shift with update-traffic. Rollback is the same command pointed at the old revision. For an agent deploying its own code this is the best primitive on the platform, and it's barely marketed.

The second-generation execution environment is a microVM with full Linux syscall compatibility rather than the gVisor sandbox of gen1, so unmodified binaries work. Request timeouts go to 3600 seconds. An instance can carry up to 10 containers sharing a network namespace, so sidecars work. Direct VPC egress no longer needs a connector. Cloud Storage buckets and NFS shares mount as filesystems. Worker pools, GA since April, give you long-lived pull-based instances with no URL, and they take L4 or Blackwell GPUs that start in about five seconds with drivers preloaded.

That's a serious piece of engineering. We don't know of anything closer to "just run my container" sitting on infrastructure that size.

The cost to start it

Here's Google's quickstart, counted. Handoffs marked.

  1. Create a Google account (handoff)
  2. Create a Cloud project (handoff)
  3. Attach a billing account (handoff)
  4. Install the gcloud CLI
  5. Run gcloud auth login, which opens a browser (handoff)
  6. Set the active project (handoff)
  7. Enable the Cloud Run API (handoff)
  8. Enable the Cloud Build API (handoff)
  9. Enable Artifact Registry (handoff)
  10. Answer the region and access prompts (handoff)
  11. Deploy

Eleven steps and nine handoffs, and that's the number we were going to publish. It's also not the number a competent engineer pays, because most of those steps have a CLI form:

gcloud projects create $PROJECT
gcloud billing projects link $PROJECT --billing-account=$BILLING_ACCOUNT
gcloud config set project $PROJECT
gcloud services enable run.googleapis.com \
    cloudbuild.googleapis.com \
    artifactregistry.googleapis.com
gcloud run deploy $NAME --source . --region=europe-west1 --allow-unauthenticated

Five commands, and the honest handoff count is three: create a Google account, create a billing account with a card, authenticate once in a browser. Two of those nobody can remove, since you have to exist and you have to prove it. The third is console-only because attaching a payment instrument is console-only everywhere. So the dramatic version of our argument doesn't survive contact with the CLI reference, and we'd rather say that ourselves than have someone say it for us.

What it costs instead is everything that script assumes you already know. That Cloud Run needs exactly those three APIs. That Cloud Build and Artifact Registry are involved at all, which the deploy command never mentions. That --allow-unauthenticated is the flag suppressing the interactive prompt. That region is mandatory and unset by default. Skip any of it and you get a permission error that names none of it, which is the specific failure mode an agent cannot recover from. A handoff at least announces itself. This doesn't.

And --source is the easy path. Take the image path instead, which is what you want the moment you care about reproducible builds, and the bill grows. You write a Dockerfile. You create a repository, because Artifact Registry does not make one for you:

gcloud artifacts repositories create $REPO \
    --repository-format=docker --location=europe-west1
gcloud auth configure-docker europe-west1-docker.pkg.dev

Then you push to a URL you assembled yourself out of four variables in a fixed order:

europe-west1-docker.pkg.dev/$PROJECT/$REPO/$IMAGE:$TAG

Region, project, repository, image. Get the region wrong and the push is denied. Forget the credential helper and the push is denied. Neither error explains itself, and neither the format nor the repository requirement appears anywhere in gcloud run deploy --help.

The cost to keep it running

Then there's the configuration, which is where this stops being a setup problem and starts being a permanent one.

Cloud Run's API is Knative Serving, and Knative Serving is a Kubernetes API. So when you ask for your service as a file, you get a Kubernetes manifest, because that is literally what it is. Here is a trimmed export of a service that does nothing but serve HTTP on port 8080:

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: my-service
  namespace: '482910573621'
  labels:
    cloud.googleapis.com/location: europe-west1
  annotations:
    run.googleapis.com/ingress: all
spec:
  template:
    metadata:
      annotations:
        autoscaling.knative.dev/maxScale: '100'
        run.googleapis.com/execution-environment: gen2
        run.googleapis.com/startup-cpu-boost: 'true'
    spec:
      containerConcurrency: 80
      timeoutSeconds: 300
      serviceAccountName: 482910573621-compute@developer.gserviceaccount.com
      containers:
      - image: europe-west1-docker.pkg.dev/my-project/my-repo/my-app:latest
        ports:
        - name: http1
          containerPort: 8080
        resources:
          limits:
            cpu: 1000m
            memory: 512Mi
  traffic:
  - percent: 100
    latestRevision: true

The image path is five levels deep. The namespace is your project number, which is not your project ID and which you have to look up. There are two metadata blocks and two spec blocks nested inside each other, meaning different things at each level.

The part that hurts most is the split between fields and annotations. Concurrency, timeout, and service account are fields. Max scale, execution environment, and startup CPU boost are annotations, in two different namespaces, with their values quoted as strings because annotations are string-valued. Nothing about a setting tells you which kind it is. You cannot derive it, you can only remember it or look it up, and an agent editing this file has no way to reason its way to the right answer.

That's three descriptions of the same service, incidentally. The gcloud flags, the console form, and this file. They don't round-trip cleanly, so drift between them is the default state rather than an accident.

The rest of the running cost is the shape of the platform. Software comes in four kinds and you choose before you understand the problem: a service is a container with a URL, a job is a separate resource with its own execute flow, a worker pool is a third thing with manual instance counts and no endpoint, and cron is a fourth product, Cloud Scheduler, with its own IAM binding to invoke any of them. CPU is throttled to near zero between requests unless you set CPU always-on, so background goroutines and timers stop silently and resume when the next request arrives. Cold starts are the price of gen2, and the fix is --min-instances, which bills continuously and cancels the thing that made scale-to-zero attractive. Volume mounts exist but a GCS FUSE mount is not a disk, so anything transactional means Cloud SQL or Firestore, which is another product and another set of credentials.

Handing an agent the keys is its own project: a service account, then run.admin, cloudbuild.builds.editor, artifactregistry.writer and iam.serviceAccountUser, then either a long-lived key file sitting on disk or workload identity federation, which is more setup than the thing you were deploying. That machinery is real and it was built for fleets and org charts. It's heavier than a 200-line repair script deserves.

Our own count, honestly

Shed from nothing is five steps and two handoffs, both of the irreducible kind. Against Cloud Run's honest three that's a difference of one, and one handoff is not a business.

The difference we believe in is the rest of the bill. There's one file. Its schema is the whole configuration surface, so there's no field-or-annotation question to get wrong. There's no second product behind the deploy and no URL to assemble by hand. A failed deploy names what failed, which means an agent can read the error, change the file, run it again, and be right the second time without a person having read a quickstart first.

Why the count is going up

The number of deployable things per engineer is climbing fast, because agents now write most of the small ones. Three handoffs plus an afternoon learning which APIs to enable and how registry URLs are spelled, amortised over a five-year production service, is noise. Nobody should care. The same cost in front of a script that runs twice and then dies is why the script stays on someone's laptop.

So the gap isn't in the runtime. Cloud Run already runs containers about as well as containers get run, and on the axes that matter to a large service it beats most of what we'll cover in this series. The missing piece is a path to it made of files and commands that describe themselves. That's the bet we're making.

Next in the series: Cloudflare is a kit of parts.