Ordered Delivery
Per-key in-order delivery with Pub/Sub ordering keys over KubeMQ — enable ordering, publish with an ordering key, consume at-most-one-in-flight per key.
Ordered delivery guarantees that messages sharing an ordering key are delivered in publish order, with at most one in flight per key. The head of a key blocks until it is acknowledged (or redelivered), and redelivery is in order — so a per-customer or per-aggregate stream is processed strictly in sequence. Messages without an ordering key are delivered unordered, and independent keys make progress in parallel. Ordering is a per-message feature with no code path beyond the standard Pub/Sub ordering-key API; the connector enforces the per-key sequencing internally.
Overview
Ordering is enabled on both sides: set enable_message_ordering on the publisher (it serializes publishes per key) and on the subscription (it enforces one-in-flight-per-key delivery). Each publish then carries an ordering_key; the key rides across the wire as the reserved tag _pubsub_ordering_key, which the connector surfaces as the message's ordering key for Pub/Sub clients.
| Step | Action | Behavior |
|---|---|---|
| Topic | CreateTopic + publisher ordering enabled | Publisher serializes publishes per key |
| Subscription | CreateSubscription(enable_message_ordering=true) | One copy in flight per key |
| Publish | Publish(ordering_key="cust-7") | In-order within the key; carried as _pubsub_ordering_key |
| Pull + Ack | Pull → Acknowledge → next per-key message released | Head-of-key blocks until acked |
| Keyless publish | Publish (no key) | Unordered; not serialized |
Ordering semantics:
- Per-key total order — within one
ordering_key, delivery follows publish order; the next message is released only after the current one is acked or redelivered. - At most one in flight per key — a key is never delivered ahead of its own un-acked head.
- Independent keys run in parallel — a round-robin cursor spreads delivery fairly across contended keys; a slow key never blocks a different one.
- Redelivery stays in order — an ack-deadline expiry or nack redelivers the head before any later message in the same key.
How it works
The publisher serializes publishes per ordering key. The connector writes each message to the topic log gcp.{topic} and fans out a copy to the subscription queue gcp.sub.{subscription}, then releases at most one message per key at a time — the next per-key message is held until the current one is acknowledged.
Messages sharing an ordering_key are delivered in publish order with at most one in flight per key; the next per-key message is released only after the current one is acknowledged, while independent keys make progress in parallel.
Publish and consume in order
Enable ordering on both the publisher and the subscription, publish interleaved messages across two keys (plus one keyless), and pull one at a time — acknowledging each before the next pull — so the per-key order is directly observable. Each client sets only PUBSUB_EMULATOR_HOST (default localhost:8085) and a project id.
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"cloud.google.com/go/pubsub"
"cloud.google.com/go/pubsub/apiv1/pubsubpb"
)
func projectID() string {
if v := os.Getenv("PUBSUB_PROJECT_ID"); v != "" {
return v
}
return "my-project"
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
client, err := pubsub.NewClient(ctx, projectID())
if err != nil {
log.Fatalf("NewClient: %v", err)
}
defer client.Close()
// CreateTopic, then enable ordering on the publisher handle (it serializes
// publishes per key). The subscription must enable ordering too — both sides.
topic, err := client.CreateTopic(ctx, "ordered")
if err != nil {
log.Fatalf("CreateTopic: %v", err)
}
topic.EnableMessageOrdering = true
defer topic.Stop()
if _, err := client.CreateSubscription(ctx, "sub-ordered", pubsub.SubscriptionConfig{
Topic: topic,
AckDeadline: 10 * time.Second,
EnableMessageOrdering: true,
}); err != nil {
log.Fatalf("CreateSubscription: %v", err)
}
// Two keys interleaved + one keyless. Await each result so the per-key publish
// order is preserved across keys.
plan := []struct{ key, body string }{
{"cust-7", "A1"}, {"cust-9", "B1"}, {"cust-7", "A2"},
{"", "keyless"}, {"cust-9", "B2"}, {"cust-7", "A3"}, {"cust-9", "B3"},
}
for _, p := range plan {
m := &pubsub.Message{Data: []byte(p.body)}
if p.key != "" {
m.OrderingKey = p.key
}
if _, err := topic.Publish(ctx, m).Get(ctx); err != nil {
log.Fatalf("Publish %q: %v", p.body, err)
}
}
// Pull one at a time, ack before the next pull → connector releases the next
// per-key message in order.
subClient, err := pubsub.NewSubscriberClient(ctx)
if err != nil {
log.Fatalf("NewSubscriberClient: %v", err)
}
defer subClient.Close()
subPath := fmt.Sprintf("projects/%s/subscriptions/%s", projectID(), "sub-ordered")
for i := 0; i < len(plan); i++ {
resp, err := subClient.Pull(ctx, &pubsubpb.PullRequest{Subscription: subPath, MaxMessages: 1})
if err != nil {
log.Fatalf("Pull: %v", err)
}
if len(resp.GetReceivedMessages()) == 0 {
i--
continue
}
rm := resp.GetReceivedMessages()[0]
key := rm.GetMessage().GetOrderingKey() // surfaced from _pubsub_ordering_key.
fmt.Printf("received body=%q ordering_key=%q\n", string(rm.GetMessage().GetData()), key)
_ = subClient.Acknowledge(ctx, &pubsubpb.AcknowledgeRequest{
Subscription: subPath, AckIds: []string{rm.GetAckId()},
})
}
// Per-key order is preserved: cust-7 → A1,A2,A3 and cust-9 → B1,B2,B3.
}import os
from google.cloud import pubsub_v1
from google.cloud.pubsub_v1.types import PublisherOptions
def project_id() -> str:
return os.environ.get("PUBSUB_PROJECT_ID", "my-project")
def main() -> None:
proj = project_id()
# The publisher MUST enable message ordering (it serializes publishes per key).
publisher = pubsub_v1.PublisherClient(
publisher_options=PublisherOptions(enable_message_ordering=True)
)
subscriber = pubsub_v1.SubscriberClient()
topic_path = publisher.topic_path(proj, "ordered") # -> gcp.ordered
sub_path = subscriber.subscription_path(proj, "sub-ordered") # -> gcp.sub.sub-ordered
publisher.create_topic(request={"name": topic_path})
# The subscription must also enable ordering.
subscriber.create_subscription(
request={"name": sub_path, "topic": topic_path, "enable_message_ordering": True}
)
# Two keys interleaved + one keyless; await each publish to preserve order.
plan = [("cust-7", "A1"), ("cust-9", "B1"), ("cust-7", "A2"),
("", "keyless"), ("cust-9", "B2"), ("cust-7", "A3"), ("cust-9", "B3")]
for key, body in plan:
publisher.publish(topic_path, body.encode(), ordering_key=key).result(timeout=15)
# Pull one at a time, ack before the next pull → next per-key message released.
for _ in range(len(plan)):
resp = subscriber.pull(request={"subscription": sub_path, "max_messages": 1}, timeout=20)
if not resp.received_messages:
continue
rm = resp.received_messages[0]
print(f"received body={rm.message.data.decode()!r} ordering_key={rm.message.ordering_key!r}")
subscriber.acknowledge(request={"subscription": sub_path, "ack_ids": [rm.ack_id]})
subscriber.close()
# cust-7 → A1,A2,A3 and cust-9 → B1,B2,B3 in order.
if __name__ == "__main__":
main()import com.google.api.gax.core.NoCredentialsProvider;
import com.google.api.gax.grpc.GrpcTransportChannel;
import com.google.api.gax.rpc.FixedTransportChannelProvider;
import com.google.api.gax.rpc.TransportChannelProvider;
import com.google.cloud.pubsub.v1.SubscriptionAdminClient;
import com.google.cloud.pubsub.v1.SubscriptionAdminSettings;
import com.google.cloud.pubsub.v1.TopicAdminClient;
import com.google.cloud.pubsub.v1.TopicAdminSettings;
import com.google.cloud.pubsub.v1.stub.GrpcSubscriberStub;
import com.google.cloud.pubsub.v1.stub.SubscriberStubSettings;
import com.google.protobuf.ByteString;
import com.google.pubsub.v1.AcknowledgeRequest;
import com.google.pubsub.v1.PublishRequest;
import com.google.pubsub.v1.PubsubMessage;
import com.google.pubsub.v1.PullRequest;
import com.google.pubsub.v1.PullResponse;
import com.google.pubsub.v1.PushConfig;
import com.google.pubsub.v1.ReceivedMessage;
import com.google.pubsub.v1.Subscription;
import com.google.pubsub.v1.SubscriptionName;
import com.google.pubsub.v1.TopicName;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import java.util.List;
public final class Main {
public static void main(String[] args) throws Exception {
String emulatorHost = System.getenv().getOrDefault("PUBSUB_EMULATOR_HOST", "localhost:8085");
String projectId = System.getenv().getOrDefault("PUBSUB_PROJECT_ID", "my-project");
ManagedChannel channel = ManagedChannelBuilder.forTarget(emulatorHost).usePlaintext().build();
TransportChannelProvider channelProvider =
FixedTransportChannelProvider.create(GrpcTransportChannel.create(channel));
NoCredentialsProvider noCreds = NoCredentialsProvider.create();
TopicName topic = TopicName.of(projectId, "ordered"); // -> gcp.ordered
SubscriptionName sub = SubscriptionName.of(projectId, "sub-ordered"); // -> gcp.sub.sub-ordered
try (TopicAdminClient topicAdmin = TopicAdminClient.create(TopicAdminSettings.newBuilder()
.setTransportChannelProvider(channelProvider).setCredentialsProvider(noCreds).build());
SubscriptionAdminClient subAdmin = SubscriptionAdminClient.create(
SubscriptionAdminSettings.newBuilder()
.setTransportChannelProvider(channelProvider)
.setCredentialsProvider(noCreds).build());
GrpcSubscriberStub subStub = GrpcSubscriberStub.create(SubscriberStubSettings.newBuilder()
.setTransportChannelProvider(channelProvider)
.setCredentialsProvider(noCreds).build())) {
topicAdmin.createTopic(topic);
// The subscription enables ordering; the publisher orders per key below.
subAdmin.createSubscription(Subscription.newBuilder()
.setName(sub.toString()).setTopic(topic.toString())
.setAckDeadlineSeconds(10).setEnableMessageOrdering(true).build());
// Two keys interleaved + one keyless; publish in order with an ordering key.
String[][] plan = {
{"cust-7", "A1"}, {"cust-9", "B1"}, {"cust-7", "A2"},
{"", "keyless"}, {"cust-9", "B2"}, {"cust-7", "A3"}, {"cust-9", "B3"},
};
for (String[] p : plan) {
PubsubMessage.Builder m = PubsubMessage.newBuilder()
.setData(ByteString.copyFromUtf8(p[1]));
if (!p[0].isEmpty()) {
m.setOrderingKey(p[0]);
}
topicAdmin.publish(PublishRequest.newBuilder()
.setTopic(topic.toString()).addMessages(m.build()).build());
}
// Pull one at a time, ack before the next pull → next per-key message released.
for (int i = 0; i < plan.length; i++) {
PullResponse resp = subStub.pullCallable().call(PullRequest.newBuilder()
.setSubscription(sub.toString()).setMaxMessages(1).build());
if (resp.getReceivedMessagesCount() == 0) {
i--;
continue;
}
ReceivedMessage got = resp.getReceivedMessages(0);
System.out.printf("received body=%s ordering_key=%s%n",
got.getMessage().getData().toStringUtf8(), got.getMessage().getOrderingKey());
subStub.acknowledgeCallable().call(AcknowledgeRequest.newBuilder()
.setSubscription(sub.toString()).addAckIds(got.getAckId()).build());
}
// cust-7 → A1,A2,A3 and cust-9 → B1,B2,B3 in order.
List.of(); // no-op to keep imports tidy
} finally {
channel.shutdown();
}
}
}import { PubSub, v1 } from "@google-cloud/pubsub";
const projectId = process.env["PUBSUB_PROJECT_ID"] ?? "my-project";
const baseOptions = new PubSub({ projectId }).options;
const options = { ...baseOptions, port: baseOptions.port === undefined ? undefined : Number(baseOptions.port) };
const publisher = new v1.PublisherClient(options);
const subscriber = new v1.SubscriberClient(options);
async function main(): Promise<void> {
const topic = publisher.projectTopicsPath(projectId, "ordered"); // -> gcp.ordered
const sub = subscriber.subscriptionPath(projectId, "sub-ordered"); // -> gcp.sub.sub-ordered
await publisher.createTopic({ name: topic });
// The subscription enables ordering; each publish below carries an orderingKey.
await subscriber.createSubscription({ name: sub, topic, ackDeadlineSeconds: 10, enableMessageOrdering: true });
// Two keys interleaved + one keyless; publish each in turn to preserve order.
const plan: Array<[string, string]> = [
["cust-7", "A1"], ["cust-9", "B1"], ["cust-7", "A2"],
["", "keyless"], ["cust-9", "B2"], ["cust-7", "A3"], ["cust-9", "B3"],
];
for (const [orderingKey, body] of plan) {
await publisher.publish({ topic, messages: [{ data: Buffer.from(body), orderingKey }] });
}
// Pull one at a time, ack before the next pull → next per-key message released.
for (let i = 0; i < plan.length; i++) {
const [pull] = await subscriber.pull({ subscription: sub, maxMessages: 1 });
if (!pull.receivedMessages || pull.receivedMessages.length === 0) {
i--;
continue;
}
const rm = pull.receivedMessages[0];
const body = Buffer.from(rm.message!.data as Uint8Array).toString("utf8");
console.log(`received body=${body} ordering_key=${rm.message!.orderingKey}`);
await subscriber.acknowledge({ subscription: sub, ackIds: [rm.ackId!] });
}
// cust-7 → A1,A2,A3 and cust-9 → B1,B2,B3 in order.
}
main().catch((err) => {
console.error(err);
process.exit(1);
});using Google.Api.Gax;
using Google.Cloud.PubSub.V1;
using Google.Protobuf;
var projectId = Environment.GetEnvironmentVariable("PUBSUB_PROJECT_ID") ?? "my-project";
var topicName = TopicName.FromProjectTopic(projectId, "ordered"); // -> gcp.ordered
var subName = SubscriptionName.FromProjectSubscription(projectId, "sub-ordered"); // -> gcp.sub.sub-ordered
var publisher = await new PublisherServiceApiClientBuilder
{
EmulatorDetection = EmulatorDetection.EmulatorOnly,
}.BuildAsync();
var subscriber = await new SubscriberServiceApiClientBuilder
{
EmulatorDetection = EmulatorDetection.EmulatorOnly,
}.BuildAsync();
await publisher.CreateTopicAsync(topicName);
// The subscription enables ordering; each publish below carries an OrderingKey.
await subscriber.CreateSubscriptionAsync(new Subscription
{
SubscriptionName = subName,
TopicAsTopicName = topicName,
AckDeadlineSeconds = 10,
EnableMessageOrdering = true,
});
// Two keys interleaved + one keyless; publish each in turn to preserve order.
var plan = new[]
{
("cust-7", "A1"), ("cust-9", "B1"), ("cust-7", "A2"),
("", "keyless"), ("cust-9", "B2"), ("cust-7", "A3"), ("cust-9", "B3"),
};
foreach (var (orderingKey, body) in plan)
{
var msg = new PubsubMessage { Data = ByteString.CopyFromUtf8(body) };
if (orderingKey.Length > 0) msg.OrderingKey = orderingKey;
await publisher.PublishAsync(topicName, new[] { msg });
}
// Pull one at a time, ack before the next pull → next per-key message released.
for (var i = 0; i < plan.Length; i++)
{
var pull = await subscriber.PullAsync(subName, maxMessages: 1);
if (pull.ReceivedMessages.Count == 0) { i--; continue; }
var rm = pull.ReceivedMessages[0];
Console.WriteLine($"received body={rm.Message.Data.ToStringUtf8()} ordering_key={rm.Message.OrderingKey}");
await subscriber.AcknowledgeAsync(subName, new[] { rm.AckId });
}
// cust-7 → A1,A2,A3 and cust-9 → B1,B2,B3 in order.# frozen_string_literal: true
require "google/cloud/pubsub"
project_id = ENV["PUBSUB_PROJECT_ID"] || "my-project"
emulator_host = ENV["PUBSUB_EMULATOR_HOST"] || "localhost:8085"
pubsub = Google::Cloud::PubSub.new(project_id: project_id, emulator_host: emulator_host)
topic_admin = pubsub.topic_admin
sub_admin = pubsub.subscription_admin
topic_path = pubsub.topic_path("ordered") # -> gcp.ordered
sub_path = pubsub.subscription_path("sub-ordered") # -> gcp.sub.sub-ordered
topic = topic_admin.create_topic(name: topic_path)
# The subscription enables ordering; each publish below carries an ordering_key.
sub_admin.create_subscription(name: sub_path, topic: topic_path,
ack_deadline_seconds: 10, enable_message_ordering: true)
# Two keys interleaved + one keyless. message_ordering: true serializes per key.
publisher = pubsub.publisher(topic.name, async: { ordered: true })
plan = [["cust-7", "A1"], ["cust-9", "B1"], ["cust-7", "A2"],
["", "keyless"], ["cust-9", "B2"], ["cust-7", "A3"], ["cust-9", "B3"]]
plan.each do |key, body|
publisher.publish(body, ordering_key: key)
end
publisher.async_publisher.stop! # flush ordered publishes in order.
# Pull one at a time, ack before the next pull → next per-key message released.
subscriber = pubsub.subscriber(sub_path)
plan.length.times do
received = subscriber.pull(immediate: false, max: 1)
next if received.empty?
rcv = received.first
puts "received body=#{rcv.data} ordering_key=#{rcv.ordering_key.inspect}"
rcv.acknowledge!
end
# cust-7 → A1,A2,A3 and cust-9 → B1,B2,B3 in order.Enable ordering on both sides. The publisher serializes publishes per key only when ordering is enabled on the publisher handle (EnableMessageOrdering / enable_message_ordering / ordered: true), and the subscription enforces one-in-flight-per-key only when it is created with ordering enabled. Enabling it on a single side is not enough.
Why pull one at a time
The examples use unary Pull and acknowledge each message before the next pull so the head-of-key blocks until acked guarantee is directly observable. A high-level streaming subscriber buffers and acks asynchronously, which preserves per-key order but obscures the strict one-in-flight-per-key sequencing. The connector enforces the order regardless; pulling one at a time only makes it visible.
Related
Was this page helpful?
Message filtering
Subscription-level attribute filtering over KubeMQ — the CEL-subset grammar, the ≤ 256-char immutable rule, and how filters apply at publish fan-out.
Publish & Subscribe
Create a topic and a subscription, publish a message, then pull and acknowledge it over KubeMQ — the core Pub/Sub round-trip on gcp.{topic} channels.