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 iterateget_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’sETag. The next sync sends If-None-Match: <etag>. If nothing has changed:
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:- The error is logged via
NexusLogger.error(...) - The existing cache continues to be served
- The next read after the TTL elapses retries automatically
Cache lifetime
The cache lives as long as theNexusClient / 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.Lockfor 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 vialoop.create_taskinsideconnect_stream().
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().