Skip to content

feat(feature-flags): Add an async feature flags runtime client - #766

Merged
stanleyphu merged 3 commits into
mainfrom
feat/feature-flags-runtime-async-client
Nov 5, 2026
Merged

stanleyphu merged 3 commits into
mainfrom
feat/feature-flags-runtime-async-client

Conversation

@stanleyphu

@stanleyphu stanleyphu commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Stack. This is #762, split into four PRs for review. Merge them in order; after each merge I'll rebase the next one onto main.

  1. chore(feature-flags): Parse and evaluate flag configurations locally #764 Configuration and evaluation
  2. feat(feature-flags): Evaluate feature flags locally with a polling runtime client #765 Sync runtime client
  3. feat(feature-flags): Add an async feature flags runtime client #766 Async runtime client ← this PR
  4. chore(feature-flags): Keep the runtime clients safe across fork() #767 Fork safety

Description

AsyncFeatureFlagsRuntimeClient, created from AsyncWorkOSClient, polls in an asyncio task. It has the same evaluation, retries, listeners and status as the sync client in #765.

flags = async_workos.create_feature_flags_runtime_client()
await flags.start()
await flags.wait_until_ready(timeout=5)
flags.is_enabled("new-checkout", {"organization": "org_123"})  # still synchronous
  • It polls through the AsyncWorkOSClient's own request() with max_retries: 0, so retries follow the runtime client's policy. asyncio.wait_for enforces request_timeout exactly.
  • Listeners may be plain functions or coroutines. Coroutines are awaited in order, each in a task of its own, so a CancelledError inside one only ends that listener and polling continues.
  • close() cancels the polling task, and any listener it's running, and waits for it to finish. Every caller waits, including concurrent ones. When close() is called from a listener running inside that task, it doesn't wait, because the task exits once the listener returns.

Where to look: _async.py mirrors _sync.py method for method, so reading them side by side is quickest. The real differences are _fetch and _refresh_locked (timeouts) and _drain, _deliver and _await_listener (running coroutine listeners).

How I checked it:

  • The full feature flags runtime lab passes against this branch: 87 of 87 checks. That's 47 for Python, including the 7 asyncio checks, and 40 for Node. The Python bridge suite passes 5 of 5.
  • nox -s ci passes locally: 3,715 tests.

Documentation

Does this require changes to the WorkOS Docs? E.g. the API Reference or code snippets need updates.

[x] Yes

The docs PR noted on #765 covers this client too.

🤖 Generated with Claude Code

@greptile-apps

greptile-apps Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no new actionable issues were found.

Summary

Adds AsyncFeatureFlagsRuntimeClient with background polling, synchronous flag evaluation, and coroutine listeners.

  • Async WorkOS apps can poll flags and check them locally.
  • Async listeners receive ordered updates without stopping polling.

Reviews (6) · Last reviewed commit: "fix(feature-flags): Make every async clo..." · Reviewed by Greptile

@stanleyphu

Copy link
Copy Markdown
Contributor Author

@greptileai review

@stanleyphu

Copy link
Copy Markdown
Contributor Author

@greptileai review

@stanleyphu

Copy link
Copy Markdown
Contributor Author

@greptileai review

@stanleyphu
stanleyphu force-pushed the feat/feature-flags-runtime-sync-client branch from 1980d27 to 3437e7d Compare November 4, 2026 23:43
@stanleyphu
stanleyphu force-pushed the feat/feature-flags-runtime-async-client branch from 73ce16b to 097d9bf Compare November 4, 2026 23:43
@stanleyphu

Copy link
Copy Markdown
Contributor Author

@greptileai review

Base automatically changed from feat/feature-flags-runtime-sync-client to main November 5, 2026 00:33
stanleyphu and others added 3 commits November 4, 2026 16:33
AsyncFeatureFlagsRuntimeClient, from AsyncWorkOSClient, polls in an asyncio
task with the same evaluation, retries, listeners and status as the sync
client. Evaluation stays synchronous, and listeners may be coroutines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…from within

A coroutine listener that awaited work something else cancelled raised
CancelledError, which skipped the remaining listeners and ended the
polling task for good. Each coroutine listener now runs in a task of its
own: its own cancellation is logged like a listener error, and cancelling
the delivering task, as close() does, still cancels the listener.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…to stop

A second close() returned as soon as it saw the client was closed, while
the first was still waiting for the polling task and a listener's cleanup,
so the second caller could close shared resources too early. Every caller
now waits for the polling task, which is cancelled only once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@stanleyphu
stanleyphu force-pushed the feat/feature-flags-runtime-async-client branch from 097d9bf to 28a35f6 Compare November 5, 2026 00:35
@stanleyphu
stanleyphu merged commit ed5c3a5 into main Nov 5, 2026
12 checks passed
@stanleyphu
stanleyphu deleted the feat/feature-flags-runtime-async-client branch November 5, 2026 00:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants