---
source_url: https://www.pubnub.com/docs/sdks/c/troubleshooting
title: Troubleshooting the C SDK
updated_at: 2026-09-24T12:25:41.000Z
sdk_name: PubNub C SDK
sdk_version: 1.0.0
---

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

This page is organized by symptom. Find the section that matches what you are seeing; each entry gives the cause and the minimum fix. For conceptual explanations of a feature, see the linked channel-level API reference page instead.

:::note Looking for PUBNUB_ASSERT?
The previous C-Core SDK's `PUBNUB_ASSERT` macro and its assert-handler mechanism do not exist in this SDK. They aren't in `include/`, in `src/`, or under any other name. There is no macro to configure and nothing to migrate. This SDK's equivalents are the compile-time `#error` diagnostics described under [Build failures](#build-failures) below, plus the runtime `pubnub_res_t` catalog described in [Status Events](https://www.pubnub.com/docs/sdks/c/status-events.md#the-pubnub_res_t-result-catalog).
:::

Before debugging anything, raise the log level. A zero-initialized `pubnub_config_t` silences the SDK completely (`log_level` defaults to `PUBNUB_LOG_LEVEL_NONE`). Use `pubnub_config_defaults()`, or set it explicitly:

```c
pubnub_config_t cfg = pubnub_config_defaults();
cfg.log_level = PUBNUB_LOG_LEVEL_DEBUG; /* PUBNUB_LOG_LEVEL_TRACE for full I/O */
```

See [Logging](https://www.pubnub.com/docs/sdks/c/logging.md) for the full level catalog and how to attach a custom logger.

## Build failures

### A feature function is undefined at compile time

```text
error: implicit declaration of function 'pubnub_here_now'
```

(Or, depending on the compiler: `use of undeclared identifier 'pubnub_here_now'`.)

**Cause:** The corresponding `PUBNUB_ENABLE_*` flag is `OFF` in your build configuration. Every feature function's entire declaration is wrapped in `#if PUBNUB_ENABLE_<FEATURE>` in its header, so with the flag off the header contributes nothing and the identifier doesn't exist for the preprocessor. This is a compile-time error, not a link-time one — the linker never gets involved, because there's no declaration to call in the first place. The `minimal` and `embedded` build profiles disable most optional features by default.

**Fix:** Enable the feature explicitly, or switch to the `full` profile.

```bash
cmake --preset full
# or, for a targeted flag on top of another profile:
cmake -B build -DPUBNUB_PROFILE=minimal -DPUBNUB_ENABLE_PRESENCE=ON
```

See [Feature flags](https://www.pubnub.com/docs/sdks/c/environment-setup.md#feature-flags) for the full flag matrix by profile.

### Crash on a strictly-aligned target (Cortex-M0, ARMv6-M)

**Cause:** A `uint8_t` array has alignment 1; `pubnub_context_t` requires `alignof(max_align_t)`. Casting an under-aligned pointer to `pubnub_context_t*` is undefined behavior and faults on hardware that enforces strict alignment.

**Fix:** Apply `PUBNUB_ALIGNAS` from `pubnub/pubnub_compat.h` to any statically or globally declared context buffer:

```c
#include "pubnub/pubnub_compat.h"

PUBNUB_ALIGNAS(max_align_t) static uint8_t ctx_storage[PUBNUB_CONTEXT_SIZE];
pubnub_context_t* ctx = (pubnub_context_t*)ctx_storage;
pubnub_init(ctx, &cfg);
```

`PUBNUB_CONTEXT_SIZE` is a compile-time upper bound set by the SDK's own CMake configuration (the configure output prints it). The SDK's own build asserts `sizeof(struct pubnub_context) <= PUBNUB_CONTEXT_SIZE`, so it's always guaranteed large enough for the build you're compiling against; call `pubnub_context_size()` at runtime if you want the exact size instead of the upper bound. Always wrap the buffer in `PUBNUB_ALIGNAS`, even when you size it with the compile-time constant. See [Utility Methods — Context size](https://www.pubnub.com/docs/sdks/c/api-reference/misc.md#context-size).

### Unknown build profile

```bash
cmake -B build -DPUBNUB_PROFILE=bare-metal
```

```text
[PubNub] Unknown profile: 'bare-metal'. Known profiles: full, minimal, embedded
```

**Cause:** Only `full`, `minimal`, and `embedded` are implemented in `cmake/profiles.cmake`. A stale comment elsewhere references a fourth "bare-metal" profile for FPU-less Cortex-M0/M0+ targets, but it was never implemented.

**Fix:** Pass one of the three real profiles. See [Build profiles and presets](https://www.pubnub.com/docs/sdks/c/environment-setup.md#build-profiles-and-presets).

### PUBNUB_ENABLE_PAM without PUBNUB_ENABLE_CRYPTO

```bash
cmake -B build -DPUBNUB_PROFILE=minimal -DPUBNUB_ENABLE_PAM=ON
```

```text
[PubNub] PUBNUB_ENABLE_PAM requires PUBNUB_ENABLE_CRYPTO. PAM request signing depends on the crypto provider.
```

**Cause:** Access Manager request signing needs the crypto provider's `hmac_sha256` vtable member, so the two flags are validated together at configure time.

**Fix:** Turn on `PUBNUB_ENABLE_CRYPTO=ON` whenever `PUBNUB_ENABLE_PAM=ON`, or leave PAM off. See [Configure-time provider conflicts](https://www.pubnub.com/docs/sdks/c/environment-setup.md#configure-time-provider-conflicts).

### Provider family left unset, or given an unrecognized name

```bash
cmake -B build -DPUBNUB_PROVIDER_TRANSPORT=libcurl
```

```text
[PubNub] transport provider is not set. Set -DPUBNUB_PROVIDER_TRANSPORT=<value> or select a profile (-DPUBNUB_PROFILE=full|minimal|embedded). Known providers: curl, socket, custom
```

**Cause:** Either a provider family's cache variable is empty and no profile was selected to default it, or it was set to a typo'd/unimplemented name (here, `libcurl` instead of `curl`).

**Fix:** Pass `-DPUBNUB_PROFILE=...`, or set every `PUBNUB_PROVIDER_*` explicitly to one of its exact known values:

* transport: `curl`, `socket`, `custom`
* serialization: `cjson`, `jsmn`, `custom`
* crypto: `openssl`, `mbedtls`, `none`, `custom`
* logger: `stdout`, `none`, `custom`
* allocator: `stdlib`, `arena`, `custom`
* platform: `posix`, `windows`, `freertos`, `custom`

See [Provider backends](https://www.pubnub.com/docs/sdks/c/environment-setup.md#provider-backends).

### cJSON serialization with a non-stdlib allocator

```bash
cmake -B build -DPUBNUB_PROVIDER_SERIALIZATION=cjson -DPUBNUB_PROVIDER_ALLOCATOR=arena
```

```text
[PubNub] Invalid provider combination: PUBNUB_PROVIDER_SERIALIZATION=cjson requires PUBNUB_PROVIDER_ALLOCATOR=stdlib (got 'arena').
```

**Cause:** cJSON's memory hooks are process-global state, incompatible with a per-context allocator such as `arena`.

**Fix:** Switch serialization to `jsmn` (`-DPUBNUB_PROVIDER_SERIALIZATION=jsmn`), or switch the allocator back to `stdlib`.

### PUBNUB_CFG_PIPELINE_MAX_MIDDLEWARES below the computed floor

```text
[PubNub] PUBNUB_CFG_PIPELINE_MAX_MIDDLEWARES=2 is less than the minimum required (6) for enabled features.
```

**Cause:** The middleware-slot floor is 3 (pnsdk identification, user ID, auth token, always present) plus 1 if Access Manager is on (signature), plus 1 if retry is on, plus 1 if request compression is on. The `full` profile has all three on, so its floor is 6. This error fires only when you explicitly set the variable below the floor; leaving it unset lets the floor apply silently.

**Fix:** Raise the value to at least the printed minimum, or disable the feature that raised the floor.

## Initialization failures

### pubnub_create() returns NULL

Check in order:

1. `cfg.subscribe_key` or `cfg.user_id` is `NULL` or empty — both are required.
2. The allocator ran out of memory (most likely on embedded, with a small arena).
3. A required provider is missing and initialization failed before the pointer was returned.

Enable `PUBNUB_LOG_LEVEL_DEBUG` and re-run; the SDK logs the first failing step.

:::note pubnub_create() doesn't exist on every profile
`pubnub_create()` is not compiled when `PUBNUB_CFG_NO_HEAP=1` (the `embedded` preset's default). On that profile, use `pubnub_init()` with a caller-provided buffer instead — see [pubnub_create()/pubnub_destroy() undeclared](#pubnub_createpubnub_destroy-undeclared) below.
:::

### pubnub_init() returns PUBNUB_ERR_PROVIDER_MISSING

**Cause:** A required provider family has no compiled-in default, the corresponding field in `pubnub_config_t` is `NULL`, and the family is one of the four the SDK validates (allocator, transport, serialization, platform). See [Providers — How provider resolution and validation works](https://www.pubnub.com/docs/sdks/c/api-reference/providers.md#how-provider-resolution-and-validation-works) for exactly which members are mandatory per family.

### pubnub_init() / pubnub_create() returns PUBNUB_ERR_INVALID_ARGUMENT

`ctx` is `NULL`, `config` is `NULL`, `config->subscribe_key` is `NULL` or empty, or `config->user_id` is `NULL` or empty.

## Futures never become ready (cooperative targets)

**Cause:** On builds without a background thread, nothing drives transport I/O unless something calls `pubnub_process()`. If you're using the `pubnub_future_is_ready()` poll loop but never calling `pubnub_process()`, the future stays `PUBNUB_IN_PROGRESS` indefinitely.

**Fix:** Drive the event loop explicitly:

```c
while (!pubnub_future_is_ready(fut)) {
    pubnub_process(ctx);
    /* optional: sleep or yield here */
}
```

`pubnub_await()` drives `pubnub_process()` internally on a cooperative build, so switching to `pubnub_await(fut)` is the simplest fix when blocking the calling thread is acceptable. See [Utility Methods — Futures and async lifecycle](https://www.pubnub.com/docs/sdks/c/api-reference/misc.md#futures-and-async-lifecycle).

## PUBNUB_ERR_QUEUE_FULL — request slots exhausted

Every SDK call reserves a slot from a pool bounded by `PUBNUB_CFG_MAX_IN_FLIGHT_REQUESTS` (`full`: 16, `minimal`: 8, `embedded`: 2). Once that pool is full, new requests enter a pending queue bounded by `PUBNUB_CFG_MAX_PENDING_REQUESTS` (`full`: 32, `minimal`: 8, `embedded`: 2). `PUBNUB_ERR_QUEUE_FULL` is returned only when **both** are exhausted.

The most common cause is forgetting `pubnub_future_release()` on completed futures, which permanently occupies a slot:

```c
/* WRONG: early return skips release */
pubnub_future_t fut = pubnub_publish(ctx, &opts);
pubnub_res_t st = pubnub_await(fut);
if (PUBNUB_OK != st) { return st; } /* fut never released */
pubnub_future_release(fut);

/* CORRECT: release on every path */
pubnub_future_t fut = pubnub_publish(ctx, &opts);
pubnub_res_t st = pubnub_await(fut);
pubnub_future_release(fut); /* before any return */
if (PUBNUB_OK != st) { return st; }
```

Also watch for:

* Futures stored in structs that get freed without releasing.
* Loop bodies that issue a new request before releasing the previous future.
* Using `pubnub_async()` and never releasing inside the callback.

To increase the slot budget:

```cmake
-DPUBNUB_CFG_MAX_IN_FLIGHT_REQUESTS=32
-DPUBNUB_CFG_MAX_PENDING_REQUESTS=64
```

On the arena allocator (the `embedded` profile's default), raising `PUBNUB_CFG_MAX_IN_FLIGHT_REQUESTS` also raises the memory budget — see [Arena pool exhausts after a few requests](#arena-pool-exhausts-after-a-few-requests) below for the sizing math. On `stdlib` (`full`/`minimal`), it mainly costs concurrent heap.

## Subscribe listener callbacks never fire

**Threaded builds:** The background I/O thread starts automatically when the first subscription is activated. If no subscription has been activated yet, the thread isn't running and no callbacks fire.

**Cooperative builds:** Callbacks fire only during `pubnub_process()` calls. This also applies when `PUBNUB_CFG_THREAD_SAFETY=0` (the `embedded` profile) or when the platform provider lacks `thread_create`/`thread_join`: the SDK silently skips starting the background thread and returns no error, so you must drive I/O yourself. If nothing is calling `pubnub_process()`, no callbacks will fire. See [Futures never become ready](#futures-never-become-ready-cooperative-targets) above.

## String views in subscribe callbacks are invalid after the callback returns

`pubnub_string_view_t` fields inside callback event structs (for example `channels` and `groups` in `pubnub_subscribe_status_event_t`) alias the event's backing storage. They're valid **only for the duration of the callback**. Storing the pointer and reading it after the callback returns is a use-after-free.

**Fix:** Copy into a caller-owned buffer before returning:

```c
static void on_status(const pubnub_subscribe_status_event_t* event, void* user_data)
{
    (void)user_data;
    char buf[256];
    size_t n = event->channels.len < sizeof(buf) ? event->channels.len : sizeof(buf) - 1;
    memcpy(buf, event->channels.ptr, n);
    buf[n] = '\0';
    /* buf is safe to use after this callback returns; event->channels.ptr is not */
}
```

This applies to every `pubnub_string_view_t` and `pubnub_timetoken_t` field returned by a subscribe callback. See [Utility Methods — String views](https://www.pubnub.com/docs/sdks/c/api-reference/misc.md#string-views) for the contract in full, including how it differs from a future-owned view (which survives until `pubnub_future_release`, not just until a callback returns).

## Network and TLS failures (PUBNUB_ERR_TRANSPORT)

`PUBNUB_ERR_TRANSPORT` covers all network-layer failures, including TLS. Get the diagnostic string from the transport layer:

```c
if (PUBNUB_ERR_TRANSPORT == pubnub_future_status(fut)) {
    pubnub_string_view_t detail = pubnub_response_error_message(fut);
    fprintf(stderr, "transport: %.*s\n", (int)detail.len, detail.ptr);
}
```

### TLS certificate verification failure

Use a custom CA bundle for enterprise proxies or self-signed development servers:

```c
pubnub_set_tls_ca_bundle(ctx, pem_chain); /* PEM, NULL = system store */
```

To disable verification in development (never in production):

```c
pubnub_set_tls_verify(ctx, 1); /* non-zero skip_verify = skip verification */
```

:::warning Never ship with verification disabled
Disabling peer verification exposes connections to man-in-the-middle attacks.
:::

### Custom DNS has no effect with the curl transport

`CURLOPT_DNS_SERVERS` requires libcurl to be built with c-ares. The `curl` transport calls `curl_easy_setopt(..., CURLOPT_DNS_SERVERS, ...)` unconditionally and does not check the result. When libcurl lacks c-ares, libcurl itself silently ignores the option and falls back to the system resolver — there is no SDK-side detection and **no log line at any level** tells you this happened. If custom DNS servers configured via `pubnub_set_dns_servers()` seem to have no effect on the curl transport, this is the first thing to check, not something you'll be warned about.

To confirm whether your libcurl has c-ares:

```bash
curl-config --features | grep -i ares   # prints "AsynchDNS" when c-ares is present
```

The `socket` transport has its own built-in DNS resolver and always honors `pubnub_set_dns_servers()`, with no libcurl dependency. See [Providers — Transport](https://www.pubnub.com/docs/sdks/c/api-reference/providers.md#transport) for the `set_dns_servers` vtable member this backs.

### Connections drop silently after inactivity (NAT / VPN timeout)

TCP keepalive detects dead peers before a request hangs. `pubnub_config_defaults()` enables it (idle 60s, interval 20s, 3 probes — `PUBNUB_TCP_KEEPALIVE_CONFIG_INIT`). With a zero-initialized config, keepalive is disabled. Enable it explicitly:

```c
cfg.tcp_keepalive.enabled      = 1;
cfg.tcp_keepalive.idle_sec     = 60;
cfg.tcp_keepalive.interval_sec = 20;
cfg.tcp_keepalive.probe_count  = 3;
```

## Server errors (PUBNUB_ERR_SERVER)

Both an HTTP 4xx/5xx response and a `200` whose body reports a logical failure map to `PUBNUB_ERR_SERVER`. Retrieve the HTTP status and error message:

```c
int http = pubnub_response_status_code(fut);
pubnub_string_view_t msg = pubnub_response_error_message(fut);
fprintf(stderr, "HTTP %d: %.*s\n", http, (int)msg.len, msg.ptr);
```

| HTTP | Typical cause | Action |
| --- | --- | --- |
| 400 | Malformed request, missing required field | Enable DEBUG logging; check the opts struct |
| 403 | Auth token missing, expired, or lacking permission | Refresh the token; check the PAM grant |
| 429 | Rate-limited | Reduce publish rate; use a retry policy |
| 500 / 503 | Server-side error | Retry with backoff; check PubNub status |

## Request timeouts (PUBNUB_ERR_TIMEOUT)

Both timeout fields default to `0` in a zero-initialized config, which means "use the compile-time default" — 10,000 ms for transactions and 310,000 ms for subscribe long-polls, across all three profiles. The SDK does **not** run without a timeout when the field is zero.

If timeouts fire too often on slow connections, increase the values:

```c
cfg.transaction_timeout_ms     = 30000;  /* 30 s */
cfg.non_transaction_timeout_ms = 310000; /* subscribe long-poll, keep >= server presence_timeout */
```

A file upload can also produce `PUBNUB_ERR_TIMEOUT` when the S3 presigned URL (issued at the start of `pubnub_send_file()`) expires before the transfer completes. Increasing `transaction_timeout_ms`, or uploading smaller files, resolves this.

## PAM — PUBNUB_ERR_NO_WALL_CLOCK

PAM HMAC-SHA256 request signing needs an accurate epoch timestamp from the platform provider's `wall_clock_ms` member. There are two different failure modes here, and they surface at two different times:

* **wall_clock_ms is NULL.** This is a mandatory platform-vtable member (along with `monotonic_ms`, `sleep_ms`, and `random_bytes`), so a `NULL` value is caught immediately at `pubnub_create()`/`pubnub_init()` with `PUBNUB_ERR_PROVIDER_MISSING`. Context creation fails outright; no PAM request is ever attempted.
* **wall_clock_ms is present but returns 0.** Context creation succeeds. The failure surfaces later, only when a PAM-signed request actually tries to sign itself, as `PUBNUB_ERR_NO_WALL_CLOCK`. This is the case for a platform with no RTC and no NTP synchronization yet.

**Fix:** Implement `wall_clock_ms` in your platform provider vtable so it returns the current epoch time in milliseconds as `pubnub_milliseconds_t`, and returns a real (non-zero) value once time is available. See [Providers — Platform](https://www.pubnub.com/docs/sdks/c/api-reference/providers.md#platform) for the full vtable contract and how this differs from `monotonic_ms` (boot-relative, unrelated to PAM).

:::note secret_key belongs on a trusted server only
`secret_key` must be set in `pubnub_config_t` for grant operations. Grant must only run on a trusted server — never embed the secret key in device firmware.
:::

## Crypto — wrong module causes PUBNUB_ERR_CRYPTO on decrypt

Messages encrypted by one module cannot be decrypted by the other. Ensure all publishers and subscribers use the same module:

* **AES-CBC (ACRH) module** (`pubnub_crypto_module_aes_cbc()`): identified on the wire as `"ACRH"`; the recommended cryptor for all new encryption.
* **Legacy module** (`pubnub_crypto_module_legacy()`): backward compatibility with data encrypted by older SDKs only. Don't choose it for new encryption.

See [Encryption](https://www.pubnub.com/docs/sdks/c/api-reference/encryption.md) for the full migration story between the two.

### Encrypt buffer too small

Query the required output size before calling `pubnub_crypto_module_encrypt_buf()`:

```c
size_t needed = pubnub_crypto_module_encrypt_size(module, plaintext_len);
uint8_t* out  = malloc(needed);
pubnub_res_t rc = pubnub_crypto_module_encrypt_buf(module, plaintext, plaintext_len,
                                                    out, needed, &out_len);
```

Unlike encrypt, `pubnub_crypto_module_decrypt_buf()` does **not** validate its output buffer against the input size — see [Undersized decrypt buffer overflows instead of erroring](#undersized-decrypt-buffer-overflows-instead-of-erroring) below.

### Custom module (pubnub_crypto_module_create()) — destroy order matters

A module created with `pubnub_crypto_module_create()` does **not** own the cryptors you passed in. Destroy the module first, then destroy each cryptor:

```c
pubnub_crypto_module_destroy(module); /* first */
pubnub_cryptor_destroy(my_cryptor);   /* then */
```

Destroying cryptors before the module leaves the module holding dangling pointers.

## Embedded / no-heap profile issues

### pubnub_create()/pubnub_destroy() undeclared

```text
error: use of undeclared identifier 'pubnub_create'
```

(Or, depending on the compiler: `implicit declaration of function 'pubnub_create'`.)

**Cause:** `PUBNUB_CFG_NO_HEAP=1`, the default the `embedded` preset forces on, removes `pubnub_create()`/`pubnub_destroy()` from the header entirely (they're compiled out with `#if !PUBNUB_CFG_NO_HEAP`). Only `pubnub_init()`/`pubnub_deinit()` on caller-supplied memory remain.

**Fix:** Switch to `pubnub_init()`/`pubnub_deinit()` with a `PUBNUB_CONTEXT_SIZE`-sized, correctly aligned buffer, or rebuild with `PUBNUB_CFG_NO_HEAP=0` if heap is actually available on the target.

```c
#include <pubnub/client.h>
#include "pubnub/pubnub_compat.h"

static PUBNUB_ALIGNAS(max_align_t) uint8_t s_ctx_mem[PUBNUB_CONTEXT_SIZE];

static pubnub_context_t* create_ctx(const pubnub_config_t* cfg)
{
    pubnub_context_t* ctx = (pubnub_context_t*)s_ctx_mem;
    return (PUBNUB_OK == pubnub_init(ctx, cfg)) ? ctx : NULL;
}
```

See [No-heap operation and static context storage](https://www.pubnub.com/docs/sdks/c/environment-setup.md#no-heap-operation-and-static-context-storage) for the complete worked example.

### Arena pool exhausts after a few requests

All memory for in-flight requests comes from an arena whose total size is:

```text
N × (PUBNUB_CFG_RESPONSE_BUFFER_SIZE + PUBNUB_CFG_OBJECT_BUFFER_SIZE + PUBNUB_CFG_SCRATCH_BUFFER_SIZE)
```

where `N = PUBNUB_CFG_MAX_IN_FLIGHT_REQUESTS`. `embedded`-profile defaults: `RESPONSE = 4096`, `OBJECT = 2048`, `SCRATCH = 1024`, `N = 2` → 14,336 bytes minimum. Increase the arena (`PUBNUB_CFG_ARENA_POOL_SIZE`) or reduce `N`:

```cmake
-DPUBNUB_CFG_MAX_IN_FLIGHT_REQUESTS=2
-DPUBNUB_CFG_ARENA_POOL_SIZE=32768
```

This formula and these defaults are specific to the `arena` allocator backend (the `embedded` profile's default). On `stdlib` (`full`/`minimal`), buffers come from the heap on demand instead of a fixed pool, so this fixed-budget math doesn't apply.

## Common API misuse patterns

### pubnub_string_view_t assumed to be null-terminated

`pubnub_string_view_t` (and its alias `pubnub_timetoken_t`) are **not** null-terminated. Using `strlen`, `strcmp`, or `printf("%s")` on `.ptr` is undefined behavior.

```c
pubnub_string_view_t tt = pubnub_publish_result_timetoken(fut);
printf("%.*s\n", (int)tt.len, tt.ptr);  /* correct */
/* printf("%s\n", tt.ptr);              WRONG -- no NUL guarantee */
```

See [Utility Methods — String views](https://www.pubnub.com/docs/sdks/c/api-reference/misc.md#string-views).

### Zero-initialized opts silently disables important defaults

Two opts structs have non-zero defaults that their `PUBNUB_*_OPTS_INIT` macro sets correctly, but a bare `= {0}` zeroes out:

| Struct | What `{0}` silently disables | Use instead |
| --- | --- | --- |
| `pubnub_fetch_messages_opts_t` | `include_uuid = 1`, `include_message_type = 1` | `PUBNUB_FETCH_MESSAGES_OPTS_INIT` |
| `pubnub_here_now_opts_t` | `include_uuids = 1` | `PUBNUB_HERE_NOW_OPTS_INIT` |

With `include_uuid` off, every fetched message has an empty UUID field. With `include_uuids` off, here-now returns only occupant counts, not the UUIDs.

### Result accessors called after pubnub_future_release()

After release, the generation counter detects the stale future and every accessor returns a zero-initialized value. String-view pointers that aliased the slot's response buffer are invalid. Copy what you need before releasing:

```c
pubnub_timetoken_t tt = pubnub_publish_result_timetoken(fut);
char buf[18] = {0};
memcpy(buf, tt.ptr, tt.len);
pubnub_future_release(fut);  /* safe to release now */
/* use buf, not tt.ptr */
```

### pubnub_json_destroy() not called after pubnub_publish()

`message_value` is borrowed — the SDK serializes the tree during `pubnub_publish()` but doesn't free it. Destroy the tree immediately after the call returns:

```c
/* PUBNUB_JSON_OBJ()'s (and pubnub_json_destroy()'s) first argument is the
   context's serialization provider, not a JSON value -- despite how easy
   it is to name that variable "json" by habit. */
pubnub_serialization_provider_t* serial = pubnub_serialization(ctx);
pubnub_json_value_t* msg = PUBNUB_JSON_OBJ(serial, PUBNUB_JSON_KV_STR(serial, "text", "hello"));
pubnub_future_t fut = pubnub_publish(ctx, &(pubnub_publish_opts_t){
    .channel = "ch", .message_value = msg,
});
pubnub_json_destroy(serial, msg); /* before await -- tree is no longer needed */
pubnub_res_t st = pubnub_await(fut);
pubnub_future_release(fut);
```

Applying this rule to `app_context.h`'s `custom_value` instead causes a double free — App Context takes ownership of that tree. See [FAQ](https://www.pubnub.com/docs/sdks/c/faq.md#i-built-a-pubnub_json_value_t-tree-for-a-request-option-do-i-free-it-or-does-the-sdk) for the full per-field ownership breakdown.

### pubnub_set_memberships() / pubnub_set_channel_members() with both counts zero

Both functions return `PUBNUB_ERR_INVALID_ARGUMENT` immediately when `set_count` and `remove_count` are both `0`. Populate at least one array before calling.

### pubnub_publish() called with two strings

```text
pubnub_publish(ctx, "my_channel", "{\"text\":\"hello\"}");
```

```text
error: too many arguments to function call, expected 2, have 3
```

**Cause:** `pubnub_publish()` takes exactly two parameters: the context and a pointer to a `pubnub_publish_opts_t`. That two-string shape belongs to the previous, incompatible C-Core SDK.

**Fix:** Build a `pubnub_publish_opts_t`, set `.channel` and `.message`, and pass its address. See [Publish](https://www.pubnub.com/docs/sdks/c/api-reference/publish-and-subscribe.md#publish) for the full field table and a complete sample.

### Listener callback with the wrong signature

```c
static void on_status(pubnub_context_t* ctx, const pubnub_subscribe_status_event_t* event)
{
    /* ... */
}

listener.on_status = on_status;
```

```text
warning: incompatible function pointer types assigning to 'pubnub_subscribe_status_cb_t' from 'void (*)(pubnub_context_t *, const pubnub_subscribe_status_event_t *)'
```

This is an unconditional error under this SDK's own `-Wall -Wextra -Wpedantic -Werror` build, and under recent Clang's default C diagnostics. On an older or laxer toolchain, you may see only a warning, and a binary that appears to work until the mismatched call actually runs.

**Cause:** Every subscribe listener callback typedef takes exactly two parameters: the event by `const` pointer and `void* user_data`, with no context parameter. A callback that needs the context must recover it through `user_data`.

**Fix:**

```c
static void on_status(const pubnub_subscribe_status_event_t* event, void* user_data)
{
    pubnub_context_t* ctx = (pubnub_context_t*)user_data;
    (void)ctx;
    (void)event;
}
```

See [Status Events — Callback signature and registration](https://www.pubnub.com/docs/sdks/c/status-events.md#callback-signature-and-registration).

### Custom allocator init/deinit signature mismatch

```text
error: incompatible pointer types assigning to 'int (*)(struct pubnub_allocator_provider *, struct pubnub_platform_provider *)' from 'int (*)(struct pubnub_allocator_provider *, const pubnub_provider_deps_t *)'
```

**Cause:** The allocator's `init`/`deinit` take a bare `pubnub_platform_provider*`, while every other provider family's `init`/`deinit` take the `pubnub_provider_deps_t` bundle. The allocator is resolved and initialized before that bundle exists (the bundle's own `allocator` field would otherwise be self-referential). Copying the shape of a transport or serialization provider onto a custom allocator produces this mismatch.

**Fix:** Write the allocator's `init`/`deinit` against `pubnub_platform_provider*`. See [Providers — Allocator](https://www.pubnub.com/docs/sdks/c/api-reference/providers.md#allocator) for the full vtable and a complete sample.

### Compile-time tunable below its floor

```text
#error "PUBNUB_CFG_RESPONSE_BUFFER_SIZE must be >= 256"
```

The generated config header enforces over 20 such floors, covering I/O buffer sizes, concurrency and count limits, timing values, HTTP shape limits, and memory sizes. Most follow the same shape: `#error "PUBNUB_CFG_<NAME> must be >= N"`, or a comparison against another tunable. One is a hard equality rather than a floor: `PUBNUB_CFG_MAX_LOG_MESSAGE_SIZE` must be exactly `0` (disabled) or `>= 64`.

**Cause:** A `PUBNUB_CFG_*` tunable was set below the hard minimum the SDK's own code requires to stay memory-safe. This check runs in the generated header itself, so it also catches a floor violation introduced by hand-editing the cache or by a non-CMake build system.

**Fix:** Raise the tunable to at least the floor the `#error` text names. See [Compile-time tunables](https://www.pubnub.com/docs/sdks/c/environment-setup.md#compile-time-tunables-pubnub_cfg_) for the representative subset and their defaults.

## Linking and packaging

### find_package(pubnub) not found

```bash
find_package(pubnub REQUIRED)
```

```text
Could not find a package configuration file provided by "pubnub"
```

This exact text is CMake's own generic message for a missing package config file, not something this SDK's source produces.

**Cause:** `CMakeLists.txt` does have an `install(TARGETS pubnub ...)` rule, so `cmake --install` does copy the built library and headers to a prefix. But there is no `install(EXPORT ...)`, no `configure_package_config_file()`, and no generated `pubnubConfig.cmake` anywhere in the build — the pieces `find_package()` actually needs to locate and import the target. Installing the SDK doesn't make it `find_package()`-able.

**Fix:** Vendor the source tree and use `add_subdirectory()` instead.

```cmake
add_subdirectory(third_party/pubnub-c)     # path to your copy of the SDK repo
add_executable(my_app main.c)
target_link_libraries(my_app PRIVATE pubnub)
```

See [Consuming the SDK from your own project](https://www.pubnub.com/docs/sdks/c/environment-setup.md#consuming-the-sdk-from-your-own-project).

## Runtime errors

### PUBNUB_ERR_PROVIDER_MISSING from a custom provider

A publish, subscribe, history, or App Context call returns `PUBNUB_ERR_PROVIDER_MISSING` even though context creation itself succeeded.

**Cause:** One of the four validated provider families (allocator, transport, serialization, platform) is missing a mandatory vtable member at the specific call site that needed it. This is proven only for those four families — it is not a general contract for the crypto or logger vtables, neither of which rejects a missing member. Do not rely on `PUBNUB_ERR_PROVIDER_MISSING` to catch an incomplete crypto or logger provider.

**Fix:** Check which mandatory member your custom provider omitted for the family the failing call uses. See [Providers — How provider resolution and validation works](https://www.pubnub.com/docs/sdks/c/api-reference/providers.md#how-provider-resolution-and-validation-works) for the mandatory-member table and the crypto/logger caveat in full.

### PUBNUB_ERR_NOT_SUPPORTED from sending a file by path

**Cause:** The platform provider's `file_load` member is `NULL` for this build. Either a custom platform provider was supplied without it, or `PUBNUB_ENABLE_FILESYSTEM` is off — which gates `file_load` identically on the POSIX, Windows, and FreeRTOS providers, not just on FreeRTOS.

**Fix:** Supply `data`/`data_len` instead of `file_path`, or turn `PUBNUB_ENABLE_FILESYSTEM` on and ensure the platform provider's `file_load` is wired. See [File Sharing](https://www.pubnub.com/docs/sdks/c/api-reference/files.md).

### FreeRTOS platform provider outside a real FreeRTOS build

On a non-FreeRTOS host build with `PUBNUB_PROVIDER_PLATFORM=freertos`, every call fails with `PUBNUB_ERR_PROVIDER_MISSING` at `pubnub_create()`/`pubnub_init()`.

**Cause:** Outside a real FreeRTOS target, the FreeRTOS platform provider resolves to an all-zero stub — every function pointer `NULL` — because the real implementation is conditionally compiled for FreeRTOS only.

**Fix:** Don't select `PUBNUB_PROVIDER_PLATFORM=freertos` on a hosted build. Use `posix` or `windows`, or actually cross-compile for FreeRTOS.

### Cross-compiling the embedded profile without a toolchain file

A cross-compiled `embedded`-profile build either fails at the link step with host-architecture errors, or silently produces a host binary instead of a target binary.

**Cause:** The `embedded` preset doesn't set `CMAKE_TOOLCHAIN_FILE` itself, and no toolchain file ships in the repository. The preset only fixes profile, feature set, and build type — not the actual cross-compiler.

**Fix:** Pass your own toolchain file explicitly.

```bash
cmake --preset embedded -DCMAKE_TOOLCHAIN_FILE=/path/to/your-target.cmake
```

### Proxy type AUTO rejected on the curl transport

**Cause:** The default `curl` transport's proxy-type switch has no `AUTO` case; it falls through to a default failure, despite the field's own doc comment describing WPAD/PAC discovery with a fallback to a direct connection.

**Fix:** Use `PUBNUB_PROXY_HTTP_CONNECT` or `PUBNUB_PROXY_SOCKS5` explicitly on the curl transport. See [Configuration — Proxy configuration](https://www.pubnub.com/docs/sdks/c/api-reference/configuration.md#proxy-configuration).

### Proxy Digest/NTLM credentials silently dropped

A proxied request fails with what looks like an auth failure at the proxy, even though `auth` is `PUBNUB_PROXY_AUTH_DIGEST` or `PUBNUB_PROXY_AUTH_NTLM` with a correct `username`/`password`.

**Cause:** Only `PUBNUB_PROXY_AUTH_BASIC` is wired on the curl transport. `DIGEST` and `NTLM` are accepted without error at configuration time, but the transport never sends the credentials for them. The request proceeds unauthenticated and then fails at the proxy.

**Fix:** Use `PUBNUB_PROXY_AUTH_BASIC`, or a proxy that doesn't require Digest or NTLM, on the curl transport.

## Silent misbehavior

No error, warning, or diagnostic exists for any of these. That's exactly why they belong on this page.

### Presence heartbeat silently disabled

Presence is on and the client subscribes, but other clients see this user time out after `presence_timeout` seconds, and `here_now` counts look stale.

**Cause:** `heartbeat_interval` is `0`, its default and what `pubnub_config_defaults()` produces. The SDK sends one heartbeat when the subscription joins, then starts no recurring timer. It doesn't derive an interval from `presence_timeout`.

**Fix:** Set `heartbeat_interval` to a non-zero number of seconds, shorter than `presence_timeout`. See [Presence — Heartbeat](https://www.pubnub.com/docs/sdks/c/api-reference/presence.md#heartbeat).

### custom_value double free in App Context

An App Context write (`pubnub_set_uuid_metadata()`, `pubnub_set_channel_metadata()`, a membership or member set with `custom_value`) crashes, corrupts heap metadata, or aborts on a double free, sometimes much later, especially on the arena allocator.

**Cause:** `custom_value`'s ownership transfers to the SDK on App Context calls — the opposite of `publish.h`'s and `signal.h`'s `message_value` and `presence.h`'s `state_value`, all of which the caller retains and must destroy itself. Calling `pubnub_json_destroy()` on a tree already handed to an App Context write function is a double free, and nothing catches it for you.

**Fix:** Never call `pubnub_json_destroy()` on a `custom_value` tree after passing it to an App Context write function. See the ownership warning at the top of [App Context](https://www.pubnub.com/docs/sdks/c/api-reference/app-context.md#custom-metadata).

### One message left after pubnub_delete_messages()

`pubnub_delete_messages()` appears to succeed, but one message at the range's tail is still present afterward.

**Cause:** `end` is exclusive for delete — the message at that timetoken is kept. This differs from `pubnub_fetch_messages()`, where `end` is inclusive.

**Fix:** Set `end` to one greater than the last message's own timetoken you want deleted. See [Storage & Playback — Delete messages](https://www.pubnub.com/docs/sdks/c/api-reference/storage-and-playback.md#delete-messages).

### Decryption failure returns ciphertext, not an error

A decrypted file, subscribe payload, or history message is silently wrong: still ciphertext, base64, or garbage, yet the call reported success.

**Cause:** Decryption failure on subscribe and history returns the still-encrypted payload unchanged, with no distinct status. File downloads set the result's `decrypted` flag to `0` rather than failing the operation.

**Fix:** Check the file result's `decrypted` flag explicitly. Never assume a successful transaction means successfully decrypted content. See [Encryption — Attach a crypto module to a context](https://www.pubnub.com/docs/sdks/c/api-reference/encryption.md#attach-a-crypto-module-to-a-context) and [File Sharing — Download file](https://www.pubnub.com/docs/sdks/c/api-reference/files.md#download-file).

### Custom crypto provider init/deinit never runs

A custom crypto provider's `init()`/`deinit()` never run, even though the provider is attached and used successfully — no crash, no error, just silently-uninitialized state the first time a crypto operation runs.

**Cause:** `init`/`deinit` on a crypto provider only run when the provider is wrapped in a `pubnub_crypto_module_t` and that module is attached through `cfg.crypto_module`. A raw `pubnub_crypto_provider_t` used any other way never gets `init`/`deinit` called by the SDK at all.

**Fix:** Attach your crypto provider to a `pubnub_crypto_module_t` — via `pubnub_crypto_module_create()` or a built-in factory function — and set `cfg.crypto_module` to it. If you genuinely need a raw crypto provider outside a module, initialize it yourself before use. See [Providers — Crypto (provider)](https://www.pubnub.com/docs/sdks/c/api-reference/providers.md#crypto-provider).

## Diagnostic checklist

When none of the above sections matches:

1. Enable `PUBNUB_LOG_LEVEL_TRACE` and capture the full log output.
2. Verify `subscribe_key` and `user_id` are non-`NULL` and non-empty.
3. Call `pubnub_future_status(fut)` and compare against `PUBNUB_ERR_*` constants before touching any result accessor.
4. Confirm `pubnub_future_release(fut)` is called on every code path, including error paths and cancellation paths.
5. On cooperative targets, confirm `pubnub_process(ctx)` is called in a loop.
6. For server errors, call `pubnub_response_status_code(fut)` for the HTTP status and `pubnub_response_error_message(fut)` for the server's error text.
7. Verify all required provider fields in `pubnub_config_t` are non-`NULL` when your build uses `PUBNUB_PROVIDER_<FAMILY>=custom`.
8. Check that the `PUBNUB_ENABLE_*` flags for the features you use are `ON` in your CMake configuration.

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

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