2.4.0 (2026-08-18)
Features
- Async sticky bucketing for
GrowthBookClient(#128, #129) by @madhuchavva:- New
AbstractAsyncStickyBucketServicebase class withasyncget_assignments/save_assignments; overrideget_all_assignmentsto batch all lookups for a user into one round trip (e.g. a single RedisMGET). Existing synchronousAbstractStickyBucketServiceimplementations keep working with both clients — the async client offloads their blocking calls to a thread pool. - Sticky bucket reads no longer block the event loop. Assignments are fetched per evaluation for the supplied
UserContext, matching the JavaScript SDK's multi-user client; concurrent evaluations for the same user share a single cancellation-safe in-flight lookup. - Sticky bucket writes are fire-and-forget: evaluation never waits on persistence. New assignments are immediately visible to later evaluations in the same process, and the client keeps an authoritative in-process copy of every document it has written, so a slow or stale store read can never roll back an assignment. Saves are serialized per document key so completion order cannot regress the stored document.
- New
GrowthBookClient.flush_sticky_bucket_saves()waits for all pending writes to persist (useful for serverless and short-lived processes);close()flushes automatically. - Optional bounded read cache for hot users via
Options(sticky_bucket_cache_ttl=..., sticky_bucket_cache_size=...); disabled by default.
- New
- Async user callbacks in
GrowthBookClient(#128, #129) by @madhuchavva:on_experiment_viewed,on_feature_usage, andsubscribe()callbacks may now be coroutines. They are scheduled on the event loop without blocking evaluation, and a tracking callback that raises is retried on the next evaluation of the same experiment/user pair. - Added
customFieldsproperty toExperiment(#125) by @vazarkevych
Performance Improvements
- Lock-free evaluation in
GrowthBookClient: feature updates now swap an immutable snapshot instead of taking a per-evaluation lock (#129) by @madhuchavva stop_refresh()no longer blocks the event loop during shutdown (#128) by @madhuchavva- In the included benchmark (
tests/scripts/benchmark_async_client.py: 100 concurrent requests, 1 ms simulated service latency), distinct-user throughput with a network-backed sticky bucket service goes from ~350 evaluations/second with multi-second event-loop stalls on 2.3.x to ~20,000 evaluations/second with sub-2 ms loop lag.
Bug Fixes
- Sticky bucket refresh no longer triggers a redundant feature reload per identifier attribute in the synchronous client (#124) by @vazarkevych
- The shared sticky bucket assignment-docs dict is now mutated in place instead of replaced when initially empty, preserving in-process read-your-writes (#128) by @madhuchavva
- The synchronous
GrowthBookclass now raisesValueErrorat construction if given an async sticky bucket service, instead of failing silently at runtime (#128) by @madhuchavva
Tests and CI
- Synced the conformance corpus with the JavaScript SDK 0.8.0 cases, with documented skips for unsupported contextual-bandit cases (#130) by @madhuchavva
- Consumer-facing typing is now checked in CI via a mypy probe; public callback annotations accept both plain functions and coroutines (#128) by @madhuchavva
- Replaced sleep-based concurrency tests with deterministic event-gated tests and added a high-concurrency benchmark harness for the async client (#128) by @madhuchavva
Compatibility notes
- Sticky bucket writes from
GrowthBookClientare now eventual rather than synchronous with evaluation. Read-your-writes is preserved in-process; short-lived processes shouldawait client.flush_sticky_bucket_saves()(orclose()) before exit to guarantee persistence. GrowthBookClientnow fetches sticky bucket assignments per evaluation instead of caching them for the lifetime of the process. This matches the JavaScript SDK and picks up cross-process assignment changes promptly, but increases service lookups; opt into bounded caching withsticky_bucket_cache_ttl/sticky_bucket_cache_sizeif needed.
Full Changelog: v2.3.1...v2.4.0