KubeMQ
LearnEvents StoreHow-To Guides

Resume After Disconnect

Understand how durable subscriptions track position and resume automatically.

Events Store subscriptions are durable by default. When a subscriber disconnects and reconnects with the same durable name, the store resumes delivery from the last tracked position — no events are missed.

How Position Tracking Works

The Events Store remembers the last delivered position per durable name; on reconnect it ignores the start position and resumes from where the subscriber left off.

Durable Name

Every Events Store subscription creates a durable name:

DurableName = "{channel}-{group}"
  • On first connection, the StartPosition determines where to begin reading
  • On subsequent connections with the same durable name, the StartPosition is ignored — delivery resumes from the last tracked position
  • To force a fresh replay, use a different group name

Step-by-Step

Subscribe with a Durable Group

Use a named group to enable durable position tracking.

durable_subscriber.go
package main

import (
    "context"
    "fmt"
    "log"

    "github.com/kubemq-io/kubemq-go/v2"
)

func main() {
    ctx := context.Background()
    client, err := kubemq.NewClient(ctx,
        kubemq.WithAddress("localhost", 50000),
    )
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    sub, err := client.SubscribeToEventsStore(ctx,
        "orders.events",
        "order-processor",
        kubemq.StartFromFirst(),
        kubemq.WithOnEvent(func(event *kubemq.Event) {
            fmt.Printf("Processing seq=%d: %s\n",
                event.Sequence, string(event.Body))
        }),
        kubemq.WithOnError(func(err error) {
            log.Println("Error:", err)
        }),
    )
    if err != nil {
        log.Fatal(err)
    }
    defer sub.Unsubscribe()

    <-ctx.Done()
}
durable_subscriber.py
import time
from kubemq import (
    PubSubClient, EventsStoreSubscription,
    EventStoreStartPosition, CancellationToken,
)

def on_event(event):
    print(f"Processing seq={event.sequence}: {event.body.decode('utf-8')}")

with PubSubClient(address="localhost:50000") as client:
    client.subscribe_to_events_store(
        subscription=EventsStoreSubscription(
            channel="orders.events",
            group="order-processor",
            start_position=EventStoreStartPosition.StartFromFirst,
            on_receive_event_callback=on_event,
            on_error_callback=lambda e: print(f"Error: {e}"),
        ),
        cancel=CancellationToken(),
    )
    time.sleep(300)
durable_subscriber.ts
import { KubeMQClient, EventStoreStartPosition } from 'kubemq-js';

const client = await KubeMQClient.create({ address: 'localhost:50000' });

client.subscribeToEventsStore({
  channel: 'orders.events',
  group: 'order-processor',
  startPosition: EventStoreStartPosition.StartFromFirst,
  onEvent: (msg) =>
    console.log(`Processing seq=${msg.sequence}: ${new TextDecoder().decode(msg.body)}`),
  onError: (err) => console.error('Error:', err.message),
});
DurableSubscriber.java
PubSubClient client = PubSubClient.builder()
    .address("localhost:50000")
    .clientId("order-processor")
    .build();

client.subscribeToEventsStore(EventsStoreSubscription.builder()
    .channel("orders.events")
    .group("order-processor")
    .startPosition(EventStoreStartPosition.StartFromFirst)
    .onReceiveEventCallback(event ->
        System.out.printf("Processing seq=%d: %s%n",
            event.getSequence(), new String(event.getBody())))
    .onErrorCallback(err -> System.err.println("Error: " + err.getMessage()))
    .build());

Thread.sleep(300_000);
client.close();
DurableSubscriber.cs
await using var client = new KubeMQClient(new KubeMQClientOptions());
await client.ConnectAsync();

await foreach (var msg in client.SubscribeToEventsStoreAsync(
    new EventsStoreSubscription
    {
        Channel = "orders.events",
        Group = "order-processor",
        StartPosition = EventStoreStartPosition.StartFromFirst,
    }))
{
    Console.WriteLine($"Processing seq={msg.Sequence}: "
        + $"{Encoding.UTF8.GetString(msg.Body.Span)}");
}
DurableSubscriber.kt
val client = KubeMQClient.pubSub {
    address = "localhost:50000"
    clientId = "order-processor"
}

client.use {
    client.subscribeToEventsStore {
        channel = "orders.events"
        group = "order-processor"
        startPosition = StartPosition.StartFromFirst
    }.collect { msg ->
        println("Processing seq=${msg.sequence}: ${String(msg.body)}")
    }
}
durable_subscriber.cc
kubemq::ClientOptions options;
options.set_address("localhost", 50000);
options.set_client_id("order-processor");
auto client = kubemq::Client::Create(options).value();

client->SubscribeToEventsStore("orders.events", "order-processor",
    kubemq::StartPosition::StartFromFirst,
    [](const kubemq::EventStoreReceived& msg) {
        std::cout << "Processing seq=" << msg.sequence()
                  << ": " << msg.body() << std::endl;
    },
    [](const std::string& err) {
        std::cerr << "Error: " << err << std::endl;
    });
durable_subscriber.rs
use kubemq::prelude::*;
use kubemq::EventsStoreSubscription;
use std::time::Duration;

#[tokio::main]
async fn main() -> kubemq::Result<()> {
    let client = KubemqClient::builder()
        .host("localhost")
        .port(50000)
        .build()
        .await?;

    // Named group enables durable position tracking.
    let sub = client
        .subscribe_to_events_store(
            "orders.events",
            "order-processor",
            EventsStoreSubscription::StartFromFirst,
            |event| {
                Box::pin(async move {
                    println!(
                        "Processing seq={}: {}",
                        event.sequence,
                        String::from_utf8_lossy(&event.body)
                    );
                })
            },
            None,
        )
        .await?;

    tokio::time::sleep(Duration::from_secs(300)).await;
    sub.unsubscribe().await;
    client.close().await?;
    Ok(())
}
durable_subscriber.rb
require 'kubemq'

client = KubeMQ::PubSubClient.new(address: 'localhost:50000', client_id: 'order-processor')
cancel = KubeMQ::CancellationToken.new

# Named group enables durable position tracking.
subscription = KubeMQ::PubSub::EventsStoreSubscription.new(
  channel: 'orders.events',
  group: 'order-processor',
  start_position: KubeMQ::PubSub::EventStoreStartPosition::START_FROM_FIRST
)

client.subscribe_to_events_store(subscription, cancellation_token: cancel, on_error: lambda { |e|
  puts "Error: #{e.message}"
}) do |event|
  puts "Processing seq=#{event.sequence}: #{event.body}"
end

sleep 300
cancel.cancel
client.close
durable_subscriber.exs
{:ok, client} =
  KubeMQ.Client.start_link(address: "localhost:50000", client_id: "order-processor")

# Named group enables durable position tracking.
{:ok, _sub} =
  KubeMQ.Client.subscribe_to_events_store(client, "orders.events",
    start_at: :start_from_first,
    group: "order-processor",
    on_event: fn event ->
      IO.puts("Processing seq=#{event.sequence}: #{event.body}")
    end,
    on_error: fn err -> IO.puts("Error: #{inspect(err)}") end
  )

Process.sleep(300_000)
KubeMQ.Client.close(client)

Disconnect and Reconnect

  1. Run the subscriber and let it process events up to seq=10
  2. Stop the subscriber (Ctrl+C)
  3. Publish more events (seq 11-15)
  4. Restart the subscriber with the same group name

Result: The subscriber picks up at seq=11, not seq=1. The StartFromFirst parameter is ignored on reconnection because the durable position already exists.

Force a Fresh Replay

To replay from the beginning again, use a different group name:

# Original group — resumes from last position
GROUP=order-processor

# New group — replays from StartFromFirst
GROUP=order-processor-v2

The old group's position data remains until the channel is purged or the MaxPurgeInactive timeout expires.

Position Tracking Details

BehaviorDetail
Tracking granularityPer durable name ({channel}-{group})
Position persistenceStored alongside the event data on disk
First connectionUses the StartPosition you specify
ReconnectionIgnores StartPosition, resumes from last position
No group specifiedEmpty string group still creates a durable name
Multiple groupsEach group tracks position independently

Even with an empty group parameter, Events Store creates a durable subscription. The durable name becomes {channel}-. To create a truly ephemeral subscription, use plain Events instead.

Was this page helpful?

On this page