5.3 Pub/Sub BigQuery/Cloud Storage Direct Subscriptions and Dead-Letter Topics
Key Takeaways
- BigQuery direct subscriptions write streaming messages directly from Pub/Sub into BigQuery tables without intermediate compute (Dataflow, Cloud Functions), drastically lowering architectural complexity, operational overhead, and latency.
- BigQuery direct subscriptions require the service account service-{project-number}@gcp-sa-pubsub.iam.gserviceaccount.com to possess roles/bigquery.dataEditor and support use_topic_schema, write_metadata, and drop_unknown_fields.
- Cloud Storage direct subscriptions stream batched messages directly into GCS buckets as raw text or Avro files, governed by batching flush triggers: max_duration (up to 10 minutes), max_bytes (up to 10 GiB), or max_messages.
- Dead-Letter Topics (DLQ) isolate poison pills and malformed payloads by automatically forwarding messages that exceed maxDeliveryAttempts (5 to 100 attempts) to a secondary topic, preventing infinite redelivery loops.
- When direct subscriptions encounter fatal schema incompatibilities or data type mismatches, messages are routed directly to the configured Dead-Letter Topic rather than silently dropped or permanently blocking ingestion.
5.3 Pub/Sub BigQuery/Cloud Storage Direct Subscriptions and Dead-Letter Topics
Exam Focus: The Google Cloud Professional Data Engineer exam expects you to recognize when to replace complex streaming ETL pipelines (Cloud Dataflow or Cloud Functions) with zero-compute Direct Subscriptions to BigQuery and Cloud Storage. You must master schema mapping flags (
use_topic_schema,write_metadata,drop_unknown_fields), understand Cloud Storage batching triggers (max_duration,max_bytes), configure Dead-Letter Topics (DLQ), and assign the exact IAM permissions required by the Pub/Sub service agent.
Historically, landing real-time streaming data from Cloud Pub/Sub into analytical data stores required deploying intermediate compute infrastructure. Engineers had to provision, monitor, and scale Apache Beam pipelines on Cloud Dataflow or maintain dozens of event-driven Cloud Functions simply to deserialize JSON payloads and stream them into BigQuery or write them as files into Cloud Storage. With the introduction of Pub/Sub Direct Subscriptions, Google Cloud eliminated intermediate compute for straightforward ingestion, providing serverless, zero-maintenance streaming pipelines at wire speed.
1. BigQuery Direct Subscriptions: Serverless, Zero-Pipeline Ingestion
A BigQuery Direct Subscription writes messages directly from a Cloud Pub/Sub topic into an existing BigQuery table without requiring a Dataflow worker pool or serverless functions.
[ Publishers ] ──> [ Pub/Sub Topic ] ──(Direct Subscription)──> [ BigQuery Table ]
│
(Under the Hood)
▼
[ BigQuery Storage Write API ]
(Zero VM Compute, Sub-Second Latency)
How BigQuery Direct Subscriptions Operate
Under the hood, Cloud Pub/Sub integrates directly with the BigQuery Storage Write API using committed streams. Pub/Sub handles all connection lifecycle management, batching, auto-scaling, and backoff retries internally:
- Cost Efficiency: Customers pay only for standard Pub/Sub delivery fees and BigQuery Storage Write API volume ingestion charges ($0.025 per GB, with the first 2 TB per month free). There are zero virtual machine, memory, or worker compute costs.
- Latency: Direct subscriptions achieve sub-second delivery latency from message publication to table queryability.
Key Configuration Parameters
When provisioning a BigQuery direct subscription, data engineers configure three critical behaviors:
gcloud pubsub subscriptions create bq-direct-sub \
--topic=telemetry-topic \
--bigquery-table=my-project:analytics_lake.sensor_telemetry \
--use-topic-schema \
--write-metadata \
--drop-unknown-fields
-
use_topic_schema:- When enabled, Pub/Sub parses message payloads using the schema registered on the topic (Avro or Protocol Buffers) and maps fields directly to corresponding BigQuery columns with matching names and compatible data types.
- If disabled (or if the topic has no schema), Pub/Sub writes the raw, unparsed message payload into a designated string or JSON column named
datain the destination table.
-
write_metadata:- Automatically injects system metadata columns into the destination BigQuery table alongside message fields. The BigQuery table schema must define these exact column names:
subscription_name(STRING): Fully qualified resource name of the subscription.message_id(STRING): Unique identifier assigned by Pub/Sub.publish_time(TIMESTAMP): Time the message was committed to Pub/Sub storage.attributes(JSONorSTRING): Key-value dictionary of user-defined message attributes.ordering_key(STRING): Present if message ordering was used.
- Architecture Value: Essential for data lineage, auditing, partition clustering, and downstream deduplication queries.
- Automatically injects system metadata columns into the destination BigQuery table alongside message fields. The BigQuery table schema must define these exact column names:
-
drop_unknown_fields:- Dictates how Pub/Sub handles schema evolution when upstream producers send records containing fields not present in the BigQuery table schema.
- If
true: Pub/Sub automatically ignores and drops any unrecognized fields, successfully writing all matching fields into BigQuery. - If
false: Pub/Sub rejects the message write operation because the record does not conform to the BigQuery table schema. The message remains unacknowledged and will either trigger delivery retries or be routed to a Dead-Letter Topic.
Mandatory IAM Permissions for BigQuery Direct Ingestion
Direct subscriptions do not run under user credentials; they execute under the Google-managed Cloud Pub/Sub Service Agent:
service-[PROJECT_NUMBER]@gcp-sa-pubsub.iam.gserviceaccount.com
To write data into BigQuery, the service agent must be granted the following IAM roles:
roles/bigquery.dataEditoron the destination BigQuery table or dataset (grants data plane write permissions via Storage Write API).roles/bigquery.metadataVieweron the dataset or project (allows Pub/Sub to inspect table schema and verify column mapping).
2. Cloud Storage Direct Subscriptions: Direct Batch File Ingestion
A Cloud Storage Direct Subscription automatically streams and batches messages directly into Google Cloud Storage buckets without spinning up compute workers. This pattern is ideal for landing raw data lake records, creating cold compliance archives, and feeding batch analytical engines like Dataproc or BigLake.
Output Formats
Cloud Storage direct subscriptions support two file formats:
- Text (Plaintext / Newline-Delimited JSON): Each message payload is written on a new line. Messages can be stored with or without trailing newlines.
- Avro (Binary Container Format): Messages are packaged into structured Avro files containing schema definitions and metadata. Avro files support block-level compression (such as Snappy or Deflate) and are splittable across distributed queries in BigLake, Apache Spark, and Presto/Trino.
Batching Flush Triggers
Writing individual 1 KB files to Cloud Storage for every incoming message would cause extreme file fragmentation, overwhelming Cloud Storage metadata operations and creating the dreaded "small file problem" for downstream analytical engines. To prevent this, Cloud Storage direct subscriptions buffer incoming messages in memory and flush them to a new file whenever any one of three configurable triggers is satisfied:
+----------------------------------------------------------------------------------+
| CLOUD STORAGE DIRECT SUBSCRIPTION FLUSH TRIGGERS |
+----------------------------------------------------------------------------------+
| |
| Incoming Streaming Messages ──> [ In-Memory Buffer ] |
| │ |
| ┌────────────────────────────────┼────────────────────────────────┐ |
| ▼ ▼ ▼ |
| Condition 1: Condition 2: Condition 3: |
| max_duration reached max_bytes reached max_messages |
| (e.g., 5 minutes) (e.g., 1 GiB) reached |
| │ │ │ |
| └────────────────────────────────┼────────────────────────────────┘ |
| ▼ |
| Flushes Buffer -> Commits New File to GCS |
| gs://data-lake-raw/events/2026/09/15/file_uuid.avro |
+----------------------------------------------------------------------------------+
max_duration: The maximum amount of time Pub/Sub buffers data before writing a file (configurable from 1 minute to 10 minutes, default: 5 minutes).max_bytes: The maximum accumulated size of buffered messages before flushing (configurable from 1 MiB to 10 GiB, default: 1 GiB).max_messages: The maximum message count allowed in a single file (configurable up to 100,000+ messages).
Customizing Object Naming and Partitioning
Direct subscriptions allow configuring dynamic directory structures and file extensions:
filename_prefix: Static directory prefix (e.g.,telemetry/raw/).filename_suffix: File extension (e.g.,.avro,.json).filename_datetime_format: Formats directory hierarchies based on current UTC time (e.g.,YYYY/MM/DD/hh/mm/). This enables automatic Hive-compatible directory partitioning (gs://bucket/telemetry/raw/2026/09/15/12/events_abc.avro) that external query engines can prune efficiently.
IAM Requirements for Cloud Storage Direct Subscriptions
The Pub/Sub Service Agent (service-[PROJECT_NUMBER]@gcp-sa-pubsub.iam.gserviceaccount.com) must possess:
roles/storage.objectCreator(orroles/storage.admin) on the destination bucket.
3. Architectural Decision Matrix: Direct Subscriptions vs. Dataflow vs. Cloud Functions
| Architectural Attribute | Pub/Sub Direct Subscriptions | Cloud Dataflow (Apache Beam) | Cloud Functions / Cloud Run |
|---|---|---|---|
| Compute Infrastructure | Zero compute; fully managed by Google | Managed VM worker pools (n2-standard-4) | Serverless container/event instances |
| Transformation Capabilities | None; raw 1-to-1 payload mapping or simple schema binding | Complex; windowing, joins, aggregations, data enrichment, masking | Lightweight; stateless parsing, filtering, schema validation |
| Ingestion Latency | Sub-second (BigQuery) / Minutes (GCS batch) | Sub-second (Streaming Engine) | Milliseconds to seconds |
| Cost Profile | Lowest; standard Pub/Sub fees + storage API ingestion | Moderate to high; hourly VM, memory, and Streaming Engine charges | Low to moderate; per-invocation and vCPU-second billing |
| Multi-Sink Fan-Out | Single destination per subscription | Writes concurrently to BigQuery, Spanner, Bigtable, GCS in one DAG | Writes to any downstream destination via client SDKs |
| Operational Overhead | Zero; no worker scaling or pipeline monitoring | Moderate; pipeline updates, draining, checkpoint tuning | Low; cold-starts, concurrency limits, timeout tuning |
| Best Exam Fit | Pure raw ingestion to BigQuery or GCS with zero transformations | Real-time analytics, session windowing, multi-source joins, PII masking | Simple webhook delivery, event-driven alerts, lightweight routing |
4. Dead-Letter Topics (DLQ) Mechanics and Architecture
A Dead-Letter Topic (DLQ) is a secondary Pub/Sub topic designated to capture undeliverable or repeatedly failing messages (often called poison pills).
Why Dead-Letter Topics Are Essential
In streaming pipelines, unparseable messages can cause catastrophic failures:
- In Push Subscriptions, a malformed message causes the webhook endpoint to return HTTP 500, triggering infinite retries that consume CPU and swamp the service.
- In Ordered Subscriptions, a failed message causes Head-of-Line blocking, freezing processing for that ordering key.
- In BigQuery Direct Subscriptions, a message that violates data types (e.g., passing a string into an integer column) will fail write operations indefinitely.
┌──────────────────────────────────────── Primary Subscription ────────────────────────────────────────┐
│ │
│ Incoming Messages ──> [ Attempt 1: Failed ] ──> [ Attempt 2: Failed ] ... ──> [ Attempt 5: Failed ] │
└──────────────────────────────────────────────────────────────────────────────────┬──────────────────┘
│
(Forward to DLQ & ACK Primary)
▼
┌─────────────────────────────────────── Dead-Letter Topic ───────────────────────────────────────────┐
│ │
│ [ Captured Poison-Pill Message ] ──> Preserves Payload + CloudPubSubDeadLetterSourceDeliveryAttempt │
│ │ │
│ ┌────────┴────────┐ │
│ ▼ ▼ │
│ [ Cloud Logging Alert ] [ Quarantine BQ Table ] │
└─────────────────────────────────────────────────────────────────────────────────────────────────────┘
How Dead-Letter Queuing Works
- Threshold Configuration (
maxDeliveryAttempts): The data engineer configures a Dead-Letter Topic and setsmaxDeliveryAttempts(an integer between 5 and 100, default: 5):gcloud pubsub subscriptions create main-orders-sub \ --topic=orders-topic \ --dead-letter-topic=orders-dlq-topic \ --max-delivery-attempts=5 - Delivery Attempt Counter: Pub/Sub increments an internal counter (
delivery_attempt) every time it attempts delivery to a subscriber and the delivery fails (due to ack deadline expiration, explicitnack, or push endpoint error). - Forwarding to DLQ: When the delivery attempt count reaches
maxDeliveryAttempts, Pub/Sub automatically:- Publishes the failed message to the configured Dead-Letter Topic.
- Injects a system attribute:
CloudPubSubDeadLetterSourceDeliveryAttemptindicating total failed delivery attempts. - Acknowledges the message on the primary subscription, removing it from the active delivery queue and immediately resolving any Head-of-Line block.
Mandatory IAM Permissions for Dead-Letter Topics
For Pub/Sub to divert failing messages, the Pub/Sub Service Agent (service-[PROJECT_NUMBER]@gcp-sa-pubsub.iam.gserviceaccount.com) must possess two specific IAM permissions:
roles/pubsub.publisheron the Dead-Letter Topic (to publish the evicted message).roles/pubsub.subscriberon the Primary Subscription (to acknowledge and evict the failing message).
Exam Trap — Silent DLQ Failure: If you configure a Dead-Letter Topic but fail to grant
roles/pubsub.publisherandroles/pubsub.subscriberto the Pub/Sub Service Agent, Pub/Sub cannot forward the message. Instead, the message will continue to be redelivered to consumers indefinitely, exceedingmaxDeliveryAttemptswithout ever routing to the DLQ.
5. DLQ Operations, Cloud Monitoring, and Poison-Pill Remediation
Isolating poison-pill messages in a Dead-Letter Topic is only the first step. Production architectures must monitor, alert on, and remediate failures:
-
Cloud Monitoring Alerting Policies:
- Monitor the metric
pubsub.googleapis.com/subscription/dead_letter_message_count. - Configure an alerting policy in Cloud Monitoring that triggers an urgent PagerDuty or Slack notification whenever the publish rate to the Dead-Letter Topic is greater than zero.
- Monitor
pubsub.googleapis.com/subscription/oldest_unacked_message_ageto detect consumer lag across both primary and DLQ subscriptions.
- Monitor the metric
-
Triage and Quarantine Sink:
- Attach a BigQuery direct subscription to the Dead-Letter Topic, writing failed records into a
quarantine_errorstable. - Engineers inspect the
quarantine_errorstable to analyze payload discrepancies, schema incompatibilities, or frontend client bugs.
- Attach a BigQuery direct subscription to the Dead-Letter Topic, writing failed records into a
-
Re-Ingestion Workflow:
- Once the application bug or schema definition is fixed, a recovery script or Dataflow job reads messages from the quarantine table or DLQ subscription, applies necessary transformations, and republishes the sanitized records back to the primary topic for reprocessing.
6. Realistic Exam Scenarios & Architecture Pitfalls
| Scenario / Challenge | Common Architecture Anti-Pattern | Correct Google Cloud Architecture |
|---|---|---|
| Serverless Clickstream Ingestion<br>A company needs to ingest 100,000 JSON clickstream events/sec into BigQuery for real-time dashboards. No complex transformations are required. | Provisioning a 20-node Cloud Dataflow streaming cluster running an Apache Beam pipeline. | Create a BigQuery Direct Subscription on the Pub/Sub topic targeting the BigQuery table. Enable write_metadata = true and grant roles/bigquery.dataEditor to the Pub/Sub service agent. Achieves zero-compute serverless ingestion at fractional cost. |
| Preventing File Fragmentation in Data Lakes<br>A Cloud Storage direct subscription writes raw IoT records to a bucket. Analysts report that Spark queries take hours because millions of 5 KB files exist in the bucket. | Writing a Cloud Function triggered on object creation to merge files together after upload. | Adjust the Cloud Storage direct subscription batching settings: configure max_duration = 10 minutes and max_bytes = 1 GiB using the Avro format. Pub/Sub buffers records in memory and writes large, query-optimized 1 GiB Avro files, permanently resolving the small file problem. |
| Infinite Redelivery Loops on Poison Pills<br>A subscription configured with a Dead-Letter Topic continues redelivering corrupted messages over 100 times without routing them to the DLQ. | Increasing maxDeliveryAttempts to 100 and redeploying consumer worker pods. | Verify IAM permissions. The Pub/Sub Service Agent lacks roles/pubsub.publisher on the Dead-Letter Topic or lacks roles/pubsub.subscriber on the primary subscription. Granting these roles enables Pub/Sub to forward the failed message and acknowledge it on the primary subscription. |
Upstream Schema Evolution Breaking Ingestion<br>A mobile app releases a new update adding an app_version string to its JSON payload. The BigQuery direct subscription immediately begins failing to write records because app_version is missing from the BigQuery table. | Rewriting the ingestion pipeline in Dataflow with custom JSON parsing logic. | Update the BigQuery direct subscription with --drop-unknown-fields. Pub/Sub drops the unexpected app_version field and writes all recognized fields cleanly into BigQuery, maintaining pipeline uptime while the data warehouse team adds the column. |
An enterprise data architecture team wants to stream clickstream events from Cloud Pub/Sub directly into BigQuery. The architecture must run completely serverless with zero compute infrastructure to operate (no Dataflow worker pools or Cloud Functions), must deliver sub-second ingestion latency, and must preserve the original Pub/Sub 'publish_time' and 'message_id' for downstream audit compliance. Furthermore, if a frontend mobile update introduces new tracking fields not yet defined in the BigQuery table schema, ingestion must continue without failing. How should this pipeline be configured?
A data engineer is designing a Cloud Storage direct subscription to land millions of streaming IoT sensor events into a Cloud Storage bucket for analytical queries by Apache Spark on Dataproc and BigLake external tables. The engineer notes that writing thousands of tiny files (such as 10 KB files every few seconds) will cause severe metadata bottlenecks on the storage system and degrade query performance due to excessive file listing overhead (the 'small file problem'). How should the direct subscription batching settings be configured to produce query-optimized files while ensuring data is committed to storage at least every 10 minutes?
A data platform team implements a Dead-Letter Topic (DLQ) on a mission-critical Cloud Pub/Sub pull subscription to isolate malformed transaction payloads. The subscription is configured with 'dead_letter_topic = "projects/finance-prod/topics/tx-dlq"' and 'max_delivery_attempts = 5'. During production testing with corrupted records, engineers observe that failing messages are never forwarded to 'tx-dlq'. Instead, the consumer application crashes, restarts, and receives the same poison-pill message repeatedly every 10 seconds, exceeding 40 failed delivery attempts. Cloud Monitoring indicates zero messages published to the dead-letter topic. What is the root cause of this failure?
A data architect is evaluating whether to use a Cloud Pub/Sub BigQuery Direct Subscription or an Apache Beam pipeline on Cloud Dataflow for a new streaming pipeline. The pipeline ingests financial trade messages from Pub/Sub. Each trade message must be enriched in real time by joining it with a customer risk profile stored in Cloud Spanner, and trades flagged with high risk scores must be masked before being written simultaneously to BigQuery and an operational Cloud Bigtable cluster. Which processing approach should the architect select and why?