Skip to content
Go back

GCP Pub/Sub Exactly-Once Delivery Latency Experiment

Updated:
Edit Page

PubSub?

I have been wondering why a nearly-idle service, using Pub/Sub for an async pipeline, has 5+ second end-to-end latency. Besides the usual ~1s of inter-service call time, Pub/Sub itself adds 3+ seconds.

Suspected our own app code, Pub/Sub’s Exactly-Once Delivery and cold start. To verify, I deployed a Go service locally and on GKE, and used Cloud Trace to investigate.

TLDR; Conclusion first.

Pub/Sub Exactly-Once Delivery(EOS) adds significant, non-trivial latency. Latency-sensitive applications should use EOS with caution. Didn’t test at 100 RPS. Maybe Pub/Sub delivers on its promise under high throughput.

Feel free to share in the comments if you know more about this. I might be wrong.

Latency Factors

Honestly, Pub/Sub’s docs don’t explain this latency at all.

GCP admits exactly-once delivery adds latency but no further explanation: https://docs.cloud.google.com/pubsub/docs/exactly-once-delivery

Exactly-once delivery subscriptions have significantly higher publish-to-subscribe latency compared to regular subscriptions.

Cold start. Probably the biggest one. Pub/Sub is fully managed. Nothing to tune server-side. We can only wish for more traffic, hoping it gets better.

NumGoroutines. Configure ReceiveSettings.NumGoroutines. Easy to overlook, but must be set right. Otherwise a service waits on the old message’s modack before pulling a new one. Docs say it doesn’t affect concurrent processing. True, but it affects pulling new messages!

Exactly-Once Delivery. Warm or cold, EOS is consistently hundreds of milliseconds higher per hop than standard. Not caused by network distance, as it was reproduced on GKE within same region.

That “hundreds of milliseconds” splits into two pieces.

The Traces

EOS chain, chain-eos-1-sub subscribe 1.303s

Warm 832c3ddd518e0624323246d2e322358b EOS warm trace, view B EOS warm trace, view A

Standard chain, chain-std-1-sub subscribe span: 264ms

Warm b47c7cb9aa31504b990e24ff5cb1dca0 Standard warm trace, view B Standard warm trace, view A

What to look at: in both traces, the simulated worker.stageN.process work is identical, 200ms. The difference is entirely inside the subscribe span that wraps it and the blank parts.

Modack and Ack

GCP docs barely explain this.

For EOS, it has to wait for the response of modack while standard subscriptions doesn’t need to.

Where each shows up in the trace, and which function writes it:

Cold, all three inflate together except your callback. Warm, they shrink and start overlapping in time.

Test Setup

Architecture

Architecture

Data

Sending method: grafana/k6 Job in the same namespace, constant-arrival-rate executor, 1 req/s per chain against api-eos/api-std’s internal Service.

/send doesn’t wait for the full chain to finish. It returns right after publishing. Latency below is measured end-to-end from Cloud Trace, not /send’s response time.

End-to-end latency after deducting the 600ms simulation processing time.

MetricSTD (n=31)EOS (n=31)
min120ms601ms
p25132ms1955ms
p50135ms2605ms
p75137ms2802ms
p90226ms3606ms
p95233ms4155ms
mean146ms2618ms
max236ms4707ms

Same gaps, summed across all 3 hops per trace so this is the total delivery/dispatch overhead contributed to one full chain run:

Gap sum (3 hops)STD (n=31)EOS (n=31)
delivery~0msmin 301ms / p50 368ms / mean 389ms / max 580ms
dispatch~0msmin 66ms / p50 1118ms / mean 1455ms / max 3093ms

Across a full 3-hop chain, EOS’s modack wait alone (dispatch gap) contributes roughly 1.1s at the median and up to 3s in the tail — on top of ~370ms of delivery gap. STD contributes essentially nothing on either front.

Data at 5 RPS

Same setup, same cluster, rate: 5 instead of rate: 1 in the k6 Job, and -num-goroutines=10 (up from the default 4) on both STD and EOS workers so the concurrency setting is fair across chains. This is a warmed-up connection, which is closer to what a real sustained-throughput service looks like:

End-to-end latency deducting the 600ms simulated handler time.

MetricSTD (n=151)EOS (n=151)
min73ms429ms
p25174ms543ms
p50181ms639ms
p75186ms650ms
p90188ms943ms
p95188ms1643ms
mean177ms758ms
max276ms2644ms

Delivery/dispatch gap sum (3 hops) at 5 RPS:

MetricSTD delivery (n=151)EOS delivery (n=151)STD dispatch (n=151)EOS dispatch (n=151)
min-12ms259ms0ms29ms
p25-4ms356ms0ms49ms
p50-3ms386ms0ms64ms
p75-2ms431ms1ms94ms
p90-1ms472ms1ms147ms
p950ms503ms1ms1062ms
mean-3ms427ms1ms157ms
max23ms3436ms4ms3094ms

I am not sure why the calculation from “Publish End” -> “Subscription Start” in the STD results in negative latencies. You can simply treat these as 0ms.

Next action: will EOS perform better at 100 RPS? 1,000 RPS?


Edit Page
Share this post on:

Next Post
Graph Algorithms