---
source_url: https://www.pubnub.com/docs/sdks/c/status-events
title: Status Events for C SDK
updated_at: 2026-09-24T12:25:41.000Z
sdk_name: PubNub C SDK
sdk_version: 1.0.0
---

# Status Events for C SDK

PubNub C SDK, use the latest version: 1.0.0

## 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.

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.

```c
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`: a `pubnub_res_t` value. It is `PUBNUB_OK` for every non-error status. For `PUBNUB_SUBSCRIBE_STATUS_DISCONNECTED_UNEXPECTEDLY` and `PUBNUB_SUBSCRIBE_STATUS_CONNECTION_ERROR`, it carries the specific `pubnub_res_t` cause.
* `http_status_code`: populated only when `reason` is `PUBNUB_ERR_SERVER`; `0` otherwise.

## 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](#disconnect-reconnect-and-unsubscribe-all) below.

## Handling status events

### Callback signature and registration

```c
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()`:

```c
#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");
        break;
    case PUBNUB_SUBSCRIBE_STATUS_DISCONNECTED_UNEXPECTEDLY:
        printf("Connection lost unexpectedly: %s\n", pubnub_res_str(event->reason));
        break;
    case PUBNUB_SUBSCRIBE_STATUS_CONNECTION_ERROR:
        printf("Could not establish the connection: %s\n", pubnub_res_str(event->reason));
        if (PUBNUB_ERR_SERVER == event->reason) {
            printf("Server HTTP status: %u\n", (unsigned)event->http_status_code);
        }
        break;
    case PUBNUB_SUBSCRIBE_STATUS_SUBSCRIPTION_CHANGED:
        printf("Channel or channel group set changed.\n");
        break;
    default:
        printf("Unrecognized status value: %d\n", (int)event->status);
        break;
    }
}

/* Registers the context-global status listener. Returns the handle you
   must keep to remove the listener later, or PUBNUB_LISTENER_HANDLE_INVALID
   on failure. */
static pubnub_listener_handle_t register_status_listener(pubnub_context_t* ctx)
{
    pubnub_subscribe_listener_t listener = { 0 };
    listener.on_status = on_status;

    return pubnub_add_listener(ctx, &listener);
}

/* Call on every exit path once the listener is no longer needed, for
   example immediately before pubnub_destroy() or pubnub_deinit(). */
static void unregister_status_listener(pubnub_context_t* ctx, pubnub_listener_handle_t handle)
{
    if (PUBNUB_LISTENER_HANDLE_INVALID != handle) {
        pubnub_remove_listener(ctx, handle);
    }
}
```

### Register 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](https://www.pubnub.com/docs/sdks/c/api-reference/publish-and-subscribe.md#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`, and `http_status_code`, safe to read anywhere.
* Two `pubnub_string_view_t` views, `channels` (comma-separated list of channels now active) and `groups` (comma-separated list of channel groups now active). Both are populated for `PUBNUB_SUBSCRIBE_STATUS_CONNECTED` and `PUBNUB_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](https://www.pubnub.com/docs/sdks/c/environment-setup.md).

`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](https://www.pubnub.com/docs/sdks/c/environment-setup.md) 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`. |

```c
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);
}
```

:::warning danger
pubnub_subscribe_disconnect
does not always emit a status
Calling `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](https://www.pubnub.com/docs/general/setup/connection-management.md#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](https://www.pubnub.com/docs/sdks/c/api-reference/configuration.md).

## 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](#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](#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_PROGRESS` is not an error. Keep driving the operation with `pubnub_process()` and `pubnub_future_is_ready()` (cooperative usage) or `pubnub_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](#future-cancellation-and-teardown).
* `PUBNUB_ERR_TRANSPORT`: call `pubnub_response_error_message()` for diagnostic detail about the transport failure.
* `PUBNUB_ERR_SERVER`: call `pubnub_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_STR` is compiled as `1` (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_STR` is compiled as `0`, used to shrink binary size on constrained targets, it returns `""` for every input, including the 17 recognized values. Do not rely on `pubnub_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.

```c
#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);
        printf("Server error %u: %.*s\n",
               (unsigned)err.status,
               (int)err.message.len,
               err.message.ptr);

        for (size_t i = 0; i < pubnub_service_error_detail_count(future); ++i) {
            pubnub_service_error_detail_t detail;

            pubnub_service_error_detail_at(future, i, &detail);
            printf("  %.*s: %.*s\n",
                   (int)detail.location.len,
                   detail.location.ptr,
                   (int)detail.message.len,
                   detail.message.ptr);
        }
    }

    /* err.message, detail.location, and detail.message above are views
       into memory owned by the future. Read everything you need BEFORE
       releasing it -- once you call pubnub_future_release, every view
       returned by the accessors above becomes invalid. */
    pubnub_future_release(future);
}
```

Related accessors for other cases:

* `pubnub_response_status_code(future)` returns the raw HTTP status code, or `0` if 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 over `pubnub_response_service_error()` that returns just the `message` field, 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.

:::warning danger
Every returned view expires at
pubnub_future_release
`service`, `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.

```c
#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
       accessors, then release ... */
    pubnub_future_release(future);
}

/* Cancels a pending or in-flight future. Its completion callback, if any,
   still fires exactly once, with PUBNUB_ERR_CANCELLED. Cancelling does not
   release the future's slot -- release it once you're done with it. */
static void cancel_and_release(pubnub_future_t future)
{
    pubnub_future_cancel(future);
    pubnub_future_release(future);
}
```

## Terms in this document

* **Channel** - A pathway for sending and receiving messages between devices, created automatically when you first use it, that can handle any number of users and messages for different communication needs, like 1-1 text chats, group conversations, and other data streaming.
* **Channel (DataSync)** - A built-in entity class in DataSync that stores channel metadata as a JSON payload. The same channel used for messaging and Presence.
* **Channel pattern** - A way to group and analyze channel data to track performance metrics like message counts and user engagement over time with PubNub Insights.
* **Listener** - A function or objectthat reacts to events or messages, like new chat messages or connection updates, letting your app respond in real-time.
* **PubNub** - PubNub is a real-time messaging platform that provides APIs and SDKs for building scalable applications. It handles the complex infrastructure of real-time communication, including: Message delivery and persistence, Presence detection, Access control, Push notifications, File sharing, Serverless processing with Functions and Events & Actions, Analytics and monitoring with BizOps Workspace, AI-powered insights with Illuminate.

Last updated at: 2026-09-24T12:25:41.000Z
