Skip to main content
The SDK keeps every config / secret / flag in an in-memory snapshot that is replaced atomically as a whole on each sync. Reads never block on the network; only the periodic background refresh does. The same cache semantics apply to both NexusClient (sync) and AsyncNexusClient (async) - the only difference is whether the background refresh runs in a daemon thread or an asyncio.Task.

When does the SDK hit the network

The atomic snapshot guarantee

The snapshot is replaced as one immutable record. Two reads (from any thread or any coroutine) either see the same old state or the same new state - never a mix. This means you can iterate get_all_configs() or get_all_flags() without worrying about an entry being added or removed mid-loop. The returned dict is also a defensive copy.

ETag / 304

Every successful sync stores the server’s ETag. The next sync sends If-None-Match: <etag>. If nothing has changed:
…the SDK resets its synced_at timestamp and serves the existing snapshot. No payload transferred.

Background sync errors

Background syncs never raise to the caller. If the network call fails:
  1. The error is logged via NexusLogger.error(...)
  2. The existing cache continues to be served
  3. The next read after the TTL elapses retries automatically

Cache lifetime

The cache lives as long as the NexusClient / AsyncNexusClient instance. Calling close() (or await close()) tears down the SSE thread/task, the polling timer, and (for async) closes the underlying httpx.AsyncClient. Typical use is one singleton client per process. For Django / Flask, the framework adapters call atexit.register(client.close) for you. For FastAPI, the lifespan adapter awaits close() on shutdown.

Diagnostics

If you wire up a NexusLogger, the debug level emits one line per sync:

Concurrency model

Sync client (NexusClient)

  • All reads are thread-safe - internally guarded by a single threading.Lock for the snapshot reference.
  • Background sync runs in a daemon thread - at most one at a time.
  • SSE runs in a separate daemon thread - started by connect_stream().

Async client (AsyncNexusClient)

  • All reads are non-async but safe to call from any coroutine.
  • Background sync runs as an asyncio.Task - at most one at a time, guarded by a flag.
  • SSE runs as a separate asyncio.Task - created via loop.create_task inside connect_stream().
In both cases, you don’t need to call close() explicitly if your process is exiting - the daemon thread / asyncio loop teardown will reclaim resources. But for clean shutdown (e.g. graceful SIGTERM handling), always call close().