# Async vs Sync Client Implementations in OpenViking: Complete Technical Comparison

> Compare OpenViking sync vs async client implementations to understand differences in execution models, initialization, and concurrency capabilities. Optimize your application performance.

- Repository: [Volcengine/OpenViking](https://github.com/volcengine/OpenViking)
- Tags: deep-dive
- Published: 2026-03-08

---

**OpenViking provides `AsyncOpenViking` for non-blocking coroutines and `SyncOpenViking` for blocking synchronous operations, with both clients sharing the same underlying singleton instance and core functionality while differing in execution models, initialization patterns, and concurrency capabilities.**

The volcengine/OpenViking repository delivers dual API surfaces to accommodate diverse programming paradigms. Understanding the architectural distinctions between these async and sync client implementations enables developers to select the optimal interface for their specific concurrency requirements and application architecture.

## Programming Model and API Design

### Method Signatures and Execution

The fundamental distinction lies in how operations are exposed to callers. In [`openviking/async_client.py`](https://github.com/volcengine/OpenViking/blob/main/openviking/async_client.py), `AsyncOpenViking` declares all public methods using `async def`, requiring callers to `await` each operation. Conversely, [`openviking/sync_client.py`](https://github.com/volcengine/OpenViking/blob/main/openviking/sync_client.py) implements `SyncOpenViking` with ordinary blocking function signatures that internally delegate to the async client.

For example, while the async client defines `async def search(self, ...) -> List[...]`, the sync client defines `def search(self, ...) -> List[...]` and internally executes `return run_async(self._async_client.search(...))`. This wrapping strategy allows synchronous codebases to consume OpenViking functionality without introducing `async/await` syntax throughout the application.

### The Bridge: run_async Utility

The synchronous client relies on a bridging utility defined in [`openviking_cli/utils.py`](https://github.com/volcengine/OpenViking/blob/main/openviking_cli/utils.py). The `run_async` function accepts an awaitable, creates a temporary event loop, and runs the coroutine to completion before returning the result. This mechanism provides the blocking behavior that synchronous callers expect while leveraging the same underlying asynchronous implementation used by `AsyncOpenViking`.

## Initialization and Lifecycle Management

### Client Initialization Patterns

Both clients require explicit initialization before performing operations, but the invocation differs significantly. The async client requires `await client.initialize()`, which starts the underlying `LocalClient` from [`openviking/client.py`](https://github.com/volcengine/OpenViking/blob/main/openviking/client.py) and sets an internal `_initialized` flag upon completion.

The sync client offers `client.initialize()` as a blocking method that internally calls `run_async(self._async_client.initialize())`. While the async client tracks initialization state directly, the sync client maintains its own `_initialized` flag and only delegates to the async counterpart's initialization logic.

### Singleton Implementation and Thread Safety

`AsyncOpenViking` implements a true singleton pattern using a class-level `_instance` variable protected by `threading.Lock` in its `__new__` method. This ensures only one async client exists per process, preventing resource duplication.

`SyncOpenViking` does not implement its own singleton logic. Instead, its `__init__` method creates an `AsyncOpenViking` instance, meaning the sync client indirectly shares the async client's singleton. Consequently, all sync client instances across a process reference the same underlying async singleton, inheriting its thread-safety guarantees without additional locking.

### Reset and Cleanup Behavior

Resource cleanup follows the same async/sync dichotomy. `AsyncOpenViking.reset()` is an `async` class method that closes the singleton instance and clears the `_instance` variable. `SyncOpenViking.reset()` provides a blocking wrapper that invokes the async reset via `run_async`, ensuring synchronous callers can trigger cleanup without managing coroutines.

## Operational Differences

### Lazy Health Checks and Status Accessors

Three specific accessors exhibit different initialization behavior between the clients: `get_status()`, `is_healthy()`, and `observer()`.

In `AsyncOpenViking`, these methods are plain coroutines that forward directly to the async implementation. In `SyncOpenViking`, these methods first ensure the client is initialized by calling `initialize()` if the `_initialized` flag is false, then delegate to the async client. This lazy initialization pattern in the sync client prevents errors when status checks occur before explicit initialization.

### Error Handling and Exception Propagation

Exception propagation mirrors the execution model of each client. With `AsyncOpenViking`, errors propagate as regular async exceptions that callers catch using `try/except` blocks around awaited calls. `SyncOpenViking` surfaces identical errors as normal synchronous exceptions because `run_async` executes the full coroutine before returning control, translating async failures into standard Python exceptions suitable for synchronous error handling.

### Performance Characteristics

**AsyncOpenViking** suits high-concurrency workloads such as web servers handling many parallel requests, as the event loop can interleave I/O operations without blocking threads. **SyncOpenViking** provides simplicity for scripts, Jupyter notebooks, or environments where async/await introduces unnecessary complexity, though each call blocks the current thread until the underlying async operation completes.

## Code Examples: Async and Sync Usage

The following examples demonstrate identical workflows using both client types:

```python

# Async usage (requires an async context)

import asyncio
from openviking import AsyncOpenViking

async def async_demo():
    client = AsyncOpenViking(path="./data")
    await client.initialize()

    # Create a session and add a message

    session = await client.create_session()
    await client.add_message(session["session_id"], role="user", content="Hello Viking!")

    # Perform a semantic search

    results = await client.search("How to use OpenViking?")
    print(results)

    await client.close()

asyncio.run(async_demo())

```

```python

# Sync usage (no async/await needed)

from openviking import SyncOpenViking

def sync_demo():
    client = SyncOpenViking(path="./data")
    client.initialize()                     # blocks until ready

    # Create a session and add a message

    session = client.create_session()
    client.add_message(session["session_id"], role="user", content="Hello Viking!")

    # Perform a semantic search

    results = client.search("How to use OpenViking?")
    print(results)

    client.close()

sync_demo()

```

## Summary

- **Execution Model**: `AsyncOpenViking` exposes `async def` coroutines while `SyncOpenViking` provides blocking wrappers via `run_async` from [`openviking_cli/utils.py`](https://github.com/volcengine/OpenViking/blob/main/openviking_cli/utils.py).
- **Singleton Architecture**: The async client enforces a process-wide singleton using `threading.Lock` in [`openviking/async_client.py`](https://github.com/volcengine/OpenViking/blob/main/openviking/async_client.py); the sync client inherits this singleton by instantiating `AsyncOpenViking` internally.
- **Initialization Differences**: Async initialization requires `await`, while sync initialization blocks via `run_async`; sync client additionally performs lazy initialization for `get_status`, `is_healthy`, and `observer`.
- **Shared Core**: Both clients utilize `LocalClient` from [`openviking/client.py`](https://github.com/volcengine/OpenViking/blob/main/openviking/client.py) for storage-backed operations, ensuring functional parity regardless of API style.
- **Concurrency Suitability**: Choose `AsyncOpenViking` for high-throughput concurrent applications; use `SyncOpenViking` for straightforward, sequential execution contexts.

## Frequently Asked Questions

### Can I use both async and sync clients in the same application?

Yes. Since `SyncOpenViking` creates an `AsyncOpenViking` instance internally and the async client implements a true singleton pattern, both interfaces share the same underlying state and resources. You can mix both styles, though you should avoid calling sync methods from async contexts to prevent event loop blocking.

### Does the sync client create a separate connection pool?

No. The sync client does not implement independent connection management. It delegates all operations to the shared `AsyncOpenViking` singleton, which manages the connection pool through `LocalClient` in [`openviking/client.py`](https://github.com/volcengine/OpenViking/blob/main/openviking/client.py). Therefore, connection pools, sessions, and resources remain shared across both client types.

### Which client should I choose for high-throughput applications?

Select `AsyncOpenViking` for high-throughput or high-concurrency scenarios. The async implementation allows the event loop to interleave multiple I/O operations without blocking threads, maximizing resource utilization. `SyncOpenViking` blocks the calling thread per operation, making it unsuitable for concurrent request handling.

### How does error handling differ between the two clients?

`AsyncOpenViking` propagates exceptions as async exceptions that you catch within `try/except` blocks surrounding awaited calls. `SyncOpenViking` surfaces the same underlying errors as standard synchronous exceptions because `run_async` executes coroutines to completion before returning. The error types and messages remain identical; only the catching mechanism differs based on your code's sync or async context.