Skip to main content
With offline support enabled, a message you send while the device has no connection is durably queued on the device instead of failing. It keeps its place across a page reload or app restart and is sent automatically once the connection is back and stable (with autoSendPendingOnReconnect on, the default). This page shows the offline-aware send listener, how to read a message’s status, how to inspect and recover the send queue, and the queue limits.
Offline support is off by default and requires SDK 4.3.0 or later. Queuing only happens once you enable it and pass a MessageSentListener. A bare await CometChat.sendMessage(message) with no listener is never queued: offline, it rejects at once, as before.

Send a Message

Pass a MessageSentListener to sendMessage. Its lifecycle is normally onMessagePending first (the message is durably queued), then its outcome: onMessageSent (the server accepted it) or onError. Offline, onError is a queued-not-failed notice and onMessageSent still fires later on reconnect, so both can fire for one send (see below). If the message cannot be queued at all, for example because its media file could not be copied (code PERMANENT), only onError fires.
The same MessageSentListener is accepted by all four send methods:
The returned Promise is not replaced by the listener. It means “was it sent now?”: offline it rejects, and the eventual send is observed through onMessageSent or getPendingMessages, never the Promise. Always attach a .catch() when you rely on the listener alone.

Queued, Not Failed

When the device is offline, the send still fires onMessagePending, then fires onError once with the code FAILED_TO_FETCH. This is a notification, not a failure: the message stays durably PENDING, and onMessageSent fires later, on reconnect, when it actually goes. Do not re-send on that error. This is the default behaviour, with autoSendPendingOnReconnect on. With it off, an offline send becomes FAILED (TRANSIENT) right away, and you recover it with retryMessage. So for a single send, onError and onMessageSent can both fire (offline, then reconnect), and onError does not always mean the message failed. Read the message’s status to tell the two apart — keep the localMessageId from onMessagePending, then look the message up in the outbox:
For a global view, onOutboxMessageFailed on the OfflineMessageListener (below) fires only for real failures and carries the reason — a simpler place to drive a “failed, tap to retry” state.
A send made while an earlier message in the same conversation is still unsent waits behind it and gets no notice: no onError, and its Promise does not reject. It resolves, with onMessageSent, when it is sent.

Message status and failure reason

Every message exposes its offline state. getLocalMessageId() returns the SDK-generated handle that is stamped at compose (while offline is running) and stays stable across the whole lifecycle — use it to retry or discard the message. MessageFailureReason values:

Inspect the Queue

getPendingMessages returns the entire un-acked outbox — every PENDING, SENDING, and FAILED message, ordered by local compose time. Sent messages are not included. Pass a conversation id to limit it to one conversation. Read each message’s status to group them.
A queued media message sent from local files comes back with its files (message.files). With offline off, or nobody signed in, it resolves with an empty list. If the local store is briefly unavailable (busy, locked, full, or still reopening), it rejects with ERROR_OFFLINE_MESSAGE_PERSIST_FAILED instead of returning an empty list — try again.

Observe the queue app-wide

The per-call MessageSentListener lives only as long as the call. To watch every queue transition — a live send, a reconnect auto-flush, a manual retry, or a resumed send after a restart — register a global OfflineMessageListener. Its failed callback carries the MessageFailureReason.

Retry and Discard

There is no automatic retry of a FAILED message — recovery is explicit and user-invoked. A FAILED message stays in the outbox until you retry or discard it. Each method accepts a message or its localMessageId.
  • retryMessage(message, listener?) re-attempts one message. A FAILED message is reset to PENDING and sent; a PENDING message is attempted now rather than waiting for a reconnect. The optional MessageSentListener and the returned Promise receive that attempt’s outcome. With no answer, it fires the not-connected notice once (FAILED_TO_FETCH) and the message stays PENDING.
  • retryAllFailedMessages(listener?) retries transient failures only; permanent ones are skipped.
  • discardLocalMessage(message, listener?) deletes an unsent (PENDING or FAILED) message from the device, including its copied media. The optional listener is { onSuccess, onError }; onSuccess receives the localMessageId.
A retry is never refused for the queue cap and never evicts other messages, so a retry may take the outbox over maxPendingMessages. retryMessage and discardLocalMessage reject, changing nothing, when:

Auto-send on Reconnect

By default, queued PENDING messages are sent automatically when the connection is re-established. This is controlled by autoSendPendingOnReconnect (default true); see Enable Offline Support and the reference. With it off, a send that gets no answer fails at once with reason TRANSIENT (its listener gets one onError with FAILED_TO_FETCH and the message is FAILED), which you recover with retryMessage. A send that gets no answer for 15 seconds (media: 40 seconds plus upload time) counts as unanswered. After five reconnect attempts with no answer it fails with TRANSIENT. Reconnect also runs a catch-up sync for received messages — see Stay in Sync.

Queue Limits

The outbox holds at most maxPendingMessages messages still trying to send (PENDING + SENDING); FAILED messages do not count. When the queue is full, pendingQueueOverflowPolicy decides what happens: A media send with a file larger than maxMediaCacheSize is refused with reason BYTE_OVERFLOW; nothing is evicted to make room. A refused send (QUEUE_FULL / BYTE_OVERFLOW) is never queued, so it fires no onMessagePending; it reports onError, and the global OfflineMessageListener receives onOutboxMessageFailed(message, reason) carrying the exact reason. For QUEUE_FULL the error’s code is QUEUE_FULL, with details.maxPendingMessages.
A refused or evicted message (QUEUE_FULL, QUEUE_OVERFLOW, BYTE_OVERFLOW) has no durable row — it does not appear in getPendingMessages, and retryMessage or discardLocalMessage on its id rejects with ERROR_OFFLINE_MESSAGE_NOT_FOUND. Capture these from the failure callback and handle them at that moment. Prefer the default REJECT_NEWEST, which never silently drops an already-queued message from another conversation.

Edits, Deletes, and Reactions Need a Connection

Offline queuing applies to sending new messages only. Changes to existing messages — edit, delete, add or remove a reaction — and other own actions are not queued offline. They are sent to the server whenever you trigger them, and they fail with the not-connected error if the request gets no answer.

Durable Media Uploads

A multi-attachment upload survives a restart: the SDK re-attaches to an in-progress batch instead of restarting it. Fetch the existing request for a conversation with getExistingUploadFileRequest (it resolves null when there is none, or offline is off), read each file’s progress with getItems(), and retry an individual file with retryAttachment(fileId). Uploads never auto-resume: restored files come back retryable and wait for the user. A FAILED file with localCopyMissing: true cannot be retried — ask the user to re-add it.
See Media Caching for downloading and caching received attachments.
For the full list of offline methods, listeners, enums, model fields, and error codes, see the Configuration & API Reference.