Updated Nov 5, 20266 min read

How to Use Google Cloud Pub/Sub with Go in 2026

Publish and receive Pub/Sub messages from Go with the v2 client library, handle acks, flow control, retries and dead-letter topics, and get the same topics and subscriptions from Go declarations that run locally without the emulator.

Google Cloud Pub/Sub is the managed message bus on GCP: a service publishes to a topic, every subscription on that topic receives a copy, and Pub/Sub redelivers until a subscriber acknowledges. The Go client is efficient, batching publishes and streaming pulls by default, and its behavior around ack deadlines, flow control and redelivery decides whether a consumer is reliable or quietly processes messages twice.

This guide covers the v2 Go client for publishing and receiving, the settings that matter in production, dead-letter topics and local testing, and then shows how Encore (a tool that works alongside your framework) provisions the same setup in your GCP project from Go declarations.

At a glance

ConceptWhat to know
Packagecloud.google.com/go/pubsub/v2
Publishclient.Publisher(id).Publish(ctx, msg) returns a result to wait on
Receiveclient.Subscriber(id).Receive(ctx, fn) streams messages concurrently
DeliveryAt least once by default; exactly-once is opt-in per subscription
Ack deadlineDefault 10 seconds on the subscription, extended automatically by the client
Flow controlReceiveSettings.MaxOutstandingMessages, default 1,000
Failed messagesDead-letter topic after a maximum number of delivery attempts
Local testingPub/Sub emulator with PUBSUB_EMULATOR_HOST

Publish messages

The Pub/Sub v2 Go client authenticates with Application Default Credentials, so the same code uses your gcloud login locally and the service account of Cloud Run or GKE in production:

package main import ( "context" "encoding/json" "log" "cloud.google.com/go/pubsub/v2" ) type OrderPlaced struct { OrderID string `json:"order_id"` } func main() { ctx := context.Background() client, err := pubsub.NewClient(ctx, "my-project") if err != nil { log.Fatal(err) } defer client.Close() pub := client.Publisher("order-placed") defer pub.Stop() // flushes batched messages data, _ := json.Marshal(OrderPlaced{OrderID: "ord_123"}) id, err := pub.Publish(ctx, &pubsub.Message{Data: data}).Get(ctx) if err != nil { log.Fatal(err) } log.Println("published", id) }

Publish returns immediately and batches messages in the background, and Get waits for the server to confirm. In a request handler, waiting on Get before responding tells you the event was accepted; in a bulk job, collect the results and check them at the end. Create the topic beforehand with gcloud pubsub topics create order-placed or your infrastructure tooling.

Receive and acknowledge

sub := client.Subscriber("order-placed-email") sub.ReceiveSettings.MaxOutstandingMessages = 50 err = sub.Receive(ctx, func(ctx context.Context, m *pubsub.Message) { var ev OrderPlaced if err := json.Unmarshal(m.Data, &ev); err != nil { m.Ack() // malformed: retrying will not help return } if err := sendConfirmation(ctx, ev.OrderID); err != nil { m.Nack() // redeliver later return } m.Ack() })

Receive blocks until the context is cancelled and calls the callback from many goroutines at once, so the callback must be safe for concurrent use. MaxOutstandingMessages caps how many unacknowledged messages the subscriber holds, which is the setting that protects a database or a rate-limited API behind the handler. The client extends ack deadlines automatically while the callback runs, up to a maximum extension, and a message that is neither acked nor nacked in that time is redelivered.

Delivery is at least once, so a message can arrive twice even after a successful ack. Make the handler idempotent, for example with an insert that uses ON CONFLICT DO NOTHING on the order ID. Subscriptions can enable exactly-once delivery, which narrows duplicates within a region at some cost in throughput and latency.

Retries and dead-letter topics

Nacked or expired messages are redelivered right away unless the subscription has a retry policy with exponential backoff, which you set on the subscription with a minimum and maximum backoff. A dead-letter topic with a maximum of between 5 and 100 delivery attempts moves messages that keep failing out of the way. The Pub/Sub service account needs publisher permission on the dead-letter topic and subscriber permission on the source subscription for forwarding to work, which is a common reason dead-lettering silently does nothing.

Test locally with the emulator

The Pub/Sub emulator runs with gcloud beta emulators pubsub start, and the Go client connects to it when PUBSUB_EMULATOR_HOST is set. Topics and subscriptions have to be created in the emulator on each start, and it does not support every feature of the service, so failure handling is best checked in a real project.

What the client leaves to you

Each topic, subscription, retry policy, dead-letter topic and IAM binding has to exist in every environment, usually through Terraform, and stay in sync with the topic and subscription IDs hard-coded in Go. The receive loop needs a long-running process, and local development needs the emulator plus a setup script.

Set up the Encore app

Get started
brew install encoredev/tap/encore &&
encore app create
Copy

Run in your terminal to get started locally.

~
$ encore app create shop --example=hello-world$ cd shop && mkdir orders

Pub/Sub topics and subscriptions declared in Go with Encore

Encore

Encore is a tool that works alongside your framework for TypeScript and Go, with a platform that provisions and deploys into your own AWS or GCP account. A Pub/Sub topic and its subscriptions are package-level declarations in Go, and on GCP Encore provisions them as Pub/Sub topics and subscriptions with dead-letter topics in your project, with the handler called for each message and no receive loop to run.

The Infrastructure page in the Encore dashboard with a database's configuration panel open, showing fields for vCPUs, memory, storage, availability type, max connections and backups.
Each provisioned resource can be reviewed and its configuration edited per environment in the Encore dashboard. The screenshot shows a GCP Cloud SQL database.
// orders/orders.go package orders import ( "context" "time" "encore.dev/pubsub" "encore.dev/rlog" ) type OrderPlaced struct { OrderID string `json:"order_id"` } var OrderPlacedTopic = pubsub.NewTopic[*OrderPlaced]("order-placed", pubsub.TopicConfig{ DeliveryGuarantee: pubsub.AtLeastOnce, }) type PlaceParams struct { OrderID string `json:"order_id"` } //encore:api public method=POST path=/orders func Place(ctx context.Context, p *PlaceParams) error { _, err := OrderPlacedTopic.Publish(ctx, &OrderPlaced{OrderID: p.OrderID}) return err } var _ = pubsub.NewSubscription(OrderPlacedTopic, "send-confirmation", pubsub.SubscriptionConfig[*OrderPlaced]{ Handler: sendConfirmation, MaxConcurrency: 50, AckDeadline: time.Minute, RetryPolicy: &pubsub.RetryPolicy{MaxRetries: 10}, }, ) // Returning an error nacks the message; after 10 retries it goes to the dead-letter topic. func sendConfirmation(ctx context.Context, ev *OrderPlaced) error { rlog.Info("sending confirmation", "order_id", ev.OrderID) return nil }

encore run delivers messages with an in-memory implementation, so publishing and handling work locally without the emulator, a GCP project or setup scripts, and the local development dashboard shows the publish and the handler in its traces. MaxConcurrency, AckDeadline and RetryPolicy map to the settings covered above, and the handler is a plain function you can call from a test run with encore test, as described in the testing docs.

On deploy to GCP, Encore provisions the topic, the subscription and its dead-letter topic, runs the service on Cloud Run or GKE with its own service account, as listed in the GCP infrastructure docs. The same code deploys to SNS and SQS on AWS. A coding agent adding a consumer writes one NewSubscription in the service that needs it.

Best fit: Go teams on GCP who want Pub/Sub with retries and dead-lettering in their own project, and local development without the emulator.

Frequently asked questions

How do I publish a message to Google Cloud Pub/Sub in Go?

Create a client with pubsub.NewClient(ctx, projectID) from cloud.google.com/go/pubsub/v2, get a publisher with client.Publisher(topicID), call Publish(ctx, &pubsub.Message{Data: data}) and wait on the returned result with Get(ctx), which returns the server-assigned message ID. Call Stop on the publisher before exiting to flush pending messages.

How do I receive Pub/Sub messages in Go?

Get a subscriber with client.Subscriber(subscriptionID) and call Receive(ctx, func(ctx context.Context, m *pubsub.Message) { ... }). Receive blocks and calls the function concurrently for incoming messages. Call m.Ack() after successful processing and m.Nack() to have the message redelivered.

What changed in the Pub/Sub v2 Go client?

Version 2, released in 2025 as cloud.google.com/go/pubsub/v2, replaces Topic and Subscription handles for data operations with Publisher and Subscriber types, and moves creating and managing topics and subscriptions to generated admin clients on the main client.

How do I use Pub/Sub in Go with Encore?

Declare a topic with pubsub.NewTopic and a subscription with pubsub.NewSubscription and a Handler from encore.dev/pubsub. encore run delivers messages locally without the emulator, and on GCP Encore provisions the topic, subscription and dead-letter topic in your project.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders