---
source_url: https://www.pubnub.com/docs/design-patterns/receive-messages-effectively
title: Receive messages effectively
updated_at: 2026-09-30T07:20:08.000Z
---

# Receive messages effectively

## Documentation index

To discover more PubNub resources:

1. Fetch [PubNub's llms.txt](https://www.pubnub.com/llms-full.txt) for a list of available pages in Markdown format.
2. Identify relevant URLs from that index.
3. Fetch the target pages.

Do not assume a path exists, always check the index first.

This guide shows you how to structure the subscribe side of a PubNub application. You'll learn how to:

* Subscribe from a direct client connection instead of through a proxy.
* Keep a checkpoint of the last message you processed.
* Replay messages published while your client was disconnected, and skip the ones you already processed.
* Copy received data into your own systems without forking every message from the client.

Every call on this page needs an SDK instance initialized with your subscribe key. If you don't have a keyset yet, start with [Set up your account](https://www.pubnub.com/docs/architecture/authentication/set-up-your-account.md). This guide assumes you already know how to register a subscription. If you don't, start with [Receive messages](https://www.pubnub.com/docs/pub-sub/subscribe/receive-messages.md).

Examples use the JavaScript, Swift, Java, Kotlin, and Python SDKs, which document every parameter this guide uses. For any other language, refer to [Available SDKs](https://www.pubnub.com/docs/getting-started/available-sdks.md).

## Subscribe from the client, not through a proxy

Open the subscription from the client that needs the messages, and let the SDK hold its own connection to PubNub's nearest point of presence. A proxy in the middle can conflict with the encryption and the long-lived TCP connection the SDK already maintains, and it inherits the proxy's own downtime as its own. For the reasoning behind a direct connection, refer to [Architectural choices](https://www.pubnub.com/docs/design-patterns/architectural-choices.md#connect-clients-directly-keep-your-server-optional).

## Recover messages published while you were disconnected

Live delivery to subscribers is at-most-once by default. On a stable connection, a subscriber receives each message at most once. After a reconnect, a replayed message can arrive again with the same timetoken. A subscriber can also miss messages if its buffer overflows or if it's disconnected when someone publishes a message.

To recover the gap, keep a checkpoint of the last message you processed and replay history from it after a reconnect. The recipe has four parts:

* **A durable checkpoint.** Store the timetoken of the last message your handler finished, and write it only after the handler succeeds. Load it from storage when your app starts, so a restart recovers the same way a reconnect does.
* **A hold on live delivery.** When the status listener reports a disconnect, buffer live messages instead of processing them.
* **Bounded, paged replay.** When the client connects, page backward through history from now to the checkpoint, up to a page limit. If the gap needs more pages than the limit, reload state from your server instead of replaying.
* **One merge path.** Process the replayed messages first, then the buffered live messages in timetoken order. Every message goes through the same check, which skips any message at or before the checkpoint, so a message that arrives on both paths runs once.

In each example, the checkpoint storage, the message handler, and the reload function are placeholders for your own code. The handler throws on failure, which leaves the checkpoint where it was and starts a replay from it.

### JavaScript

```javascript
const CHANNEL = 'channel_1';
const PAGE_SIZE = 100;
const MAX_PAGES = 10;
const RETRY_DELAY_MS = 5000;

// Timetoken of the last message your handler finished, from durable storage.
let checkpoint = loadCheckpoint();
let holdLive = checkpoint !== null;
let recoveryRunning = false;
const liveBuffer = [];

function processMessage(timetoken, message) {
  if (checkpoint !== null && BigInt(timetoken) <= BigInt(checkpoint)) return;
  handleMessage(message); // throws on failure, so the checkpoint doesn't move
  checkpoint = timetoken;
  saveCheckpoint(checkpoint);
}

subscription.onMessage = (event) => {
  const timetoken = String(event.timetoken);
  if (holdLive) {
    liveBuffer.push({ timetoken, message: event.message });
    return;
  }
  try {
    processMessage(timetoken, event.message);
  } catch (error) {
    console.error('Handler failed, replaying from the checkpoint:', error);
    recoverMissedMessages();
  }
};

pubnub.addListener({
  status: (event) => {
    switch (event.category) {
      case 'PNDisconnectedCategory':
      case 'PNDisconnectedUnexpectedlyCategory':
      case 'PNConnectionErrorCategory':
      case 'PNNetworkDownCategory':
        holdLive = true;
        break;
      case 'PNConnectedCategory':
      case 'PNReconnectedCategory':
        recoverMissedMessages();
        break;
    }
  },
});

async function recoverMissedMessages() {
  if (recoveryRunning) return;
  recoveryRunning = true;
  holdLive = true;
  try {
    if (checkpoint !== null) {
      for (const item of await fetchSinceCheckpoint()) {
        processMessage(String(item.timetoken), item.message);
      }
    }
    liveBuffer.sort((a, b) => (BigInt(a.timetoken) < BigInt(b.timetoken) ? -1 : 1));
    while (liveBuffer.length > 0) {
      processMessage(liveBuffer[0].timetoken, liveBuffer[0].message);
      liveBuffer.shift();
    }
    holdLive = false;
    recoveryRunning = false;
  } catch (error) {
    recoveryRunning = false;
    if (error.gapTooLarge) {
      reloadStateFromYourServer();
      return;
    }
    console.error('Recovery failed, retrying:', error);
    setTimeout(recoverMissedMessages, RETRY_DELAY_MS);
  }
}

// Pages backward from now to the checkpoint and returns messages oldest first.
async function fetchSinceCheckpoint() {
  const pages = [];
  let start;
  for (let page = 0; page < MAX_PAGES; page++) {
    const response = await pubnub.fetchMessages({
      channels: [CHANNEL],
      end: checkpoint,
      start,
      count: PAGE_SIZE,
    });
    const items = response.channels[CHANNEL] ?? [];
    pages.unshift(items);
    if (items.length < PAGE_SIZE) return pages.flat();
    start = String(items[0].timetoken);
  }
  throw Object.assign(new Error('Gap exceeds MAX_PAGES'), { gapTooLarge: true });
}
```

### Swift

```swift
let channel = "channel_1"
let pageSize = 100
let maxPages = 10
let retryDelay: TimeInterval = 5

enum RecoveryError: Error {
  case gapTooLarge
}

// Timetoken of the last message your handler finished, from durable storage.
var checkpoint: Timetoken? = loadCheckpoint()
var holdLive = checkpoint != nil
var recoveryRunning = false
var liveBuffer: [PubNubMessage] = []

func process(_ message: PubNubMessage) throws {
  if let last = checkpoint, message.published <= last { return }
  try handleMessage(message) // throws on failure, so the checkpoint doesn't move
  checkpoint = message.published
  saveCheckpoint(message.published)
}

subscription.onMessage = { message in
  if holdLive {
    liveBuffer.append(message)
    return
  }
  do {
    try process(message)
  } catch {
    print("Handler failed, replaying from the checkpoint: \(error)")
    recoverMissedMessages()
  }
}

pubnub.onConnectionStateChange = { status in
  switch status {
  case .disconnected, .disconnectedUnexpectedly, .connectionError:
    holdLive = true
  case .connected:
    recoverMissedMessages()
  default:
    break
  }
}

func recoverMissedMessages() {
  guard !recoveryRunning else { return }
  recoveryRunning = true
  holdLive = true
  guard let from = checkpoint else {
    finishRecovery(missed: [])
    return
  }
  fetchPages(end: from, start: nil, pages: []) { result in
    switch result {
    case let .success(missed):
      finishRecovery(missed: missed)
    case .failure(RecoveryError.gapTooLarge):
      recoveryRunning = false
      reloadStateFromYourServer()
    case let .failure(error):
      retryRecovery(after: error)
    }
  }
}

func finishRecovery(missed: [PubNubMessage]) {
  do {
    for message in missed {
      try process(message)
    }
    liveBuffer.sort { $0.published < $1.published }
    while let next = liveBuffer.first {
      try process(next)
      liveBuffer.removeFirst()
    }
    holdLive = false
    recoveryRunning = false
  } catch {
    retryRecovery(after: error)
  }
}

func retryRecovery(after error: Error) {
  print("Recovery failed, retrying: \(error)")
  recoveryRunning = false
  DispatchQueue.main.asyncAfter(deadline: .now() + retryDelay) {
    recoverMissedMessages()
  }
}

// Pages backward from now to the checkpoint and returns messages oldest first.
func fetchPages(
  end: Timetoken,
  start: Timetoken?,
  pages: [[PubNubMessage]],
  completion: @escaping (Result<[PubNubMessage], Error>) -> Void
) {
  guard pages.count < maxPages else {
    completion(.failure(RecoveryError.gapTooLarge))
    return
  }
  pubnub.fetchMessageHistory(
    for: [channel],
    page: PubNubBoundedPageBase(start: start, end: end, limit: pageSize)
  ) { result in
    switch result {
    case let .success(response):
      let items = response.messagesByChannel[channel] ?? []
      let collected = [items] + pages
      if items.count < pageSize {
        completion(.success(collected.flatMap { $0 }))
      } else {
        fetchPages(end: end, start: items[0].published, pages: collected, completion: completion)
      }
    case let .failure(error):
      completion(.failure(error))
    }
  }
}
```

### Java

```java
import com.google.gson.JsonElement;
import com.pubnub.api.PubNubException;
import com.pubnub.api.enums.PNStatusCategory;
import com.pubnub.api.java.PubNub;
import com.pubnub.api.java.v2.subscriptions.Subscription;
import com.pubnub.api.models.consumer.history.PNFetchMessageItem;
import com.pubnub.api.models.consumer.history.PNFetchMessagesResult;
import com.pubnub.api.models.consumer.pubsub.PNMessageResult;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public abstract class MessageRecovery {
    private static final String CHANNEL = "channel_1";
    private static final int PAGE_SIZE = 100;
    private static final int MAX_PAGES = 10;
    private static final long RETRY_DELAY_MS = 5000;

    private final PubNub pubnub;
    private final Object lock = new Object();
    private final List<PNMessageResult> liveBuffer = new ArrayList<>();
    private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
    private Long checkpoint;
    private boolean holdLive;
    private boolean recoveryRunning = false;

    // Timetoken of the last message your handler finished, from durable storage.
    protected abstract Long loadCheckpoint();
    protected abstract void saveCheckpoint(long timetoken);
    // Throws on failure, so the checkpoint doesn't move.
    protected abstract void handleMessage(JsonElement message);
    protected abstract void reloadStateFromYourServer();

    public MessageRecovery(PubNub pubnub, Subscription subscription) {
        this.pubnub = pubnub;
        checkpoint = loadCheckpoint();
        holdLive = checkpoint != null;
        subscription.setOnMessage(this::onMessage);
        pubnub.addListener((pn, status) -> onStatus(status.getCategory()));
    }

    private void processMessage(long timetoken, JsonElement message) {
        if (checkpoint != null && timetoken <= checkpoint) return;
        handleMessage(message);
        checkpoint = timetoken;
        saveCheckpoint(timetoken);
    }

    private void onMessage(PNMessageResult event) {
        synchronized (lock) {
            if (holdLive) {
                liveBuffer.add(event);
                return;
            }
            try {
                processMessage(event.getTimetoken(), event.getMessage());
            } catch (RuntimeException error) {
                System.err.println("Handler failed, replaying from the checkpoint: " + error);
                startRecovery();
            }
        }
    }

    private void onStatus(PNStatusCategory category) {
        switch (category) {
            case PNDisconnectedCategory:
            case PNUnexpectedDisconnectCategory:
            case PNConnectionError:
                synchronized (lock) {
                    holdLive = true;
                }
                break;
            case PNConnectedCategory:
                startRecovery();
                break;
            default:
                break;
        }
    }

    private void startRecovery() {
        synchronized (lock) {
            if (recoveryRunning) return;
            recoveryRunning = true;
            holdLive = true;
        }
        executor.execute(this::recoverMissedMessages);
    }

    private void recoverMissedMessages() {
        try {
            List<PNFetchMessageItem> missed = fetchSinceCheckpoint();
            synchronized (lock) {
                for (PNFetchMessageItem item : missed) {
                    processMessage(item.getTimetoken(), item.getMessage());
                }
                liveBuffer.sort(Comparator.comparing(PNMessageResult::getTimetoken));
                while (!liveBuffer.isEmpty()) {
                    PNMessageResult event = liveBuffer.get(0);
                    processMessage(event.getTimetoken(), event.getMessage());
                    liveBuffer.remove(0);
                }
                holdLive = false;
                recoveryRunning = false;
            }
        } catch (GapTooLargeException error) {
            synchronized (lock) {
                recoveryRunning = false;
            }
            reloadStateFromYourServer();
        } catch (Exception error) {
            System.err.println("Recovery failed, retrying: " + error);
            synchronized (lock) {
                recoveryRunning = false;
            }
            executor.schedule(this::startRecovery, RETRY_DELAY_MS, TimeUnit.MILLISECONDS);
        }
    }

    // Pages backward from now to the checkpoint and returns messages oldest first.
    private List<PNFetchMessageItem> fetchSinceCheckpoint() throws PubNubException, GapTooLargeException {
        Long end;
        synchronized (lock) {
            end = checkpoint;
        }
        if (end == null) return Collections.emptyList();
        LinkedList<List<PNFetchMessageItem>> pages = new LinkedList<>();
        Long start = null;
        for (int page = 0; page < MAX_PAGES; page++) {
            PNFetchMessagesResult response = pubnub.fetchMessages()
                    .channels(Collections.singletonList(CHANNEL))
                    .end(end)
                    .start(start)
                    .maximumPerChannel(PAGE_SIZE)
                    .sync();
            List<PNFetchMessageItem> items = response.getChannels().getOrDefault(CHANNEL, Collections.emptyList());
            pages.addFirst(items);
            if (items.size() < PAGE_SIZE) {
                List<PNFetchMessageItem> all = new ArrayList<>();
                pages.forEach(all::addAll);
                return all;
            }
            start = items.get(0).getTimetoken();
        }
        throw new GapTooLargeException();
    }

    private static class GapTooLargeException extends Exception {
    }
}
```

### Kotlin

```kotlin
import com.google.gson.JsonElement
import com.pubnub.api.PubNub
import com.pubnub.api.enums.PNStatusCategory
import com.pubnub.api.models.consumer.PNBoundedPage
import com.pubnub.api.models.consumer.PNStatus
import com.pubnub.api.models.consumer.history.PNFetchMessageItem
import com.pubnub.api.models.consumer.pubsub.PNMessageResult
import com.pubnub.api.v2.callbacks.StatusListener
import com.pubnub.api.v2.subscriptions.Subscription
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

abstract class MessageRecovery(private val pubnub: PubNub, subscription: Subscription) {
    private val lock = Any()
    private val liveBuffer = mutableListOf<PNMessageResult>()
    private val executor = Executors.newSingleThreadScheduledExecutor()
    private var checkpoint: Long? = null
    private var holdLive = false
    private var recoveryRunning = false

    // Timetoken of the last message your handler finished, from durable storage.
    protected abstract fun loadCheckpoint(): Long?
    protected abstract fun saveCheckpoint(timetoken: Long)
    // Throws on failure, so the checkpoint doesn't move.
    protected abstract fun handleMessage(message: JsonElement)
    protected abstract fun reloadStateFromYourServer()

    init {
        checkpoint = loadCheckpoint()
        holdLive = checkpoint != null
        subscription.onMessage = { event -> onMessage(event) }
        pubnub.addListener(object : StatusListener {
            override fun status(pubnub: PubNub, status: PNStatus) = onStatus(status.category)
        })
    }

    private fun processMessage(timetoken: Long, message: JsonElement) {
        checkpoint?.let { if (timetoken <= it) return }
        handleMessage(message)
        checkpoint = timetoken
        saveCheckpoint(timetoken)
    }

    private fun onMessage(event: PNMessageResult) = synchronized(lock) {
        if (holdLive) {
            liveBuffer.add(event)
            return
        }
        try {
            processMessage(event.timetoken!!, event.message)
        } catch (error: Exception) {
            println("Handler failed, replaying from the checkpoint: $error")
            startRecovery()
        }
    }

    private fun onStatus(category: PNStatusCategory) {
        when (category) {
            PNStatusCategory.PNDisconnectedCategory,
            PNStatusCategory.PNUnexpectedDisconnectCategory,
            PNStatusCategory.PNConnectionError -> synchronized(lock) { holdLive = true }
            PNStatusCategory.PNConnectedCategory -> startRecovery()
            else -> Unit
        }
    }

    private fun startRecovery() {
        synchronized(lock) {
            if (recoveryRunning) return
            recoveryRunning = true
            holdLive = true
        }
        executor.execute { recoverMissedMessages() }
    }

    private fun recoverMissedMessages() {
        try {
            val missed = fetchSinceCheckpoint()
            synchronized(lock) {
                missed.forEach { processMessage(it.timetoken!!, it.message) }
                liveBuffer.sortBy { it.timetoken }
                while (liveBuffer.isNotEmpty()) {
                    val event = liveBuffer.first()
                    processMessage(event.timetoken!!, event.message)
                    liveBuffer.removeAt(0)
                }
                holdLive = false
                recoveryRunning = false
            }
        } catch (error: GapTooLargeException) {
            synchronized(lock) { recoveryRunning = false }
            reloadStateFromYourServer()
        } catch (error: Exception) {
            println("Recovery failed, retrying: $error")
            synchronized(lock) { recoveryRunning = false }
            executor.schedule({ startRecovery() }, RETRY_DELAY_MS, TimeUnit.MILLISECONDS)
        }
    }

    // Pages backward from now to the checkpoint and returns messages oldest first.
    private fun fetchSinceCheckpoint(): List<PNFetchMessageItem> {
        val end = synchronized(lock) { checkpoint } ?: return emptyList()
        val pages = ArrayDeque<List<PNFetchMessageItem>>()
        var start: Long? = null
        repeat(MAX_PAGES) {
            val response = pubnub.fetchMessages(
                channels = listOf(CHANNEL),
                page = PNBoundedPage(start = start, end = end, limit = PAGE_SIZE),
            ).sync()
            val items = response.channels[CHANNEL].orEmpty()
            pages.addFirst(items)
            if (items.size < PAGE_SIZE) return pages.flatten()
            start = items.first().timetoken
        }
        throw GapTooLargeException()
    }

    private class GapTooLargeException : Exception()

    companion object {
        private const val CHANNEL = "channel_1"
        private const val PAGE_SIZE = 100
        private const val MAX_PAGES = 10
        private const val RETRY_DELAY_MS = 5000L
    }
}
```

### Python

```python
import threading

from pubnub.callbacks import SubscribeCallback
from pubnub.enums import PNStatusCategory

CHANNEL = 'channel_1'
PAGE_SIZE = 100
MAX_PAGES = 10
RETRY_DELAY_S = 5

# Timetoken of the last message your handler finished, from durable storage.
checkpoint = load_checkpoint()
hold_live = checkpoint is not None
recovery_running = False
live_buffer = []
lock = threading.RLock()

class GapTooLarge(Exception):
    pass

def process_message(timetoken, message):
    global checkpoint
    if checkpoint is not None and timetoken <= checkpoint:
        return
    handle_message(message)  # raises on failure, so the checkpoint doesn't move
    checkpoint = timetoken
    save_checkpoint(checkpoint)

class RecoveryListener(SubscribeCallback):
    def message(self, pubnub, event):
        with lock:
            if hold_live:
                live_buffer.append((int(event.timetoken), event.message))
                return
            try:
                process_message(int(event.timetoken), event.message)
            except Exception as error:
                print('Handler failed, replaying from the checkpoint:', error)
                start_recovery()

    def status(self, pubnub, status):
        global hold_live
        if status.category in (
            PNStatusCategory.PNDisconnectedCategory,
            PNStatusCategory.PNUnexpectedDisconnectCategory,
            PNStatusCategory.PNConnectionErrorCategory,
        ):
            with lock:
                hold_live = True
        elif status.category in (
            PNStatusCategory.PNConnectedCategory,
            PNStatusCategory.PNReconnectedCategory,
        ):
            start_recovery()

def start_recovery():
    global hold_live, recovery_running
    with lock:
        if recovery_running:
            return
        recovery_running = True
        hold_live = True
    threading.Thread(target=recover_missed_messages, daemon=True).start()

def recover_missed_messages():
    global hold_live, recovery_running
    try:
        missed = fetch_since_checkpoint() if checkpoint is not None else []
        with lock:
            for timetoken, message in missed:
                process_message(timetoken, message)
            live_buffer.sort(key=lambda item: item[0])
            while live_buffer:
                process_message(*live_buffer[0])
                live_buffer.pop(0)
            hold_live = False
            recovery_running = False
    except GapTooLarge:
        with lock:
            recovery_running = False
        reload_state_from_your_server()
    except Exception as error:
        print('Recovery failed, retrying:', error)
        with lock:
            recovery_running = False
        threading.Timer(RETRY_DELAY_S, start_recovery).start()

# Pages backward from now to the checkpoint and returns messages oldest first.
def fetch_since_checkpoint():
    pages = []
    start = None
    for _ in range(MAX_PAGES):
        request = pubnub.fetch_messages().channels([CHANNEL]).end(checkpoint).count(PAGE_SIZE)
        if start is not None:
            request = request.start(start)
        items = request.sync().result.channels.get(CHANNEL, [])
        pages.insert(0, [(int(item.timetoken), item.message) for item in items])
        if len(items) < PAGE_SIZE:
            return [item for page in pages for item in page]
        start = int(items[0].timetoken)
    raise GapTooLarge()

pubnub.add_listener(RecoveryListener())
```

The examples recover one channel. For several channels, keep one checkpoint and one live buffer per channel. The Swift example relies on the SDK's default of running callbacks on the main queue. If you set a different callback queue, serialize access to the recovery state, as the Java, Kotlin, and Python examples do with a lock.

The SDK retries a dropped connection on its own, and a short outage may not produce a disconnect status at all. When the client reconnects, the subscriber buffer delivers what it holds:

The subscriber message buffer queues messages for a reconnecting client. It holds 100 messages for up to 16 minutes by default, and discards the oldest first (FIFO) when a burst exceeds that size. Larger buffers, for example 300 or 500 messages, can be provisioned per keyset by PubNub Support.

The examples start recovery on a status event or a handler failure. If you don't want to rely on a status event for every gap, also call the recovery function on a schedule or when your app returns to the foreground. It's safe to call at any time, because the checkpoint check skips messages you already processed.

The checkpoint moves only after the handler returns. If your handler fails partway through, for example after it wrote to a database, it runs again on the same message, so make its side effects safe to repeat. For publish-side options, refer to [Exactly-once processing](https://www.pubnub.com/docs/pub-sub/publish/overview.md#exactly-once-processing).

For how to register the status listener in your SDK, refer to [Monitor and respond to connection status changes](https://www.pubnub.com/docs/architecture/connection-management/monitor-and-respond-to-connection-status-changes.md#branch-on-the-status-category). For the fetch call's parameters, page limits, and how far back it can reach, refer to [Retrieve message history](https://www.pubnub.com/docs/data-storage/message-history/retrieve-message-history.md#fetch-missed-messages).

## Copy received data to your own systems without forking every message

Don't have the client forward a second copy of every message to your server on top of handling it. That doubles the mobile data and battery cost of every message, on top of whatever the client already does with it.

Use an After Publish Function to forward a copy from PubNub's own network instead. Or read the data back later through [Message Persistence](https://www.pubnub.com/docs/data-storage/message-history/overview.md), rather than capturing a copy of each message as it arrives. Refer to [Copy published data to your own systems without publishing twice](https://www.pubnub.com/docs/design-patterns/send-messages-effectively.md#copy-published-data-to-your-own-systems-without-publishing-twice) for both options. The choice is the same regardless of which side of the connection triggers it.

## Related tasks

* [Receive messages](https://www.pubnub.com/docs/pub-sub/subscribe/receive-messages.md). Create a subscription, register handlers, and subscribe to several channels at once.
* [Monitor and respond to connection status changes](https://www.pubnub.com/docs/architecture/connection-management/monitor-and-respond-to-connection-status-changes.md). Attach the status listener this guide's recovery step depends on.
* [Retrieve message history](https://www.pubnub.com/docs/data-storage/message-history/retrieve-message-history.md). Page through a long history and count unread messages.
* [Send messages effectively](https://www.pubnub.com/docs/design-patterns/send-messages-effectively.md). The publish-side half of this pattern, including the After Publish Function that avoids client-side forking.
* [Connection management](https://www.pubnub.com/docs/architecture/connection-management/overview.md). What the SDK retries on its own and what each status means.
* [Architectural choices](https://www.pubnub.com/docs/design-patterns/architectural-choices.md). The reasoning behind a direct client connection and where a data copy belongs.

Last updated at: 2026-09-30T07:20:08.000Z
