On this page

Troubleshooting the C SDK

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.

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 below, plus the runtime pubnub_res_t catalog described in Status Events.

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:

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

See Logging for the full level catalog and how to attach a custom logger.

Build failures

A feature function is undefined at compile time

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.

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 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:

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

Unknown build profile

cmake -B build -DPUBNUB_PROFILE=bare-metal
[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.

PUBNUB_ENABLE_PAM without PUBNUB_ENABLE_CRYPTO

cmake -B build -DPUBNUB_PROFILE=minimal -DPUBNUB_ENABLE_PAM=ON
[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.

Provider family left unset, or given an unrecognized name

cmake -B build -DPUBNUB_PROVIDER_TRANSPORT=libcurl
[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.

cJSON serialization with a non-stdlib allocator

cmake -B build -DPUBNUB_PROVIDER_SERIALIZATION=cjson -DPUBNUB_PROVIDER_ALLOCATOR=arena
[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

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

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 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 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:

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.

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:

/* 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:

-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 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 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:

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 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:

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:

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

To disable verification in development (never in production):

pubnub_set_tls_verify(ctx, 1); /* non-zero skip_verify = skip verification */
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:

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 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:

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:

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);
HTTPTypical causeAction
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:

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 for the full vtable contract and how this differs from monotonic_ms (boot-relative, unrelated to PAM).

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 for the full migration story between the two.

Encrypt buffer too small

Query the required output size before calling pubnub_crypto_module_encrypt_buf():

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 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:

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

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.

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

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:

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

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.

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:

StructWhat {0} silently disablesUse 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:

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:

/* 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 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

pubnub_publish(ctx, "my_channel", "{\"text\":\"hello\"}");
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 for the full field table and a complete sample.

Listener callback with the wrong signature

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

listener.on_status = on_status;
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:

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.

Custom allocator init/deinit signature mismatch

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 for the full vtable and a complete sample.

Compile-time tunable below its floor

#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 for the representative subset and their defaults.

Linking and packaging

find_package(pubnub) not found

find_package(pubnub REQUIRED)
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.

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.

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

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.

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.

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.

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.

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.

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 and File Sharing — 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).

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.
On this page