Pub/Sub plus Cloud Scheduler: a cron job that fans out to filtered subscriptions
Pub/Sub decouples the code that sends events from the code that handles them, and Cloud Scheduler adds the clock. Build a topic with two subscriptions, one filtered, feed it on a schedule, then delete it all.
Pub/Sub, in plain words
Pub/Sub is Google Cloud's managed messaging service. A publisher sends a message (a body plus optional key-value attributes) to a topic. Every subscription attached to that topic gets its own copy, and a subscriber either pulls messages from it or has them pushed to an endpoint. The subscriber then acknowledges each message; anything not acknowledged in time is delivered again. Because senders never wait for receivers, you can add, pause or scale consumers without touching the code that produces events.
Subscriptions can carry a filter on attributes, so one topic can feed several consumers that each care about different messages. Pub/Sub acknowledges non-matching messages for you, so they never reach that subscriber. Filters only look at attributes, never at the message body, and you can't edit a filter after the subscription exists.
Cloud Scheduler is the managed cron service that pairs naturally with it. A job has a five-field cron schedule, a time zone (UTC unless you set one) and a target: an HTTP endpoint, an App Engine app, or a Pub/Sub topic. With a topic target, each run publishes one message, so anything subscribed to that topic becomes a scheduled task. Since 2022 these jobs no longer need an App Engine app; you just pick a region per job.
Reach for it when
- Several independent systems need to react to the same event, like an order that must update stock, send an email and land in BigQuery, and you don't want the sender to call each one.
- Producers and consumers run at different speeds, and you want a buffer that holds a burst of events for days instead of dropping them when a worker is busy.
- You need a job to run on a schedule (nightly report, hourly cleanup, a weekly batch prediction) and want the trigger to be a message that any consumer can pick up and retry.
Skip it when
- The caller needs an answer right away, like a web request waiting for a price. Call the service directly over HTTP or gRPC instead of putting a queue in the middle.
- You need to control exactly when and how fast each individual task hits one HTTP endpoint, with per-task delays and rate limits. Cloud Tasks is built for that kind of targeted dispatch.
- You need multi-step logic with branches, waits and error handling across several services. Workflows coordinates those steps; Pub/Sub only moves messages.
The AI angle
Pub/Sub is often the plumbing in front of AI on Google Cloud. A scheduled message can kick off a nightly batch prediction or re-embedding job, and a stream of events can feed a model one message at a time. Pub/Sub's AI Inference single message transform can call a model on Gemini Enterprise Agent Platform (formerly Vertex AI) for each message and attach the model's answer before delivery, at $60 per TiB of throughput plus the model's own charges, so it is not part of today's free lab. Google's docs recommend putting that transform on a subscription rather than the topic, so a failing model call is retried instead of failing the publish.
Hands-on lab: Fan out a scheduled message to a filtered and an unfiltered subscription
Create a topic with two pull subscriptions (one that only accepts messages marked high priority), publish test messages by hand, have Cloud Scheduler publish on a schedule, check who received what, then delete every resource.
Before you start
- A Google Cloud project you own with billing enabled (or the free-trial credit). Use a sandbox project, not a work one.
- Owner, or Cloud Scheduler Admin (
roles/cloudscheduler.admin) plus Pub/Sub Editor (roles/pubsub.editor) and permission to enable APIs. - Your project ID, to replace
YOUR_PROJECT_IDbelow.
Open Cloud Shell, set variables and enable two APIs
Choose Activate Cloud Shell in the console's top bar. The names below only need to be unique inside your project. If Cloud Shell reconnects later, run this block again to restore the variables.
gcloud config set project YOUR_PROJECT_ID REGION=us-central1 TOPIC=lab-ticks SUB_ALL=lab-all SUB_URGENT=lab-urgent JOB=lab-tick-job gcloud services enable pubsub.googleapis.com cloudscheduler.googleapis.comCreate one topic and two subscriptions
lab-allreceives every message.lab-urgenthas a filter, so Pub/Sub only delivers messages whosepriorityattribute is exactlyhigh(keys and values are case-sensitive). Note the quoting: single quotes around the whole filter, double quotes around the value.gcloud pubsub topics create $TOPIC gcloud pubsub subscriptions create $SUB_ALL --topic=$TOPIC gcloud pubsub subscriptions create $SUB_URGENT --topic=$TOPIC \ --message-filter='attributes.priority = "high"'Publish two test messages and pull them
One message is marked high priority, one low.
--auto-ackacknowledges what you pull so it isn't delivered again. Expect two messages fromlab-alland only the high one fromlab-urgent. A pull can come back short or empty; just run that pull again.gcloud pubsub topics publish $TOPIC --message="disk almost full" --attribute=priority=high gcloud pubsub topics publish $TOPIC --message="weekly digest ready" --attribute=priority=low gcloud pubsub subscriptions pull $SUB_ALL --auto-ack --limit=5 gcloud pubsub subscriptions pull $SUB_URGENT --auto-ack --limit=5Add the clock: a Cloud Scheduler job that publishes to the topic
The schedule
*/5 * * * *means every five minutes, evaluated in UTC.--attributesmarks each scheduled message as high priority, so both subscriptions should receive it. Rather than wait,jobs runtriggers one run now; the first job in a project can take a few minutes to fire.gcloud scheduler jobs create pubsub $JOB \ --location=$REGION \ --schedule="*/5 * * * *" \ --time-zone="Etc/UTC" \ --topic=$TOPIC \ --message-body="scheduled health check" \ --attributes=priority=high,source=scheduler gcloud scheduler jobs run $JOB --location=$REGIONCheck the scheduled message arrived, then pause the job
Wait about a minute, then pull from the filtered subscription and look for
scheduled health checkwithsource=scheduleramong its attributes.describeshows the job'sstateandlastAttemptTime; a missing or emptystatusmeans the last run succeeded. Pausing stops further runs while you look around.gcloud pubsub subscriptions pull $SUB_URGENT --auto-ack --limit=5 gcloud scheduler jobs describe $JOB --location=$REGION \ --format="yaml(state,schedule,timeZone,lastAttemptTime,status)" gcloud scheduler jobs pause $JOB --location=$REGION
Check that it worked
Step 3 shows two messages from lab-all but only disk almost full from lab-urgent. Step 5 shows scheduled health check from lab-urgent (if it's empty, wait another minute and pull again), and the job's state is ENABLED before the pause. A NOT_FOUND error on the scheduler commands usually means --location is missing or different from the one you created the job in.
Clean up
Delete in this order: the scheduler job first so it stops publishing, then the subscriptions, then the topic. Subscriptions are separate resources from their topic, so remove them explicitly. Each delete may ask you to confirm. Then list what's left: neither list should show the lab names. Disabling the two APIs is optional and only sensible in a sandbox where nothing else uses them. If this project exists only for practice, you can delete the whole project instead.
gcloud scheduler jobs delete $JOB --location=$REGION
gcloud pubsub subscriptions delete $SUB_ALL $SUB_URGENT
gcloud pubsub topics delete $TOPIC
# confirm nothing is left
gcloud scheduler jobs list --location=$REGION
gcloud pubsub topics list --filter="name~lab-ticks"
gcloud pubsub subscriptions list --filter="name~lab-"
# optional, sandbox only:
# gcloud services disable cloudscheduler.googleapis.com
# optional, practice-only project:
# gcloud projects delete YOUR_PROJECT_IDGotchas
- Both services deliver at least once: a scheduled run can occasionally fire twice and a message can arrive more than once. Make consumers idempotent, for example by remembering message IDs they've already handled.
- A subscription only receives messages published after it was created. Create subscriptions first, then start publishing, or you lose the early messages.
- Filters match attributes only and can't be changed later. To change one you create a new subscription (Google documents a snapshot-and-seek path to avoid losing messages) and switch consumers over.
- Pub/Sub bills every request as at least 1 KB, so millions of tiny one-message publishes cost more than their bytes suggest. Client libraries batch messages to avoid this.
- Cloud Scheduler runs on wall-clock time in the job's time zone, so daylight-saving changes can skip or repeat a run. Google recommends UTC when the exact interval matters.