Pub/Sub & Event-Driven Architecture
Build event-driven systems with Google Cloud Pub/Sub, covering topics, subscriptions, push/pull delivery, Eventarc, ordering, and schema validation.
Prerequisites
- Basic understanding of messaging patterns
- Familiarity with GCP Cloud Functions or Cloud Run
- Experience with gcloud CLI
Event-Driven Architecture on GCP
Event-driven architecture (EDA) is a design paradigm where services communicate by producing and consuming events rather than making direct synchronous calls. On Google Cloud, Pub/Sub is the foundational messaging service that enables this pattern, providing a fully managed, globally distributed message bus that can ingest millions of messages per second with sub-second latency. EDA decouples producers from consumers, allowing each service to scale independently, fail gracefully, and evolve without coordinating deployments across teams.
The event-driven model on GCP extends beyond Pub/Sub alone. Eventarc provides a unified eventing layer that routes events from over 130 GCP sources (Cloud Storage uploads, BigQuery job completions, Firestore document changes, and more) to event handlers like Cloud Run, Cloud Functions, and GKE services. Together, Pub/Sub and Eventarc form the backbone of modern, loosely coupled architectures on Google Cloud.
Common event-driven patterns on GCP include:
- Fan-out: A single event published to a topic is delivered to multiple subscriptions, each processing the event differently (e.g., one writes to a database, another sends a notification, a third updates a search index).
- Event sourcing: All state changes are captured as an immutable sequence of events in Pub/Sub, with consumers materializing views from the event stream.
- Choreography: Services react to events from other services without a central orchestrator, each publishing new events that trigger downstream processing.
- CQRS: Commands are processed by one service and published as events. Query services consume events to maintain optimized read models.
- Streaming pipelines: Pub/Sub feeds real-time data into Dataflow for stream processing, windowed aggregations, and real-time analytics.
When NOT to Use Event-Driven Architecture
Event-driven architecture adds complexity. It is not the right choice for every use case. Avoid EDA for simple CRUD applications with a single consumer, request-response patterns where the client needs an immediate result, scenarios requiring strong consistency across services, or systems where the operational overhead of managing topics, subscriptions, and dead-letter queues outweighs the benefits. Start with synchronous calls and introduce events only when you have a clear need for decoupling or fan-out.
Pub/Sub Core Concepts
Pub/Sub is built around four fundamental concepts: topics, subscriptions, messages, and acknowledgments. A publisher sends a message to a topic. The topic fans out the message to all attached subscriptions. Each subscription delivers the message to one or more subscriber clients. A subscriber processes the message and sends an acknowledgment (ack) back to Pub/Sub, which removes the message from the subscription's backlog. If no ack is received within the acknowledgment deadline, Pub/Sub redelivers the message.
Key characteristics of Pub/Sub:
| Property | Value | Notes |
|---|---|---|
| Message size limit | 10 MB | Use Cloud Storage for larger payloads with a reference in the message |
| Message retention | 7 days (default), up to 31 days | Configurable per topic; messages are deleted after the retention period |
| Throughput | Millions of messages/sec per topic | Auto-scales; no pre-provisioning needed |
| Latency | < 100ms (p99) | End-to-end publish-to-deliver latency within a single region |
| Ordering guarantee | None by default; per-key ordering with ordering keys | Unordered delivery maximizes throughput |
| Delivery guarantee | At-least-once (default); exactly-once available | Exactly-once requires pull subscriptions with exactly-once enabled |
| Global availability | Messages stored in multiple zones | Configurable message storage policy for data residency |
# Create a topic
gcloud pubsub topics create order-events \
--message-retention-duration=7d \
--labels=team=platform,env=production
# Create a topic with a schema (Avro or Protocol Buffers)
gcloud pubsub topics create user-events \
--schema=projects/my-project/schemas/user-event-schema \
--message-encoding=JSON
# Create a pull subscription
gcloud pubsub subscriptions create order-processing \
--topic=order-events \
--ack-deadline=60 \
--message-retention-duration=7d \
--expiration-period=never \
--enable-exactly-once-delivery \
--labels=team=orders,env=production
# Create a push subscription that delivers to Cloud Run
gcloud pubsub subscriptions create order-notifications \
--topic=order-events \
--push-endpoint=https://notification-service-abc123.run.app/events \
--push-auth-service-account=pubsub-push@my-project.iam.gserviceaccount.com \
--ack-deadline=30 \
--max-delivery-attempts=5 \
--dead-letter-topic=projects/my-project/topics/order-events-dlq \
--min-retry-delay=10s \
--max-retry-delay=600s
# Create a BigQuery subscription (writes directly to BigQuery)
gcloud pubsub subscriptions create order-analytics \
--topic=order-events \
--bigquery-table=my-project:analytics.order_events \
--write-metadata \
--use-topic-schema
# Publish a test message
gcloud pubsub topics publish order-events \
--message='{"order_id":"ORD-001","status":"created","total":99.99}' \
--attribute='event_type=order.created,source=api'
# Pull messages for testing
gcloud pubsub subscriptions pull order-processing --limit=10 --auto-ackPush vs Pull Subscriptions
Pub/Sub offers two delivery mechanisms: pull and push. The choice between them significantly affects your architecture, scaling behavior, error handling, and operational complexity. Understanding the trade-offs is essential for building reliable event-driven systems.
| Feature | Pull Subscription | Push Subscription |
|---|---|---|
| Delivery mechanism | Subscriber polls Pub/Sub for messages | Pub/Sub sends HTTP POST to subscriber endpoint |
| Scaling | Subscriber controls concurrency | Pub/Sub controls delivery rate (with flow control) |
| Authentication | Subscriber authenticates to Pub/Sub | Pub/Sub authenticates to subscriber via OIDC token |
| Best for | High-throughput, GKE/Compute Engine workloads | Cloud Run, Cloud Functions, serverless endpoints |
| Exactly-once | Supported | Not supported |
| Ordering keys | Supported | Supported |
| Network | Works behind firewall/VPC | Requires publicly reachable HTTPS endpoint |
| Backpressure | Natural via subscriber concurrency | Managed by Pub/Sub; subscriber can return non-2xx to nack |
from google.cloud import pubsub_v1
from concurrent.futures import TimeoutError
import json
import time
project_id = "my-project"
subscription_id = "order-processing"
subscriber = pubsub_v1.SubscriberClient()
subscription_path = subscriber.subscription_path(project_id, subscription_id)
def process_message(message):
"""Process an individual message."""
print(f"Received message ID: {message.message_id}")
print(f"Data: {message.data.decode('utf-8')}")
print(f"Attributes: {dict(message.attributes)}")
print(f"Publish time: {message.publish_time}")
print(f"Ordering key: {message.ordering_key}")
try:
data = json.loads(message.data.decode("utf-8"))
# Process the order event
if message.attributes.get("event_type") == "order.created":
handle_new_order(data)
elif message.attributes.get("event_type") == "order.paid":
handle_payment_confirmation(data)
# Acknowledge successful processing
message.ack()
print(f"Acknowledged message {message.message_id}")
except json.JSONDecodeError:
# Bad message format - ack to prevent infinite retry
print(f"Invalid JSON in message {message.message_id}, acking to discard")
message.ack()
except TransientError:
# Transient error - nack to retry
print(f"Transient error, nacking message {message.message_id}")
message.nack()
except Exception as e:
# Unexpected error - nack with a delay
print(f"Unexpected error: {e}")
message.modify_ack_deadline(30) # Extend deadline before nack
message.nack()
# Configure flow control to manage concurrency
flow_control = pubsub_v1.types.FlowControl(
max_messages=100, # Max outstanding messages
max_bytes=10 * 1024 * 1024, # Max outstanding bytes (10 MB)
max_lease_duration=600, # Max time to hold a message lease
)
# Start the subscriber with streaming pull
streaming_pull_future = subscriber.subscribe(
subscription_path,
callback=process_message,
flow_control=flow_control,
)
print(f"Listening for messages on {subscription_path}...")
try:
# Block the main thread; timeout=None for indefinite listening
streaming_pull_future.result(timeout=None)
except TimeoutError:
streaming_pull_future.cancel()
streaming_pull_future.result() # Wait for cleanup
except KeyboardInterrupt:
streaming_pull_future.cancel()
print("Subscriber shut down.")# Cloud Run service that receives Pub/Sub push messages
from flask import Flask, request, jsonify
import base64
import json
import logging
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
@app.route("/events", methods=["POST"])
def handle_pubsub_message():
"""Handle incoming Pub/Sub push message."""
envelope = request.get_json()
if not envelope:
return jsonify({"error": "No JSON payload"}), 400
if "message" not in envelope:
return jsonify({"error": "No message in envelope"}), 400
pubsub_message = envelope["message"]
# Decode the message data
if "data" in pubsub_message:
data = base64.b64decode(pubsub_message["data"]).decode("utf-8")
payload = json.loads(data)
else:
payload = {}
attributes = pubsub_message.get("attributes", {})
message_id = pubsub_message.get("messageId", "unknown")
logging.info(f"Processing message {message_id}: {payload}")
try:
# Process based on event type
event_type = attributes.get("event_type")
if event_type == "order.created":
send_order_confirmation_email(payload)
elif event_type == "order.shipped":
send_shipping_notification(payload)
else:
logging.warning(f"Unknown event type: {event_type}")
# Return 2xx to acknowledge the message
return jsonify({"status": "processed", "messageId": message_id}), 200
except Exception as e:
logging.error(f"Error processing message {message_id}: {e}")
# Return non-2xx to nack the message (Pub/Sub will retry)
return jsonify({"error": str(e)}), 500
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)Push Subscription Authentication
Always enable authentication on push subscriptions. Configure the subscription with a service account, and Pub/Sub will include a signed OIDC token in the Authorization header of each push request. In your Cloud Run service, verify the token's audience claim matches your service URL and the email claim matches your push service account. Cloud Run handles this automatically when you restrict ingress to "internal and Cloud Load Balancing" traffic.
Exactly-Once Delivery
By default, Pub/Sub provides at-least-once delivery, meaning a message may be delivered more than once if the ack is lost or delayed. For many use cases (analytics, logging, notifications), this is acceptable. However, for payment processing, inventory updates, and other idempotency-sensitive operations, exactly-once delivery prevents duplicate processing without requiring application-level deduplication logic.
Exactly-once delivery is available only for pull subscriptions and works by assigning each message a unique delivery attempt ID. The subscriber client tracks acknowledged messages and prevents redelivery of already-acked messages within the deduplication window. This feature is supported by the Pub/Sub client libraries for Java, Python, Go, C++, and Node.js.
Important caveats with exactly-once delivery:
- Regional constraint: Messages are deduplicated within a single cloud region. Cross-region message storage policies may affect deduplication behavior.
- Performance impact: Exactly-once delivery adds latency to ack operations (typically 50-100ms additional) because the ack must be persisted before confirmation.
- Redelivery on failure: If a subscriber crashes after processing but before acknowledging, the message will be redelivered. Your processing logic must still be idempotent for this edge case.
- Not available for push: Push subscriptions do not support exactly-once delivery. For push-based architectures, you need application-level deduplication.
# Create a subscription with exactly-once delivery enabled
gcloud pubsub subscriptions create payment-processing \
--topic=payment-events \
--enable-exactly-once-delivery \
--ack-deadline=60 \
--message-retention-duration=1d
# Check if exactly-once is enabled on an existing subscription
gcloud pubsub subscriptions describe payment-processing \
--format='value(enableExactlyOnceDelivery)'
# Update an existing subscription to enable exactly-once
gcloud pubsub subscriptions update payment-processing \
--enable-exactly-once-deliveryDead-Letter Topics & Error Handling
Dead-letter topics (DLTs) are essential for handling messages that cannot be successfully processed. When a message exceeds the maximum number of delivery attempts, Pub/Sub automatically forwards it to the dead-letter topic instead of continuing to retry. This prevents "poison pill" messages from blocking the subscription and consuming resources indefinitely.
A well-designed dead-letter strategy includes:
- Maximum delivery attempts: Configure between 5 and 100 attempts. Lower values (5-10) are appropriate for idempotent operations that will fail consistently; higher values (50-100) suit operations that may recover from transient failures.
- DLT monitoring: Create a subscription on the dead-letter topic and set up alerting when messages arrive. Each dead-letter message is a processing failure that needs investigation.
- DLT replay: Build tooling to inspect dead-letter messages, fix the underlying issue, and republish them to the original topic for reprocessing.
- DLT retention: Set a longer retention period on dead-letter topics (e.g., 14-31 days) to give your team time to investigate and fix issues.
# Create the dead-letter topic
gcloud pubsub topics create order-events-dlq \
--message-retention-duration=31d
# Create a subscription on the DLT for monitoring/replay
gcloud pubsub subscriptions create order-events-dlq-monitor \
--topic=order-events-dlq \
--ack-deadline=120 \
--message-retention-duration=31d \
--expiration-period=never
# Update the main subscription to use the dead-letter topic
gcloud pubsub subscriptions update order-processing \
--dead-letter-topic=projects/my-project/topics/order-events-dlq \
--max-delivery-attempts=10 \
--min-retry-delay=10s \
--max-retry-delay=600s
# Grant Pub/Sub permission to publish to the DLT
# The Pub/Sub service account needs pubsub.publisher on the DLT
PROJECT_NUMBER=$(gcloud projects describe my-project --format='value(projectNumber)')
gcloud pubsub topics add-iam-policy-binding order-events-dlq \
--member="serviceAccount:service-${PROJECT_NUMBER}@gcp-sa-pubsub.iam.gserviceaccount.com" \
--role="roles/pubsub.publisher"
# Grant Pub/Sub permission to ack messages on the source subscription
gcloud pubsub subscriptions add-iam-policy-binding order-processing \
--member="serviceAccount:service-${PROJECT_NUMBER}@gcp-sa-pubsub.iam.gserviceaccount.com" \
--role="roles/pubsub.subscriber"
# Create an alerting policy for DLT messages
gcloud beta monitoring policies create \
--display-name="Dead Letter Queue Alert" \
--condition-display-name="Messages in DLQ" \
--condition-filter='resource.type="pubsub_subscription"
AND resource.labels.subscription_id="order-events-dlq-monitor"
AND metric.type="pubsub.googleapis.com/subscription/num_undelivered_messages"' \
--condition-threshold-value=1 \
--condition-threshold-comparison=COMPARISON_GT \
--condition-threshold-duration=60s \
--notification-channels=projects/my-project/notificationChannels/CHANNEL_IDDLT Permission Requirements
The Pub/Sub service account service-PROJECT_NUMBER@gcp-sa-pubsub.iam.gserviceaccount.com must have roles/pubsub.publisher on the dead-letter topic and roles/pubsub.subscriber on the source subscription. Without these permissions, dead-letter forwarding fails silently and messages continue to be redelivered to the original subscription indefinitely, defeating the entire purpose of the DLT configuration.
Eventarc Integration
Eventarc is GCP's unified eventing layer that routes events from over 130 GCP sources, third-party SaaS providers, and custom applications to event handlers. Eventarc builds on top of Pub/Sub and Cloud Audit Logs to provide a standardized, CloudEvents-compatible event delivery system. Instead of manually creating Pub/Sub topics and configuring triggers, Eventarc lets you declare event triggers that automatically wire up the infrastructure.
Eventarc supports three types of event sources:
- Direct events: Published by GCP services directly through Pub/Sub. Examples include Cloud Storage object changes, Firestore document writes, and Firebase Authentication events.
- Audit Log events: Generated from Cloud Audit Logs when API calls are made to any GCP service. This covers virtually every GCP operation: VM creation, IAM changes, BigQuery queries, and more.
- Third-party events: Published by integrated SaaS providers through Eventarc's partner event sources (e.g., Datadog, Check Point).
# Trigger Cloud Run when a file is uploaded to Cloud Storage
gcloud eventarc triggers create process-upload \
--location=us-central1 \
--destination-run-service=file-processor \
--destination-run-region=us-central1 \
--event-filters="type=google.cloud.storage.object.v1.finalized" \
--event-filters="bucket=my-uploads-bucket" \
--service-account=eventarc-sa@my-project.iam.gserviceaccount.com
# Trigger Cloud Run on BigQuery job completion via Audit Logs
gcloud eventarc triggers create bq-job-complete \
--location=us-central1 \
--destination-run-service=etl-notifier \
--destination-run-region=us-central1 \
--event-filters="type=google.cloud.audit.log.v1.written" \
--event-filters="serviceName=bigquery.googleapis.com" \
--event-filters="methodName=google.cloud.bigquery.v2.JobService.InsertJob" \
--service-account=eventarc-sa@my-project.iam.gserviceaccount.com
# Trigger Cloud Functions (2nd gen) on Firestore document writes
gcloud eventarc triggers create user-change-handler \
--location=us-central1 \
--destination-function=on-user-change \
--destination-function-region=us-central1 \
--event-filters="type=google.cloud.firestore.document.v1.written" \
--event-filters-path-pattern="database=(default)/documents/users/{userId}" \
--service-account=eventarc-sa@my-project.iam.gserviceaccount.com
# List all triggers
gcloud eventarc triggers list --location=us-central1
# Describe a trigger
gcloud eventarc triggers describe process-upload --location=us-central1Ordering & Schema Validation
By default, Pub/Sub does not guarantee message ordering. Messages published to a topic may be delivered to subscribers in any order, which maximizes throughput and allows Pub/Sub to distribute messages across multiple servers. However, many use cases require ordered delivery. For example, processing a sequence of state changes for a specific entity (order created → order paid → order shipped).
Ordering keys solve this by grouping messages into ordered sequences. When you publish a message with an ordering key, Pub/Sub guarantees that messages with the same key are delivered to the subscriber in the order they were published. Different ordering keys are processed independently, maintaining parallelism across entities while preserving per-entity order.
from google.cloud import pubsub_v1
publisher = pubsub_v1.PublisherClient()
topic_path = publisher.topic_path("my-project", "order-events")
# Enable message ordering on the publisher
publisher_options = pubsub_v1.types.PublisherOptions(
enable_message_ordering=True,
)
publisher = pubsub_v1.PublisherClient(publisher_options=publisher_options)
# Publish ordered messages for a specific order
order_id = "ORD-12345"
events = [
{"event": "order.created", "data": {"total": 99.99}},
{"event": "order.payment_received", "data": {"payment_id": "PAY-001"}},
{"event": "order.shipped", "data": {"tracking": "TRACK-789"}},
{"event": "order.delivered", "data": {"signed_by": "Jane Doe"}},
]
for event in events:
import json
data = json.dumps(event).encode("utf-8")
future = publisher.publish(
topic_path,
data,
ordering_key=order_id, # Same key ensures in-order delivery
event_type=event["event"],
)
print(f"Published {event['event']} with message ID: {future.result()}")
# IMPORTANT: If a publish with an ordering key fails, all subsequent
# publishes with that key will fail. You must call resume_publish()
# to clear the error state:
try:
future = publisher.publish(topic_path, b"data", ordering_key="key-1")
future.result()
except Exception:
publisher.resume_publish("my-project", "order-events", "key-1")Schema Validation
Pub/Sub supports schema validation using Avro or Protocol Buffers. When a schema is attached to a topic, every published message is validated against the schema before being accepted. Invalid messages are rejected with an error, preventing malformed data from entering your event pipeline. Schemas can be versioned and evolved following compatibility rules (backward, forward, or full compatibility).
# Create an Avro schema
gcloud pubsub schemas create order-event-schema \
--type=AVRO \
--definition='{
"type": "record",
"name": "OrderEvent",
"fields": [
{"name": "order_id", "type": "string"},
{"name": "event_type", "type": {"type": "enum", "name": "EventType",
"symbols": ["CREATED", "PAID", "SHIPPED", "DELIVERED", "CANCELLED"]}},
{"name": "timestamp", "type": {"type": "long", "logicalType": "timestamp-millis"}},
{"name": "total_amount", "type": ["null", "double"], "default": null},
{"name": "customer_id", "type": "string"},
{"name": "metadata", "type": {"type": "map", "values": "string"}, "default": {}}
]
}'
# Create a Protocol Buffers schema
gcloud pubsub schemas create user-event-schema \
--type=PROTOCOL_BUFFER \
--definition='syntax = "proto3";
message UserEvent {
string user_id = 1;
string event_type = 2;
int64 timestamp = 3;
string email = 4;
map<string, string> properties = 5;
}'
# Attach a schema to a topic
gcloud pubsub topics create order-events-validated \
--schema=order-event-schema \
--message-encoding=JSON
# Validate a message against a schema (dry run)
gcloud pubsub schemas validate-message \
--schema-name=order-event-schema \
--message-encoding=JSON \
--message='{"order_id":"ORD-001","event_type":"CREATED","timestamp":1700000000000,"customer_id":"CUST-001"}'
# List schema revisions
gcloud pubsub schemas list-revisions order-event-schema
# Commit a new schema revision
gcloud pubsub schemas commit order-event-schema \
--type=AVRO \
--definition='...'Pub/Sub with Dataflow
Pub/Sub and Dataflow form a powerful combination for real-time stream processing. Pub/Sub handles ingestion and buffering while Dataflow (built on Apache Beam) performs transformations, aggregations, windowing, and enrichment before writing results to downstream sinks like BigQuery, Cloud Storage, Bigtable, or another Pub/Sub topic. This architecture is the standard pattern for real-time analytics, ETL, and event processing pipelines on GCP.
import apache_beam as beam
from apache_beam.options.pipeline_options import PipelineOptions, StandardOptions
from apache_beam.transforms.window import FixedWindows, SlidingWindows
from apache_beam.transforms.trigger import AfterWatermark, AccumulationMode
import json
class ParseAndValidate(beam.DoFn):
"""Parse JSON messages and validate required fields."""
def process(self, element):
try:
data = json.loads(element.decode("utf-8"))
if "order_id" not in data or "total" not in data:
# Send invalid messages to dead-letter output
yield beam.pvalue.TaggedOutput("dead_letter", element)
return
yield data
except json.JSONDecodeError:
yield beam.pvalue.TaggedOutput("dead_letter", element)
class EnrichWithCustomerData(beam.DoFn):
"""Enrich order events with customer data from Firestore."""
def setup(self):
from google.cloud import firestore
self.db = firestore.Client()
def process(self, element):
customer_ref = self.db.collection("customers").document(
element["customer_id"]
)
customer = customer_ref.get()
if customer.exists:
element["customer_name"] = customer.get("name")
element["customer_tier"] = customer.get("tier")
yield element
def run():
options = PipelineOptions()
options.view_as(StandardOptions).streaming = True
with beam.Pipeline(options=options) as p:
# Read from Pub/Sub
messages = (
p
| "Read from Pub/Sub" >> beam.io.ReadFromPubSub(
subscription="projects/my-project/subscriptions/order-analytics",
with_attributes=True,
)
| "Extract Data" >> beam.Map(lambda msg: msg.data)
)
# Parse and validate
parsed = messages | "Parse" >> beam.ParDo(
ParseAndValidate()
).with_outputs("dead_letter", main="valid")
# Process valid messages
enriched = (
parsed["valid"]
| "Enrich" >> beam.ParDo(EnrichWithCustomerData())
)
# Window into 5-minute fixed windows for aggregation
windowed = (
enriched
| "Window" >> beam.WindowInto(
FixedWindows(300), # 5-minute windows
trigger=AfterWatermark(),
accumulation_mode=AccumulationMode.DISCARDING,
)
)
# Aggregate revenue by customer tier
revenue_by_tier = (
windowed
| "Key by Tier" >> beam.Map(
lambda x: (x.get("customer_tier", "unknown"), x["total"])
)
| "Sum Revenue" >> beam.CombinePerKey(sum)
| "Format" >> beam.Map(
lambda kv: {"tier": kv[0], "revenue": kv[1]}
)
)
# Write enriched events to BigQuery
enriched | "Write to BQ" >> beam.io.WriteToBigQuery(
table="my-project:analytics.enriched_orders",
schema="order_id:STRING,total:FLOAT,customer_id:STRING,"
"customer_name:STRING,customer_tier:STRING",
write_disposition=beam.io.BigQueryDisposition.WRITE_APPEND,
create_disposition=beam.io.BigQueryDisposition.CREATE_IF_NEEDED,
)
# Write dead letters to a separate topic
parsed["dead_letter"] | "Dead Letter" >> beam.io.WriteToPubSub(
topic="projects/my-project/topics/order-events-dlq"
)
if __name__ == "__main__":
run()Security & IAM
Pub/Sub security follows the principle of least privilege. Every publisher and subscriber should have only the permissions it needs, scoped to the specific topics and subscriptions it accesses. Pub/Sub provides granular IAM roles that control who can create, delete, publish to, and subscribe from topics and subscriptions.
| Role | Permissions | Typical User |
|---|---|---|
roles/pubsub.publisher | Publish messages to topics | Application service accounts that produce events |
roles/pubsub.subscriber | Consume messages from subscriptions, ack/nack | Application service accounts that consume events |
roles/pubsub.viewer | List and get topics/subscriptions/schemas | Monitoring and debugging tools |
roles/pubsub.editor | Create, update, delete topics/subscriptions | CI/CD pipelines, platform team service accounts |
roles/pubsub.admin | Full access including IAM management | Infrastructure admins only |
# Grant publisher access to a specific topic
gcloud pubsub topics add-iam-policy-binding order-events \
--member="serviceAccount:order-api@my-project.iam.gserviceaccount.com" \
--role="roles/pubsub.publisher"
# Grant subscriber access to a specific subscription
gcloud pubsub subscriptions add-iam-policy-binding order-processing \
--member="serviceAccount:order-worker@my-project.iam.gserviceaccount.com" \
--role="roles/pubsub.subscriber"
# Configure Customer-Managed Encryption Keys (CMEK)
gcloud pubsub topics create sensitive-events \
--topic-encryption-key=projects/my-project/locations/us-central1/keyRings/pubsub-ring/cryptoKeys/pubsub-key
# Grant Pub/Sub access to the KMS key
gcloud kms keys add-iam-policy-binding pubsub-key \
--location=us-central1 \
--keyring=pubsub-ring \
--member="serviceAccount:service-PROJECT_NUMBER@gcp-sa-pubsub.iam.gserviceaccount.com" \
--role="roles/cloudkms.cryptoKeyEncrypterDecrypter"
# Restrict message storage to specific regions (data residency)
gcloud pubsub topics update order-events \
--message-storage-policy-allowed-regions=us-central1,us-east1Best Practices & Architecture Patterns
Building reliable event-driven systems requires careful attention to message design, error handling, and operational practices. The following best practices are drawn from Google's own recommendations and production experience with Pub/Sub at scale.
Message Design
- Keep messages small: Messages should contain the event data needed for processing, not entire entity snapshots. For large payloads, store the data in Cloud Storage and include a reference URI in the message.
- Use attributes for routing: Put event type, source, and version information in message attributes rather than the payload. Attributes can be used in subscription filters without parsing the message body.
- Include a message schema version: Always include a schema version in your messages to support backward-compatible evolution. Consumers should gracefully handle unknown fields.
- Use CloudEvents format: Adopt the CloudEvents specification for event metadata. This provides a standard envelope with
type,source,specversion,id, andtimefields, making events interoperable with Eventarc and other CloudEvents-compatible systems.
Subscription Filters
Pub/Sub subscription filters let you receive only a subset of messages published to a topic, based on message attributes. This is more efficient than subscribing to all messages and discarding irrelevant ones in your application code, because filtered messages never leave Pub/Sub and do not count toward delivery charges.
# Terraform configuration for a complete Pub/Sub setup
resource "google_pubsub_topic" "order_events" {
name = "order-events"
message_retention_duration = "604800s" # 7 days
schema_settings {
schema = google_pubsub_schema.order_event.id
encoding = "JSON"
}
labels = {
team = "platform"
environment = "production"
}
}
resource "google_pubsub_schema" "order_event" {
name = "order-event-schema"
type = "AVRO"
definition = file("schemas/order-event.avsc")
}
# Filtered subscription - only receives "order.shipped" events
resource "google_pubsub_subscription" "shipping_notifications" {
name = "shipping-notifications"
topic = google_pubsub_topic.order_events.id
filter = "attributes.event_type = "order.shipped""
push_config {
push_endpoint = google_cloud_run_service.shipping_notifier.status[0].url
oidc_token {
service_account_email = google_service_account.pubsub_push.email
audience = google_cloud_run_service.shipping_notifier.status[0].url
}
}
retry_policy {
minimum_backoff = "10s"
maximum_backoff = "600s"
}
dead_letter_policy {
dead_letter_topic = google_pubsub_topic.order_events_dlq.id
max_delivery_attempts = 10
}
expiration_policy {
ttl = "" # Never expires
}
}
resource "google_pubsub_topic" "order_events_dlq" {
name = "order-events-dlq"
message_retention_duration = "2678400s" # 31 days
}
# BigQuery subscription for analytics
resource "google_pubsub_subscription" "order_analytics" {
name = "order-analytics-bq"
topic = google_pubsub_topic.order_events.id
bigquery_config {
table = "${var.project_id}:analytics.order_events"
write_metadata = true
use_topic_schema = true
}
}Monitoring Pub/Sub Health
Monitor these key metrics to ensure healthy Pub/Sub operations: subscription/num_undelivered_messages (backlog size), subscription/oldest_unacked_message_age (processing lag), subscription/dead_letter_message_count (failed messages), and topic/send_request_count (publish throughput). Set alerts on oldest_unacked_message_age exceeding your SLO's acceptable processing delay. This is the single most important indicator that your consumers cannot keep up with the message volume.
Key Takeaways
- 1Pub/Sub provides global, scalable messaging with at-least-once delivery and 7-day retention.
- 2Pull subscriptions give consumers control over message flow; push subscriptions deliver to HTTP endpoints.
- 3Exactly-once delivery eliminates duplicate processing for critical workflows.
- 4Eventarc provides a unified event routing layer connecting GCP services, Pub/Sub, and third-party sources.
- 5Ordering keys guarantee message order within a key while maintaining parallel processing across keys.
- 6Schema validation enforces Avro or Protocol Buffer schemas at the topic level.
Frequently Asked Questions
What is the difference between Pub/Sub and Eventarc?
How does exactly-once delivery work?
How much does Pub/Sub cost?
Can I use Pub/Sub for cross-project messaging?
What are dead-letter topics?
Written by CloudToolStack Team
Cloud engineers and architects with hands-on experience across AWS, Azure, and GCP. We write guides based on real-world production patterns, not just documentation rewrites.
Disclaimer: This guide is for educational purposes. Cloud services change frequently; always refer to official documentation for the latest information. AWS, Azure, and GCP are trademarks of their respective owners.