Verified10 commandsAI-assisted

Serverless: Cloud Functions, Pub/Sub, Eventarc & Scheduler

.md

Verified against Google Cloud SDK 553.0.0, flags verified via `gcloud functions deploy --help`, · official docs

What it is and where it fits 🎯#

Cloud Run (page 02) is GCP's general-purpose serverless container platform; this page covers the layer of services built specifically around events, rather than long-running HTTP services. Cloud Functions is a function-as-a-service runtime (single-purpose, one trigger, one piece of code, the equivalent of AWS Lambda or Azure Functions), Pub/Sub is GCP's managed message queue/event bus, Eventarc is the routing layer that turns dozens of different GCP event sources into a uniform trigger any of these can react to, and Cloud Scheduler/Cloud Tasks handle time-based and deferred-work triggering respectively. This page assumes the project/auth basics from page 01; Cloud Functions (2nd gen) is actually built on Cloud Run under the hood, so several concepts from page 02's Cloud Run section (concurrency, --min-instances, revisions) apply here too.

Core concepts: how an event actually reaches your code#

Diagram

Two real trigger paths exist for a 2nd-gen Cloud Function, and picking the right one matters. Eventarc is the general-purpose router, subscribing to it gives a function access to dozens of event source types (Cloud Storage object changes, Firestore document writes, Cloud Audit Logs entries, Pub/Sub messages, and more) through one consistent CloudEvents-formatted payload. A direct Pub/Sub trigger (--trigger-topic) is the older, simpler, and still entirely valid shortcut specifically for "run this function whenever a message lands on this topic", skipping Eventarc's extra hop when you don't need its broader source catalog.

Deploying a Cloud Function#

gcloud functions deploy process-upload \
  --gen2 --runtime=python312 --region=us-central1 \
  --source=. --entry-point=handle_upload \
  --trigger-bucket=my-uploads-bucket \
  --memory=512Mi --timeout=120s --max-instances=20 \
  --service-account=processor@my-project-id.iam.gserviceaccount.com
  • --gen2 targets 2nd-generation Cloud Functions, built on Cloud Run and Eventarc, with a larger request timeout ceiling and concurrency support gen1 never had; new functions should default to it.
  • --entry-point names the specific function inside your source file that's the actual handler, the rest of the file can hold helpers, imports, whatever the runtime needs.
  • --trigger-bucket is shorthand for an Eventarc trigger on that bucket's object-finalize events; --trigger-topic, --trigger-http, and --trigger-event-filters (the general Eventarc form) are the other trigger shapes, mutually exclusive with each other.
  • --max-instances caps concurrent scale-out the same way it does on Cloud Run, worth setting deliberately rather than leaving unbounded on anything triggered by an event source you don't fully control the volume of (a public upload bucket, say).

HTTP-triggered functions and authentication#

gcloud functions deploy api-webhook \
  --gen2 --runtime=nodejs20 --region=us-central1 \
  --source=. --entry-point=handleWebhook \
  --trigger-http --no-allow-unauthenticated

gcloud functions add-invoker-policy-binding api-webhook \
  --region=us-central1 --member="serviceAccount:caller@my-project-id.iam.gserviceaccount.com"

--trigger-http gives the function its own HTTPS endpoint, functionally identical to a Cloud Run service at that point, --no-allow-unauthenticated (the safer default, same as Cloud Run on page 02) requires every caller to present a valid IAM-authenticated identity token. add-invoker-policy-binding is the function- specific equivalent of granting roles/cloudfunctions.invoker (or, on the underlying Cloud Run service, roles/run.invoker) to a specific caller identity, without which an authenticated-but-unauthorized caller gets a 403.

Managing deployed functions#

gcloud functions list --regions=us-central1
gcloud functions describe process-upload --region=us-central1
gcloud functions describe process-upload --region=us-central1 --format="value(state)"

gcloud functions logs read process-upload --region=us-central1 --limit=50
gcloud functions delete process-upload --region=us-central1

gcloud functions deploy against an existing function name updates it in place, deploying a new revision rather than creating a duplicate, the same "just redeploy" model as gcloud run deploy; there's no separate "update" subcommand for the function's code or trigger configuration.

Pub/Sub: topics and publishing#

gcloud pubsub topics create order-events
gcloud pubsub topics list
gcloud pubsub topics publish order-events --message='{"orderId": "12345", "status": "shipped"}' \
  --attribute=eventType=order.shipped

Note

gcloud pubsub topics publish is documented as being for testing and troubleshooting, not production traffic, an application should publish through a client library, which batches and retries properly. All subscribers to a topic must be able to consume and acknowledge whatever gets published, including a message sent manually this way, or Pub/Sub keeps re-attempting delivery of it for up to 7 days.

Pub/Sub: subscriptions#

gcloud pubsub subscriptions create order-events-processor \
  --topic=order-events --ack-deadline=60 \
  --dead-letter-topic=order-events-dlq --max-delivery-attempts=5

gcloud pubsub subscriptions create order-events-webhook \
  --topic=order-events --push-endpoint=https://my-service.a.run.app/pubsub-push \
  --push-auth-service-account=pubsub-invoker@my-project-id.iam.gserviceaccount.com

gcloud pubsub subscriptions pull order-events-processor --auto-ack --limit=5

A pull subscription (the first example) is what a worker actively polls, appropriate for a service that controls its own consumption rate; a push subscription (the second) has Pub/Sub actively HTTP-POST the message to an endpoint, appropriate for a Cloud Run/Functions consumer that doesn't want to run a long-lived polling loop at all. --dead-letter-topic combined with --max-delivery-attempts is the pattern that keeps one permanently-failing message from being retried forever, after the attempt count is exhausted it's routed to the dead-letter topic instead, where it can be inspected without blocking the rest of the subscription's throughput.

Important

A dead-letter topic needs its own IAM grant before Pub/Sub can actually publish to it — the Pub/Sub service agent needs roles/pubsub.publisher on the dead-letter topic specifically. Skipping this grant is a common "messages just vanish after the retry limit instead of showing up in the DLQ" surprise, since the subscription creation itself succeeds even with the grant missing; the failure only surfaces the first time a message actually exhausts its retries.

Eventarc: routing GCP events to a destination#

gcloud eventarc triggers create gcs-upload-trigger \
  --location=us-central1 \
  --event-filters="type=google.cloud.storage.object.v1.finalized" \
  --event-filters="bucket=my-uploads-bucket" \
  --destination-run-service=process-upload-run \
  --destination-run-region=us-central1 \
  --service-account=eventarc-invoker@my-project-id.iam.gserviceaccount.com

gcloud eventarc triggers list --location=us-central1
gcloud eventarc triggers describe gcs-upload-trigger --location=us-central1

Every --event-filters entry narrows which events actually invoke the destination, type is mandatory and identifies the CloudEvents event type; additional filters (like bucket above) further scope it to a specific resource rather than every bucket in the project. Eventarc's destination can be a Cloud Run service, a GKE service, a Cloud Function, or a workflow, not just a function, making it the shared routing layer across every compute option this site's GCP pages cover, not something Cloud Functions-specific.

Cloud Scheduler: cron-triggered jobs#

gcloud scheduler jobs create http nightly-report \
  --location=us-central1 --schedule="0 2 * * *" \
  --uri=https://my-service.a.run.app/generate-report \
  --http-method=POST \
  --oidc-service-account-email=scheduler-invoker@my-project-id.iam.gserviceaccount.com

gcloud scheduler jobs create pubsub cleanup-trigger \
  --location=us-central1 --schedule="*/15 * * * *" \
  --topic=cleanup-events --message-body='{"action":"purge-expired"}'

gcloud scheduler jobs list --location=us-central1
gcloud scheduler jobs run nightly-report --location=us-central1   # trigger it immediately, outside the schedule

--schedule takes standard cron syntax, evaluated in --time-zone (UTC by default). create http invokes a URL directly, --oidc-service-account-email is what authenticates that call as a real IAM identity rather than an anonymous HTTP request, the Scheduler equivalent of the WIF-authenticated CI patterns on page 01. create pubsub publishes onto a topic on schedule instead, useful when several downstream consumers should all react to the same scheduled tick via their own subscriptions rather than one job calling one URL directly. jobs run is genuinely useful for testing a job's actual behavior without waiting for its next scheduled tick.

Cloud Tasks: deferred and rate-limited work#

gcloud tasks queues create email-queue \
  --location=us-central1 \
  --max-dispatches-per-second=10 --max-concurrent-dispatches=50 \
  --max-attempts=5

gcloud tasks queues describe email-queue --location=us-central1
gcloud tasks queues pause email-queue --location=us-central1     # stop dispatching without deleting the queue

Cloud Tasks solves a different problem than Pub/Sub: explicit per-task control (schedule a specific task for 30 minutes from now, retry with a specific backoff, guarantee at-most-once execution ordering per queue) at the cost of needing a known HTTP endpoint to dispatch to, rather than Pub/Sub's fan-out-to-many-subscribers model. --max-dispatches-per-second is the mechanism for rate-limiting calls to a downstream system that can't handle unbounded concurrency, a third-party email API with its own rate limits, for instance, in a way a raw Pub/Sub push subscription has no equivalent knob for.

Real-world scenario: image-processing pipeline with a dead-letter safety net#

A photo-sharing app needs every uploaded image resized into three formats, with failures visible instead of silently retried forever:

gcloud pubsub topics create image-resize-dlq

gcloud eventarc triggers create image-upload-trigger \
  --location=us-central1 \
  --event-filters="type=google.cloud.storage.object.v1.finalized" \
  --event-filters="bucket=user-uploads" \
  --destination-run-service=resize-worker --destination-run-region=us-central1 \
  --service-account=eventarc-invoker@my-project-id.iam.gserviceaccount.com

gcloud pubsub topics add-iam-policy-binding image-resize-dlq \
  --member="serviceAccount:service-<project-number>@gcp-sa-pubsub.iam.gserviceaccount.com" \
  --role="roles/pubsub.publisher"

The dead-letter IAM grant is applied explicitly rather than assumed, per the IMPORTANT callout above, before the pipeline ever ships a genuinely malformed image that would otherwise vanish silently after exhausting its retries.

Real-world scenario: nightly batch job pre-flight checklist#

  • Confirm the target Cloud Run service or function has --no-allow-unauthenticated and Scheduler's service account is explicitly granted invoker on it, not relying on a broad IAM binding
  • Set --attempt-deadline on the job to something shorter than the target's own request timeout, so a genuinely hung downstream call fails the Scheduler job cleanly rather than the job itself timing out first with a less useful error
  • Run gcloud scheduler jobs run <job-name> manually once and inspect the actual response/logs before trusting the cron schedule to validate it for you overnight
  • Confirm --time-zone matches what the team actually expects, 0 2 * * * in UTC is a very different wall-clock hour for a team based outside UTC

CI/CD integration recipe: deploying an event-triggered function on push#

# .github/workflows/deploy-function.yml
name: Deploy function
on:
  push:
    branches: [main]
    paths: ["functions/process-upload/**"]

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: projects/<project-number>/locations/global/workloadIdentityPools/github-pool/providers/github-provider
          service_account: ci@my-project-id.iam.gserviceaccount.com
      - uses: google-github-actions/setup-gcloud@v2
      - run: |
          gcloud functions deploy process-upload \
            --gen2 --runtime=python312 --region=us-central1 \
            --source=functions/process-upload --entry-point=handle_upload \
            --trigger-bucket=my-uploads-bucket

The paths filter on the workflow trigger keeps this deploy from firing on unrelated commits elsewhere in a monorepo, worth pairing with any function deploy pipeline living alongside other services in one repo.

Common pitfalls#

  • Publishing directly via gcloud pubsub topics publish from production code paths. It's a troubleshooting tool; use a client library for real traffic, per the NOTE above.
  • Creating a dead-letter subscription without granting the Pub/Sub service agent publish rights on it. Messages exhausting their retries vanish instead of landing in the DLQ, see the IMPORTANT callout above.
  • Leaving a Scheduler job's --attempt-deadline unset on a call to a slow or occasionally-hanging endpoint. The job then waits on the target's own (often longer) timeout instead of failing fast.
  • Reaching for Cloud Tasks when Pub/Sub's simpler fan-out model would do, or vice versa. Tasks are for explicit per-task scheduling/rate-limiting against one known endpoint; Pub/Sub is for fan-out to potentially many independent subscribers.
  • Forgetting --gen2 and getting 1st-generation defaults. Gen1 functions have a lower timeout ceiling and no Eventarc-based trigger catalog, worth confirming explicitly rather than assuming the current default matches what a tutorial or older doc page describes.

Exit codes#

0 success, non-zero on any API/validation error, gcloud functions deploy in particular can fail at build time (a broken dependency, a syntax error in the source) with a non-zero exit and a build-log URL printed to stderr, gcloud functions logs read <name> --min-log-level=error is the fastest way to see the actual runtime (as opposed to build-time) failure once a function is deployed but misbehaving.

When to reach for something else#

For a stateless HTTP service with no genuine single-purpose-function shape, or one that needs more control over concurrency, startup behavior, or a non-standard runtime, prefer Cloud Run directly (page 02) over Cloud Functions, gen2 functions are Cloud Run underneath anyway, and Cloud Run's own gcloud run deploy exposes strictly more configuration surface. For declarative, reviewable event-routing and queue provisioning across environments, prefer Terraform's google_cloudfunctions2_function/ google_pubsub_topic/google_eventarc_trigger/google_cloud_scheduler_job resources over a growing shell script of the commands on this page, consistent with the IaC guidance on earlier pages.