Cloud Run Scaling, Traffic and Jobs

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

See the section bibliography for the reference material consulted while preparing these pages.

Commands and behaviour below are current for the gcloud CLI 586.x line, the Google Cloud Client Libraries for Java (libraries-bom) and the Cloud Run documentation as checked on 2026-09-30; re-verify against gcloud version and the linked reference pages before relying on them in a pipeline. Dated launch stages quoted here (worker pools GA on 2026-04-14, multi-region services GA on 2026-06-29, Cloud Run instances still in Preview) come from the Cloud Run release notes.

Cloud Run covers what a service is, how to deploy one (source or image), configuration, identity and networking, and deploys the Bookshelf catalog-api. This page starts where that one stops: how Cloud Run decides how many instances run, how you move traffic between revisions safely, how run-to-completion work (the Bookshelf nightly-report job) and background consumers (a worker pool) fit in, and the newer building blocks (instances, GPUs, multi-region services). The platform-wide choice between Cloud Run, GKE and Compute Engine is in Choosing a Compute Service.

Autoscaling

A Cloud Run service scales instances out and in automatically between a minimum and a maximum. Each revision of the service is an immutable snapshot of the container image plus its configuration; instances of a revision are the running containers. Cloud Run uses a metrics-based autoscaler: it compares the average CPU utilisation, and the number of concurrent requests per instance, against targets and adds or removes instances to stay near them.

Control Behaviour

Concurrency (--concurrency)

Maximum simultaneous requests one instance accepts. Default 80, maximum 1,000. Set to 1 for code that is not safe to share between requests. A Java service on virtual threads or reactive code usually does well at the default or higher.

Min instances (--min)

Instances kept warm even with no traffic. Default 0 (scale to zero). Removes most cold starts but the idle instances are billed.

Max instances (--max)

Upper bound. Every service has a default limit that depends on the regional quota; set an explicit value to protect a backend such as a Cloud SQL connection limit or a third-party API from a traffic spike.

CPU and concurrency targets (--scaling-cpu-target, --scaling-concurrency-target)

Utilisation the autoscaler aims for. Defaults 60 percent for each; either one may be disabled but not both.

Startup CPU boost (--cpu-boost)

Temporarily gives an instance more CPU while it starts and until the first health check passes. Cuts JVM start-up time noticeably for Spring Boot containers.

Manual scaling (--scaling)

A fixed instance count instead of autoscaling, for predictable workloads. Set --scaling=auto to go back.

How it behaves in practice:

  • Requests that arrive while no instance has capacity are queued for a short period (related to the average container start-up time, with a floor of about ten seconds) and answered with HTTP 429 if no instance becomes available.

  • When traffic falls, instances with very low utilisation are shut down first; others are kept idle for a few minutes as spare capacity. Instances can also be stopped at any time, so an application must treat shutdown (SIGTERM) as normal.

  • The autoscaler tolerates a small band around the target before changing the instance count, so the number of instances does not oscillate.

  • Cold start is the time between a request arriving at a scaled-to-zero service and the container being ready. Remedies in order of cost: smaller images and lazy initialisation, startup CPU boost, --min of 1 or more, and, for Java, native-image or CDS/AOT builds (see Quarkus Reference).

Sizing rule of thumb for Bookshelf: catalog-api is I/O bound (database calls), so it runs at the default concurrency with --min 1 in production and --max set so that max x pool size stays below the Cloud SQL connection limit; orders-api is similar; a CPU-heavy variant would lower concurrency or raise the CPU target.

In the Google Cloud console

  1. Open the project picker and select the Bookshelf project.

  2. Go to Cloud Run and select the catalog-api service (APIs must be enabled: Cloud Run Admin API; Cloud Shell has the CLI preinstalled).

  3. Choose Edit & deploy new revision.

  4. Under Container(s) set Startup CPU boost in the Resources section.

  5. Under Revision autoscaling set Minimum number of instances and Maximum number of instances; the Scaling tab of the service edit page sets service-level (all revisions) values and the scaling mode (auto or manual).

  6. Under Requests set Maximum concurrent requests per instance.

  7. Choose Deploy. The new revision starts receiving traffic as configured by the service’s traffic settings.

With the gcloud CLI

gcloud services enable run.googleapis.com cloudscheduler.googleapis.com artifactregistry.googleapis.com

PROJECT_ID=$(gcloud config get-value project)
REGION=us-central1
SERVICE=catalog-api

# Per-revision settings: a new revision is created
gcloud run services update $SERVICE --region $REGION \
  --concurrency 80 \
  --cpu-boost \
  --min 1 \
  --max 20

# Tune what the autoscaler aims for (service level, inherited by new revisions)
gcloud run services update $SERVICE --region $REGION \
  --scaling-cpu-target 0.7 \
  --scaling-concurrency-target 0.6

# Fixed instance count (manual scaling) and back to autoscaling
gcloud run services update $SERVICE --region $REGION --scaling 3
gcloud run services update $SERVICE --region $REGION --scaling auto

# Inspect the effective settings
gcloud run services describe $SERVICE --region $REGION \
  --format="yaml(spec.template.metadata.annotations, spec.template.spec.containerConcurrency)"

--min and --max set per-revision values on the service template; the service-level scaling controls (a separate setting applied across revisions) are what --scaling and the target flags configure, and they are inherited by later revisions unless you change them. Min instances at service level are not allocated to tagged revisions that receive no traffic, which matters when you use the canary pattern below.

Concurrency, CPU billing and cost

Concurrency interacts with billing mode (see Cloud Run): with request-based billing you pay for CPU and memory only while a request is being processed (plus instance start-up) and per request, so higher concurrency means several overlapping requests share one billed instance-second. With instance-based billing you pay for the whole instance lifetime, which suits background work and GPUs. Idle min instances are billed at a lower idle rate under request-based billing. Current rates, the free tier and regional tiers are on the Cloud Run pricing page (pricing changes; re-check before budgeting).

Revisions

Every deploy that changes the container or its configuration creates a new revision named SERVICE-NNNNN-xyz. Revisions are immutable; traffic and tags are what move. You can list and inspect them, and revisions that receive no traffic and are not tagged are retained only up to a quota, after which the oldest are deleted automatically.

gcloud run revisions list --service $SERVICE --region $REGION
gcloud run revisions describe catalog-api-00007-abc --region $REGION
gcloud run revisions delete catalog-api-00003-xyz --region $REGION

Pin latest versus a named revision deliberately. A service whose traffic is --to-latest always sends 100 percent to the newest revision; as soon as you route to a named revision, new deployments no longer take traffic unless you ask for it (they are served only on their tag URL or after another update-traffic).

Traffic management

Cloud Run splits traffic by percentage between revisions of a service. The pieces are:

--no-traffic

Deploy a new revision that receives 0 percent of traffic.

--tag NAME

Attach a tag to the revision, which gives it a dedicated URL of the form https://NAME---SERVICE-hash-region.a.run.app (the exact host is shown in the console and in gcloud run services describe). Tags let you test a revision with real dependencies before it serves users.

gcloud run services update-traffic

Changes the split: --to-revisions REV=PERCENT,…, --to-tags TAG=PERCENT,…, --to-latest, and --set-tags, --update-tags, --remove-tags, --clear-tags.

In-flight requests are not dropped when the split changes; during the transition a request may reach either the old or the new revision. Traffic percentages must add up to 100.

flowchart LR C[Clients] --> S[Service catalog-api] S -->|90 percent| R1[Revision 00007
stable] S -->|10 percent| R2[Revision 00008
canary] T[Tag URL canary---catalog-api] -.->|100 percent of tagged requests| R2 R1 --> DB[(Cloud SQL
bookshelf-catalog)] R2 --> DB

Safe deployment: deploy dark, test, then shift

The pattern behind gradual rollout, blue-green and canary releases is the same three steps.

  1. Deploy without traffic and with a tag. The old revision keeps serving 100 percent.

  2. Test the tagged URL (smoke tests, a Bookshelf integration run against the real database, or a manual check).

  3. Shift traffic in steps and watch error rate and latency, or flip 100 percent for a blue-green cut-over.

Differences between the strategies are only in how step 3 is done: a canary moves a small percentage and waits; a gradual rollout increases it in stages (for example 5, 25, 50, 100); blue-green moves everything at once but keeps the old revision ready for an instant switch back.

In the Google Cloud console

  1. Open Cloud Run, select catalog-api, choose Edit & deploy new revision.

  2. In the deploy form, clear Serve this revision immediately so the revision receives no traffic, and set a Revision name suffix if you want a readable name. Choose Deploy.

  3. Open the Revisions tab, choose Manage traffic.

  4. Assign percentages to the revisions (for example 90 and 10) and save. Repeat with larger percentages as the canary proves healthy.

  5. To tag a revision, in the Revisions tab use the revision’s Add tag action; the tag URL is listed in its row.

  6. For a rollback, in Manage traffic give 100 percent to the previous revision.

With the gcloud CLI

# 1. Deploy a new revision dark, tagged "canary"
gcloud run deploy $SERVICE --region $REGION \
  --image $REGION-docker.pkg.dev/$PROJECT_ID/bookshelf/catalog-api:v2 \
  --no-traffic --tag canary

# 2. Get the tag URL and smoke test it
CANARY_URL=$(gcloud run services describe $SERVICE --region $REGION \
  --format="value(status.traffic.filter(tag=canary).url)")
curl -s -H "Authorization: Bearer $(gcloud auth print-identity-token)" "$CANARY_URL/actuator/health"

# 3. Canary 10 percent, then more
gcloud run services update-traffic $SERVICE --region $REGION --to-tags canary=10
gcloud run services update-traffic $SERVICE --region $REGION --to-tags canary=50

# Promote: everything to the newest revision, and drop the tag
gcloud run services update-traffic $SERVICE --region $REGION --to-latest
gcloud run services update-traffic $SERVICE --region $REGION --remove-tags canary

# Roll back instantly to a known good revision
gcloud run services update-traffic $SERVICE --region $REGION --to-revisions catalog-api-00007-abc=100

If a service ever needs to be managed declaratively, the traffic block is part of the service YAML, and the same split can be expressed with gcloud run services replace service.yaml or the Terraform google_cloud_run_v2_service resource (traffic blocks with type, revision, tag and percent; see Infrastructure as Code).

# fragment of a service YAML
spec:
  traffic:
    - revisionName: catalog-api-00007-abc
      percent: 90
    - revisionName: catalog-api-00008-def
      percent: 10
      tag: canary

Official docs: Cloud Run YAML reference.

Managing tags and cost

Tagged revisions that you forget keep their URL alive and, if min instances are set on the revision, may keep instances running. List tags with gcloud run services describe and remove those you no longer need with --remove-tags or --clear-tags. Quotas cap the number of revision tags per project and region.

Automating the progression

Cloud Deploy supports Cloud Run targets with canary strategies (percentages per phase, verification jobs and automatic rollback) driven from a Skaffold configuration; it is the managed way to run the manual steps above across dev, staging and prod projects. It belongs to the delivery pipeline described in Developer Tools and DevOps. Official docs: Deploy to Cloud Run with Cloud Deploy.

Multi-region services

A Cloud Run service lives in one region. A multi-region service deploys the same service to several regions with one command and puts a global external Application Load Balancer, a serverless NEG per region, a Google-managed certificate and a global anycast IP address in front of it, so users are routed to the nearest healthy region. It became generally available on 2026-06-29. With Cloud Run service health (a readiness-probe based signal) the load balancer fails traffic over to another region automatically and fails back when the region recovers.

gcloud run deploy $SERVICE \
  --image $REGION-docker.pkg.dev/$PROJECT_ID/bookshelf/catalog-api:v2 \
  --regions us-central1,europe-west1 \
  --min 2 \
  --readiness-probe httpGet.path=/actuator/health/readiness

# add or drop a region later
gcloud run deploy $SERVICE --add-regions asia-southeast1
gcloud run deploy $SERVICE --remove-regions asia-southeast1

The regional services created this way are managed through the multi-region definition. Design consequences: the database must itself be reachable from both regions (Cloud SQL cross-region replicas or a multi-region data store, see Cloud SQL), min instances are billed in every region, and traffic between the load balancer and the regions is billed as load-balancer egress. Custom domains and Cloud Armor attach to the load balancer, as in Load Balancing and Cloud Armor.

Jobs

A Cloud Run job runs a container to completion instead of serving requests. A job execution consists of one or more tasks (identical containers) that run in parallel and may retry; the execution succeeds when every task has exited successfully. Jobs are for batch processing, migrations, report generation and maintenance. In Bookshelf the nightly-report job imports the catalogue feed and produces a summary report.

Setting Meaning

--tasks

Number of tasks per execution, 1 to 10,000 (default 1).

--parallelism

Maximum tasks running at once. Without it Cloud Run starts as many as the regional capacity allows.

--max-retries

Retries per failed task, 0 to 10 (default 3). A task that exhausts retries fails the execution.

--task-timeout

Maximum run time of one attempt. Default 10 minutes, maximum 168 hours (7 days); 1 hour when GPUs are attached.

CLOUD_RUN_TASK_INDEX, CLOUD_RUN_TASK_COUNT, CLOUD_RUN_TASK_ATTEMPT

Environment variables injected into each task: the task’s index from 0, the total number of tasks, and the retry attempt number. Use the index to pick the task’s slice of the data.

--cpu, --memory, --service-account, --set-secrets, --set-env-vars

Same meaning as for services; jobs also run as a dedicated service account and use Direct VPC egress the same way.

A job is not a service: it has no URL, does not scale on requests, and tasks must exit (not listen on a port). Jobs are billed on instance-based billing for the time tasks run. Overrides at execution time (--tasks, --args, --update-env-vars, --task-timeout) change one run without editing the job.

Task code

package com.example.bookshelf.report;

import java.util.List;

public class NightlyReport {

    public static void main(String[] args) {
        int index = Integer.parseInt(System.getenv().getOrDefault("CLOUD_RUN_TASK_INDEX", "0"));
        int count = Integer.parseInt(System.getenv().getOrDefault("CLOUD_RUN_TASK_COUNT", "1"));
        int attempt = Integer.parseInt(System.getenv().getOrDefault("CLOUD_RUN_TASK_ATTEMPT", "0"));
        System.out.printf("task %d of %d, attempt %d%n", index, count, attempt);

        List<String> shards = ShardCatalog.shards();
        for (int i = index; i < shards.size(); i += count) {
            process(shards.get(i));        // each task handles every count-th shard
        }
        // a non-zero exit (or an uncaught exception) marks the attempt failed and triggers a retry
    }

    private static void process(String shard) { /* read from Cloud SQL, write to the uploads bucket */ }
}

The class uses Application Default Credentials (the job’s attached service account) through the Google Cloud Client Libraries; never mount a downloaded key. Make tasks idempotent because a retry repeats work. Official docs: Create jobs and Execute jobs.

In the Google Cloud console

  1. Go to Cloud Run, choose the Jobs tab, then Deploy container (or Create job).

  2. Select the image (REGION-docker.pkg.dev/PROJECT/bookshelf/nightly-report), name the job nightly-report, pick the region.

  3. Expand Container, Volumes, Connections, Security: set Task timeout, Number of tasks, Parallelism, Max retries, environment variables and secrets, and the Service account.

  4. Choose Create, then Execute to run it, and open the execution’s Tasks and Logs tabs.

  5. To run it nightly, open the job, choose Triggers, Add Scheduler Trigger, enter the schedule (unix cron) and time zone, select a service account allowed to invoke the job, and create.

With the gcloud CLI

JOB=nightly-report

gcloud run jobs deploy $JOB --region $REGION \
  --image $REGION-docker.pkg.dev/$PROJECT_ID/bookshelf/nightly-report:v1 \
  --service-account report-job@$PROJECT_ID.iam.gserviceaccount.com \
  --tasks 10 --parallelism 5 \
  --max-retries 2 --task-timeout 30m \
  --set-secrets DB_PASSWORD=catalog-db-password:latest

# Run it and wait for completion
gcloud run jobs execute $JOB --region $REGION --wait

# Override for one run
gcloud run jobs execute $JOB --region $REGION --tasks 50 --args="--full-import"

# Inspect executions and logs
gcloud run jobs executions list --job $JOB --region $REGION
gcloud logging read "resource.type=cloud_run_job AND resource.labels.job_name=$JOB" --limit 20

The service account of the job needs roles/secretmanager.secretAccessor on the secret and any data roles, as for a service. The account that triggers the job needs roles/run.invoker on it (or roles/run.developer for console use); IAM roles are explained in Identity and Access.

Running jobs on a schedule

Cloud Scheduler calls the Cloud Run Admin API’s jobs/JOB:run endpoint on a cron schedule, authenticating with an OAuth access token minted for a service account (the endpoint is a Google API, so an OAuth token, not an OIDC ID token, is required). The same mechanism works for any Google API.

flowchart LR CS[Cloud Scheduler
0 2 * * *] -->|POST jobs/nightly-report:run
OAuth token of scheduler SA| API[Cloud Run Admin API] API --> E[Execution] E --> T0[Task 0] E --> T1[Task 1] E --> T2[Task 2] E --> TN[Task 9] T0 --> DB[(Cloud SQL)] T1 --> DB T2 --> DB TN --> B[(Uploads bucket)]
gcloud iam service-accounts create report-scheduler \
  --display-name "Bookshelf nightly-report scheduler"

gcloud run jobs add-iam-policy-binding $JOB --region $REGION \
  --member "serviceAccount:report-scheduler@$PROJECT_ID.iam.gserviceaccount.com" \
  --role roles/run.invoker

gcloud scheduler jobs create http nightly-report-trigger \
  --location $REGION \
  --schedule "0 2 * * *" --time-zone "Etc/UTC" \
  --uri "https://run.googleapis.com/v2/projects/$PROJECT_ID/locations/$REGION/jobs/$JOB:run" \
  --http-method POST \
  --oauth-service-account-email report-scheduler@$PROJECT_ID.iam.gserviceaccount.com

# Test it now
gcloud scheduler jobs run nightly-report-trigger --location $REGION

Cloud Scheduler is billed per scheduler job per month with a small free allowance; check the Cloud Scheduler pricing page. Longer multi-step processes (import, then report, then notify) are better composed with Workflows, which can start a job and wait for it (the googleapis.run.v2.projects.locations.jobs.run connector); see Messaging and Integration. For Pub/Sub-driven work use a push subscription to a service or a worker pool below.

Worker pools

A worker pool is Cloud Run’s resource for continuous background work that has no incoming HTTP: a Pub/Sub pull subscriber, a Kafka consumer or a queue-draining process. It became generally available on 2026-04-14. Compared with services, a worker pool has no endpoint and does not scale on requests; compared with jobs, it is not expected to exit. Scaling is manual: you set the instance count, and you scale it from your own signal (queue depth) with a script, or with an external autoscaler.

gcloud run worker-pools deploy notifier-pull --region $REGION \
  --image $REGION-docker.pkg.dev/$PROJECT_ID/bookshelf/notifier:v1 \
  --service-account notifier@$PROJECT_ID.iam.gserviceaccount.com \
  --instances 2

gcloud run worker-pools update notifier-pull --region $REGION --instances 5
gcloud run worker-pools update notifier-pull --region $REGION --instances 0   # stop it
gcloud run worker-pools list --region $REGION

Worker pool names are at most 49 characters and unique per region and project. The deployer needs roles/run.developer, roles/iam.serviceAccountUser on the runtime service account and roles/artifactregistry.reader for the image. All requested instances are billed as active whether busy or idle, so the instance count is the cost lever; see the pricing page for regional tiers.

In the console: Cloud Run, Worker pools, Deploy container, select the image, name, region, and in Scaling the instance count.

Pull consumer in Java using Application Default Credentials (the pool’s service account needs roles/pubsub.subscriber on the subscription):

package com.example.bookshelf.notifier;

import com.google.cloud.pubsub.v1.AckReplyConsumer;
import com.google.cloud.pubsub.v1.MessageReceiver;
import com.google.cloud.pubsub.v1.Subscriber;
import com.google.pubsub.v1.ProjectSubscriptionName;
import com.google.pubsub.v1.PubsubMessage;

public class NotifierWorker {

    public static void main(String[] args) throws InterruptedException {
        String projectId = System.getenv("GOOGLE_CLOUD_PROJECT");
        ProjectSubscriptionName subscription = ProjectSubscriptionName.of(projectId, "orders-notifier-sub");

        MessageReceiver receiver = (PubsubMessage message, AckReplyConsumer consumer) -> {
            try {
                sendConfirmationEmail(message.getData().toStringUtf8());
                consumer.ack();
            } catch (RuntimeException e) {
                consumer.nack();
            }
        };

        Subscriber subscriber = Subscriber.newBuilder(subscription, receiver).build();
        subscriber.startAsync().awaitRunning();
        subscriber.awaitTerminated();   // the pool keeps running until stopped
    }

    private static void sendConfirmationEmail(String orderJson) { /* call the e-mail relay */ }
}

Official docs: Pub/Sub pull subscriptions and Pub/Sub client libraries. The event-driven (push) alternative for the same notifier is in Cloud Run Functions and Events; pull with a worker pool suits high throughput and controlled concurrency, while push to a service scales to zero and needs no code to poll.

Instances

A Cloud Run instance (Preview, the gcloud beta run instances surface) is an individually addressable singleton container that you start, stop and delete yourself, without a service or autoscaling. It targets one-off or long-lived single-container workloads such as a development environment or a stateful agent. As a Preview feature it is not covered by an SLA, may change, and is not available in every region (the documentation lists excluded regions); confirm status in the release notes before depending on it.

gcloud beta run instances create dev-sandbox --region $REGION \
  --image $REGION-docker.pkg.dev/$PROJECT_ID/bookshelf/catalog-api:v2
gcloud beta run instances delete dev-sandbox --region $REGION

GPUs

Cloud Run services and jobs can attach one GPU per instance (NVIDIA L4 and RTX PRO 6000 Blackwell are listed, in specific regions; services reached general availability on 2025-04-07). GPU instances use instance-based billing, still scale to zero, start in a few seconds and require a larger minimum of CPU and memory (for the L4, at least 4 vCPU and 16 GiB). Zonal redundancy is on by default, which reserves capacity across zones at a higher cost; turning it off saves money but offers only best-effort failover. A GPU job task has a maximum timeout of one hour. New projects get a small starter GPU quota; more requires a quota request.

gcloud run services update $SERVICE --region $REGION --gpu 1 --gpu-type nvidia-l4 \
  --cpu 4 --memory 16Gi --no-cpu-throttling

Official docs: GPU support for services, GPU support for jobs. Bookshelf has no GPU workload; the managed AI services are in AI, Data and Analytics Overview.

Quotas and limits

Limits change; the authoritative table is the Cloud Run quotas page. Values below were checked on 2026-09-30.

Item Limit

Request timeout (services)

Up to 60 minutes

Concurrent requests per instance

Default 80, maximum 1,000

CPU / memory per instance

Up to 8 vCPU / 32 GiB (GPU configurations require more)

Max instances

Region quota based; default applied to every service

Revisions per service

1,000 non-addressable revisions (older ones are deleted automatically)

Revisions serving traffic

4,000 per project and region

Revision tags

2,000 per project and region

Services per project and region

1,000

Job task timeout

Up to 168 hours (1 hour with GPUs)

Tasks per job execution

10,000

Task retries

10 per task

Job executions

1,000 per project and region (1,000 retained per job)

Worker pools

1,000 per project and region; name up to 49 characters

Troubleshooting

Requests return 429

Max instances or the regional quota is reached, or the queue wait exceeded its limit during a spike. Raise --max or the quota, increase concurrency, lower startup time, add --min.

Requests return 503

No instance became ready, usually because the container does not listen on $PORT fast enough, crashes at start or fails its probes. Check gcloud run services logs read and the revision’s Conditions.

Requests return 504

The request exceeded the request timeout. Raise --timeout (up to 60 minutes) or move long work to a job.

Slow first requests

Cold starts. Use --cpu-boost, --min 1, smaller images and lazy initialisation.

Costs keep rising after a canary

A tagged revision with min instances, or a forgotten tag, is still running; run gcloud run services describe and remove the tag.

Traffic split ignores a new deploy

The service is routed to a named revision; use --to-latest or the tag flow above.

Job fails with exit code 137

Out of memory. Raise --memory or process data in smaller shards.

Job tasks restart forever

Retries are exhausted at --max-retries; the execution then fails. Check the task’s logs for the attempt number in CLOUD_RUN_TASK_ATTEMPT.

Scheduler trigger returns 403

The scheduler service account lacks roles/run.invoker on the job, or an OIDC token was used where the Admin API needs an OAuth token.

Worker pool shows no activity

Instances are 0, the service account lacks roles/pubsub.subscriber, or the subscription is wrong.

Clean up

gcloud scheduler jobs delete nightly-report-trigger --location $REGION --quiet
gcloud run jobs delete $JOB --region $REGION --quiet
gcloud run worker-pools delete notifier-pull --region $REGION --quiet
gcloud beta run instances delete dev-sandbox --region $REGION --quiet

# remove tags and extra revisions on the service, then the service itself if it was only for the exercise
gcloud run services update-traffic $SERVICE --region $REGION --clear-tags
gcloud run services update $SERVICE --region $REGION --scaling auto --min 0
gcloud run services delete $SERVICE --region $REGION --quiet

gcloud iam service-accounts delete report-scheduler@$PROJECT_ID.iam.gserviceaccount.com --quiet

Official docs: gcloud run services delete, gcloud scheduler jobs delete. If you deployed a multi-region service, delete it with the same command, which also removes the load balancer resources it created. Deleting a throwaway project removes everything at once after a retention period.