Skip to main content
GCPMessaging & Eventsintermediate

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.

CloudToolStack Team25 min readPublished Feb 22, 2026

Prerequisites

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:

PropertyValueNotes
Message size limit10 MBUse Cloud Storage for larger payloads with a reference in the message
Message retention7 days (default), up to 31 daysConfigurable per topic; messages are deleted after the retention period
ThroughputMillions of messages/sec per topicAuto-scales; no pre-provisioning needed
Latency< 100ms (p99)End-to-end publish-to-deliver latency within a single region
Ordering guaranteeNone by default; per-key ordering with ordering keysUnordered delivery maximizes throughput
Delivery guaranteeAt-least-once (default); exactly-once availableExactly-once requires pull subscriptions with exactly-once enabled
Global availabilityMessages stored in multiple zonesConfigurable message storage policy for data residency
Create topics and subscriptions with gcloud
# 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-ack

Push 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.

FeaturePull SubscriptionPush Subscription
Delivery mechanismSubscriber polls Pub/Sub for messagesPub/Sub sends HTTP POST to subscriber endpoint
ScalingSubscriber controls concurrencyPub/Sub controls delivery rate (with flow control)
AuthenticationSubscriber authenticates to Pub/SubPub/Sub authenticates to subscriber via OIDC token
Best forHigh-throughput, GKE/Compute Engine workloadsCloud Run, Cloud Functions, serverless endpoints
Exactly-onceSupportedNot supported
Ordering keysSupportedSupported
NetworkWorks behind firewall/VPCRequires publicly reachable HTTPS endpoint
BackpressureNatural via subscriber concurrencyManaged by Pub/Sub; subscriber can return non-2xx to nack
Pull subscriber in Python with concurrency control
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.")
Push subscriber with Cloud Run
# 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.
Enable exactly-once delivery on a subscription
# 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-delivery

Dead-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.
Configure dead-letter topics and monitoring
# 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_ID

DLT 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).
Create Eventarc triggers
# 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-central1

Ordering & 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.

Publishing with ordering keys in Python
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 and use Pub/Sub schemas
# 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.

Dataflow streaming pipeline with Pub/Sub (Apache Beam Python)
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.

RolePermissionsTypical User
roles/pubsub.publisherPublish messages to topicsApplication service accounts that produce events
roles/pubsub.subscriberConsume messages from subscriptions, ack/nackApplication service accounts that consume events
roles/pubsub.viewerList and get topics/subscriptions/schemasMonitoring and debugging tools
roles/pubsub.editorCreate, update, delete topics/subscriptionsCI/CD pipelines, platform team service accounts
roles/pubsub.adminFull access including IAM managementInfrastructure admins only
Configure Pub/Sub IAM and CMEK encryption
# 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-east1

Best 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, and time fields, 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.

Subscription filters and Terraform configuration
# 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

  1. 1Pub/Sub provides global, scalable messaging with at-least-once delivery and 7-day retention.
  2. 2Pull subscriptions give consumers control over message flow; push subscriptions deliver to HTTP endpoints.
  3. 3Exactly-once delivery eliminates duplicate processing for critical workflows.
  4. 4Eventarc provides a unified event routing layer connecting GCP services, Pub/Sub, and third-party sources.
  5. 5Ordering keys guarantee message order within a key while maintaining parallel processing across keys.
  6. 6Schema validation enforces Avro or Protocol Buffer schemas at the topic level.

Frequently Asked Questions

What is the difference between Pub/Sub and Eventarc?
Pub/Sub is the underlying messaging infrastructure for publishing and subscribing to messages. Eventarc is an event routing layer built on top of Pub/Sub that provides a declarative way to connect event sources (Cloud Storage, Cloud Audit Logs, custom applications) to event targets (Cloud Run, Cloud Functions, GKE).
How does exactly-once delivery work?
Exactly-once delivery in Pub/Sub uses message deduplication and acknowledgment tracking to ensure each message is delivered and processed exactly once. Subscribers must acknowledge messages within the ack deadline. Pub/Sub tracks acked messages to prevent redelivery.
How much does Pub/Sub cost?
Pub/Sub charges $40/TB for message delivery (first 10 GB/month free). Storage for retained messages costs $0.27/GB/month. Seek-related operations and snapshot storage have additional costs. For most applications, the free tier covers development and low-traffic production.
Can I use Pub/Sub for cross-project messaging?
Yes. Pub/Sub topics and subscriptions support cross-project access via IAM. You can publish from one project and subscribe from another by granting the appropriate Pub/Sub roles. This is common for centralized event processing across multiple projects.
What are dead-letter topics?
Dead-letter topics receive messages that fail processing after a configured number of delivery attempts (max_delivery_attempts). This prevents poison messages from blocking the subscription. Monitor dead-letter topics and set up alerting for messages that arrive there.

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.