Status Events for C SDK
The PubNub C SDK reports connection state and error information through two mechanisms: a polling accessor that returns the current connection state, and a push-based status listener that fires whenever the state changes. Both are driven by the subscribe event engine and apply to every subscribe operation. The SDK has exactly one subscribe and status model, with no compile-time switch between architectures.
This page covers the connection-state model, the status-category enum and its listener contract, the complete pubnub_res_t result-code catalog, retrieving normalized server error detail, the control functions that affect connection state, and the cancellation contract that fires during context teardown.
Connection states
pubnub_subscribe_state() returns the context's current connection state as a pubnub_subscribe_connection_state_t value. It is a polling accessor: call it whenever you need to know the current state, independent of the status listener described later on this page.
static void show_state(pubnub_context_t* ctx)
{
pubnub_subscribe_connection_state_t state = pubnub_subscribe_state(ctx);
/* ... */
}
| Connection state | Value | Meaning |
|---|---|---|
PUBNUB_SUBSCRIBE_IDLE | 0 | No active subscription. The event engine is idle. |
PUBNUB_SUBSCRIBE_CONNECTING | 1 | Handshake in progress, either the first connect or a change to the subscribed channel set. |
PUBNUB_SUBSCRIBE_CONNECTED | 2 | Long-poll active. The context is receiving real-time events. |
PUBNUB_SUBSCRIBE_RECONNECTING | 3 | Reconnecting after a failure, either a failed handshake or a failed receive. |
PUBNUB_SUBSCRIBE_DISCONNECTED | 4 | User-initiated disconnect (stopped). |
pubnub_subscribe_state() also returns PUBNUB_SUBSCRIBE_IDLE when the subscribe feature has not been initialized on the context, or when ctx is NULL.
Status categories
The SDK pushes status events by invoking your on_status callback with a pubnub_subscribe_status_t value. This is a distinct enum from the connection state above. Connection state is a snapshot you poll; status categories are discrete events the SDK notifies you of.
| Status | Value | Meaning |
|---|---|---|
PUBNUB_SUBSCRIBE_STATUS_CONNECTED | 0 | Handshake succeeded, or a lost connection was re-established. The real-time stream is active. |
PUBNUB_SUBSCRIBE_STATUS_DISCONNECTED | 1 | User-initiated, graceful disconnect. |
PUBNUB_SUBSCRIBE_STATUS_DISCONNECTED_UNEXPECTEDLY | 2 | Connection lost without user intent (network drop, timeout). |
PUBNUB_SUBSCRIBE_STATUS_CONNECTION_ERROR | 3 | A handshake attempt failed (network, server, or timeout). |
PUBNUB_SUBSCRIBE_STATUS_SUBSCRIPTION_CHANGED | 4 | The channel or channel group set changed while actively receiving. |
There is no separate "reconnected" status. PUBNUB_SUBSCRIBE_STATUS_CONNECTED fires both for the first successful handshake and for a handshake that succeeds after PUBNUB_SUBSCRIBE_RECONNECTING. To tell the two apart, track the connection state going into the handshake with pubnub_subscribe_state() instead of looking for a distinct status.
Each status event carries two more fields alongside status:
reason: apubnub_res_tvalue. It isPUBNUB_OKfor every non-error status. ForPUBNUB_SUBSCRIBE_STATUS_DISCONNECTED_UNEXPECTEDLYandPUBNUB_SUBSCRIBE_STATUS_CONNECTION_ERROR, it carries the specificpubnub_res_tcause.http_status_code: populated only whenreasonisPUBNUB_ERR_SERVER;0otherwise.
How the connection state changes
The two diagrams below show every state transition confirmed by the SDK's own state-machine tests. Nodes are the public connection states (PUBNUB_SUBSCRIBE_IDLE, PUBNUB_SUBSCRIBE_CONNECTING, PUBNUB_SUBSCRIBE_CONNECTED, PUBNUB_SUBSCRIBE_RECONNECTING, PUBNUB_SUBSCRIBE_DISCONNECTED, shown without their PUBNUB_SUBSCRIBE_ prefix for readability), and edges are labeled with the triggering call or event. Where a transition emits a status event, the edge label names it; unlabeled transitions change the connection state without emitting any status to on_status.
The diagrams are split into a handshake phase and a receiving phase. PUBNUB_SUBSCRIBE_RECONNECTING and PUBNUB_SUBSCRIBE_DISCONNECTED are reached the same way whether the SDK was attempting a handshake or already receiving messages when the failure or disconnect happened, and pubnub_subscribe_state() cannot tell the two origins apart. Read both diagrams together for the complete picture; each one alone shows only one phase of the connection's life.
Handshake phase
Covers subscribing for the first time, a handshake failure, and a disconnect or unsubscribe issued before any data has been received.
Receiving phase
Covers what happens once the context is CONNECTED and actively receiving, and recovering from a receive failure or a disconnect issued while connected.
Together, the two diagrams cover 28 verified transitions. Two trivial cases are intentionally left out because they leave the state unchanged and emit nothing: changing the channel set while it is already empty and idle, and calling pubnub_subscribe_disconnect while already idle.
Notice that pubnub_subscribe_disconnect does not emit the same status in both diagrams. Called from CONNECTED, it emits PUBNUB_SUBSCRIBE_STATUS_DISCONNECTED. Called during the CONNECTING handshake, it emits no status at all. See Disconnect, reconnect, and unsubscribe-all below.
Handling status events
Callback signature and registration
typedef void (*pubnub_subscribe_status_cb_t)(
const pubnub_subscribe_status_event_t* event,
void* user_data
);
Assign a function of this type to the on_status field of a pubnub_subscribe_listener_t, then register the listener with pubnub_add_listener():
#include <pubnub/features/subscribe.h>
#include <pubnub/features/subscribe_types.h>
#include <pubnub/error.h>
#include <stdio.h>
static void on_status(const pubnub_subscribe_status_event_t* event, void* user_data)
{
(void)user_data;
switch (event->status) {
case PUBNUB_SUBSCRIBE_STATUS_CONNECTED:
printf("Connected. Receiving real-time updates.\n");
break;
case PUBNUB_SUBSCRIBE_STATUS_DISCONNECTED:
printf("Disconnected (requested by the application).\n");
show all 53 linesRegister status listeners on the context
pubnub_add_listener() registers a context-global listener. This is the only listener level that receives status events. Per-subscription and per-set listeners (registered separately, one per subscription or subscription set) also expose an on_status field on their listener struct, but the SDK never invokes it there; dispatch skips any listener bound to a subscription or set. Register on_status on a context-global listener via pubnub_add_listener(). See Listeners for the full callback field list across all three registration levels.
Data lifetime
The pubnub_subscribe_status_event_t the callback receives is stack-allocated at the point the SDK emits it. The pointer is valid only for the duration of that callback invocation. It carries five fields:
- Three scalar values,
status,reason, andhttp_status_code, safe to read anywhere. - Two
pubnub_string_view_tviews,channels(comma-separated list of channels now active) andgroups(comma-separated list of channel groups now active). Both are populated forPUBNUB_SUBSCRIBE_STATUS_CONNECTEDandPUBNUB_SUBSCRIBE_STATUS_SUBSCRIPTION_CHANGED, and empty for the other status categories. They alias memory valid only for the duration of the callback invocation.
If you need the status after the callback returns, copy the scalars into your own storage, and memcpy the channels/groups byte ranges if you need those too. Never retain the event pointer.
Thread and callback context
In cooperative usage, on_status runs on whatever thread calls pubnub_process(). On threaded builds (PUBNUB_CFG_THREAD_SAFETY=1 with a platform provider that supplies threads), the SDK starts a background thread when the first subscription activates, and listener callbacks run on that thread instead, and the SDK invokes them outside any SDK-internal lock. Whether it's safe to call back into the SDK from inside on_status is not documented, so avoid issuing blocking SDK calls from within the callback. For the full async/cooperative/callback consumption model, see Environment Setup.
PUBNUB_CFG_MAX_SUBSCRIBE_LISTENERS (default 8) fixes, at compile time, the total number of listener registrations a context can hold. This pool is shared across all three registration levels, context-global (pubnub_add_listener()), per-subscription, and per-set. It is not a separate allowance for context-global listeners alone. See Environment Setup for build configuration.
Disconnect, reconnect, and unsubscribe-all
Three control functions change connection state directly. Each is declared in pubnub/features/subscribe.h.
| Function | Effect |
|---|---|
pubnub_subscribe_disconnect(ctx) | Stops the active handshake or long-poll. Subscriptions remain registered and can be restarted with pubnub_subscribe_reconnect(). |
pubnub_subscribe_reconnect(ctx) | Resumes a stopped or failed subscription, moving the context back to PUBNUB_SUBSCRIBE_CONNECTING. |
pubnub_subscribe_unsubscribe_all(ctx) | Clears every subscription and channel group, moving the context to PUBNUB_SUBSCRIBE_IDLE. |
static void stop_and_resume(pubnub_context_t* ctx)
{
pubnub_subscribe_disconnect(ctx);
/* ... later ... */
pubnub_subscribe_reconnect(ctx);
/* ... or, to drop every subscription entirely ... */
pubnub_subscribe_unsubscribe_all(ctx);
}
danger
pubnub_subscribe_disconnect does not always emit a statusCalling pubnub_subscribe_disconnect() while the context is PUBNUB_SUBSCRIBE_CONNECTED emits PUBNUB_SUBSCRIBE_STATUS_DISCONNECTED to on_status. Calling it while the context is still PUBNUB_SUBSCRIBE_CONNECTING (the handshake has not completed yet) emits no status event at all, even though it does move the context to PUBNUB_SUBSCRIBE_DISCONNECTED. Do not write listener logic that assumes every disconnect call produces a status event. To confirm a disconnect took effect from the CONNECTING state, poll pubnub_subscribe_state() instead of waiting on on_status.
For general reconnection-policy background shared across PubNub SDKs, see SDK Connection Lifecycle. Retry behavior for individual transactions (as opposed to the subscribe loop's own reconnect/unsubscribe-all controls above) is configured through retry_configuration; see Configuration.
The pubnub_res_t result catalog
Every operation in the SDK, not only subscribe, completes with a pubnub_res_t value. There are 17 values, grouped into 8 numeric classes.
| Value | Numeric | Class | Meaning | Typical behavior* |
|---|---|---|---|---|
PUBNUB_OK | 0 | completion | Operation completed successfully. | Terminal (success). |
PUBNUB_IN_PROGRESS | 1 | completion | Operation is still in progress (non-blocking). | Not an error; keep driving the future. |
PUBNUB_ERR_CANCELLED | 2 | completion | Operation was cancelled. | Terminal. See Future cancellation and teardown. |
PUBNUB_ERR_INVALID_ARGUMENT | 16 | argument/lifecycle | Invalid argument supplied by the caller. | Terminal; fix the call site. |
PUBNUB_ERR_NOT_INITIALIZED | 17 | argument/lifecycle | Client context is not initialized. | Terminal; fix the call site. |
PUBNUB_ERR_PROVIDER_MISSING | 18 | argument/lifecycle | A required provider was not supplied and no compiled-in default exists. | Terminal; fix configuration. |
PUBNUB_ERR_NOT_SUPPORTED | 19 | argument/lifecycle | The feature is not enabled in this build. | Terminal; requires a different build configuration. |
PUBNUB_ERR_OUT_OF_MEMORY | 32 | memory/capacity | Memory allocation failure. | Usually terminal. |
PUBNUB_ERR_BUFFER_TOO_SMALL | 33 | memory/capacity | The supplied buffer is too small for the requested operation. | Terminal for that call; retry with a larger buffer. |
PUBNUB_ERR_QUEUE_FULL | 34 | memory/capacity | The maximum in-flight or pending request limit was reached. | Often transient; retry once in-flight work drains. |
PUBNUB_ERR_TIMEOUT | 48 | timing | The request timed out. | Often transient. |
PUBNUB_ERR_NO_WALL_CLOCK | 49 | timing | Wall-clock time is unavailable, but is required to sign an Access Manager (PAM) request. | Terminal; PAM signing cannot proceed without a clock source. |
PUBNUB_ERR_TRANSPORT | 64 | transport/network | Transport or network failure, including TLS handshake and certificate-verification failures. | Network-dependent; often transient. |
PUBNUB_ERR_SERVER | 80 | server | The server returned an error response. | Depends on the server-supplied detail; see Retrieving server error detail. |
PUBNUB_ERR_SERIALIZATION | 96 | payload | Serialization or deserialization failure. | Usually a data or schema problem; terminal. |
PUBNUB_ERR_CRYPTO | 97 | payload | Crypto operation failure. | Terminal. |
PUBNUB_ERR_INTERNAL | 240 | invariant breaks | Internal or unspecified SDK invariant break. | Terminal; likely an SDK defect. |
* pubnub_res_t carries no retryable flag or comment marking a value as transient or terminal. The Typical behavior column reflects each value's stated meaning, not a documented SDK guarantee. Beyond the specific cases below, the header does not prescribe a recovery action for any value:
PUBNUB_IN_PROGRESSis not an error. Keep driving the operation withpubnub_process()andpubnub_future_is_ready()(cooperative usage) orpubnub_await()(blocking usage).PUBNUB_ERR_CANCELLED: result accessors return zero-initialized values once you see this. Use the completion callback only to release resources; see Future cancellation and teardown.PUBNUB_ERR_TRANSPORT: callpubnub_response_error_message()for diagnostic detail about the transport failure.PUBNUB_ERR_SERVER: callpubnub_response_service_error()for normalized server error detail; see the next section.
Diagnostic strings
pubnub_res_str(pubnub_res_t res) returns a static, non-NULL string describing a result value, for logging. Its output depends on a build-time setting:
- When
PUBNUB_CFG_RES_STRis compiled as1(the default), it returns a short label for every one of the 17 values above, and"Unknown error"for anything outside that set. - When
PUBNUB_CFG_RES_STRis compiled as0, used to shrink binary size on constrained targets, it returns""for every input, including the 17 recognized values. Do not rely onpubnub_res_str()output for anything other than optional diagnostic logging, since a constrained build can legitimately return an empty string for every call.
Retrieving server error detail
When a completed operation's result is PUBNUB_ERR_SERVER, retrieve detail from the underlying pubnub_future_t rather than from the result code alone. The result code only tells you that the server rejected the request, not why.
Different PubNub endpoints report errors in different wire shapes:
- An array-style envelope for publish and signal.
- A simple string-error object for push management.
- A flat object for history-style endpoints.
- A nested object for Access Manager.
- A boolean error-flag object.
- A generic object for anything else.
- Occasionally raw text, when the response body cannot be parsed as JSON at all.
pubnub_response_service_error() normalizes every one of these shapes into a single pubnub_service_error_t, so your error-handling code does not need to branch on which endpoint produced the error.
#include <pubnub/future.h>
#include <pubnub/response.h>
#include <pubnub/service_error.h>
#include <pubnub/error.h>
#include <stddef.h>
#include <stdio.h>
static void log_server_error(pubnub_future_t future)
{
pubnub_res_t result = pubnub_future_status(future);
if (PUBNUB_ERR_SERVER == result) {
pubnub_service_error_t err;
pubnub_response_service_error(future, &err);
show all 38 linesRelated accessors for other cases:
pubnub_response_status_code(future)returns the raw HTTP status code, or0if the future is not complete, is invalid, or no HTTP response was ever received (a transport-level failure before any server reply).pubnub_response_body(future)returns the raw, not-NUL-terminated response body.pubnub_response_error_message(future)is a shim overpubnub_response_service_error()that returns just themessagefield, useful for simple logging without the full struct.pubnub_service_error_channel_count(future)/pubnub_service_error_channel_at(future, index)iterate the channels named in an error that identifies specific affected channels.pubnub_service_error_channel_at()returns an empty view for an out-of-range index.
danger
pubnub_future_releaseservice, message, source, every detail's message and location, every channel-name view, and the raw response body from pubnub_response_body() are all views into memory owned by the future. Every one of them is valid only until you call pubnub_future_release() on that future. Read and copy out anything you need before releasing; using one of these views afterward is a use-after-free.
The pubnub_service_error_t struct also exposes error_flag (whether the server explicitly flagged the response as an error) and code (a numeric sub-code, populated for file-related operations only at this time). source is populated only for Access Manager-style (PAM) errors.
Future cancellation and teardown
Every asynchronous operation's completion callback, pubnub_async_cb_t, fires exactly once when its future reaches a terminal state. That includes context teardown. Calling pubnub_destroy() or pubnub_deinit() while operations are still pending or in flight causes their callbacks to fire with status set to PUBNUB_ERR_CANCELLED.
When you see PUBNUB_ERR_CANCELLED, in a completion callback or from a future's status:
- Result accessors return zero-initialized values. Do not call them expecting real data.
- Use the callback only to release resources you own (buffers, contexts, application state tied to that operation), not to inspect a result.
pubnub_future_cancel(future) produces the same contract explicitly. For a pending future, the completion callback fires immediately with PUBNUB_ERR_CANCELLED. For a future already in flight, the callback fires once cancellation is confirmed. Cancelling a future does not release its slot; call pubnub_future_release() afterward regardless of how the future ended.
#include <pubnub/future.h>
#include <pubnub/error.h>
static void on_complete(pubnub_future_t future, pubnub_res_t status, void* user_data)
{
(void)user_data;
if (PUBNUB_ERR_CANCELLED == status) {
/* Result accessors return zero-initialized values here. Release
any resources you own and return -- do not read the result. */
pubnub_future_release(future);
return;
}
/* ... handle the real result using the response/service-error
show all 27 lines