# Limitations of Async Operations in FlexSearch: When to Use Synchronous Methods

> Explore FlexSearch async operation limitations: 250ms delays, non-deterministic order. Learn when to use sync methods for small datasets or critical sections.

- Repository: [Nextapps GmbH/flexsearch](https://github.com/nextapps-de/flexsearch)
- Tags: deep-dive
- Published: 2026-02-23

---

**Async operations in FlexSearch may delay execution by up to 250ms per cycle, complete in non-deterministic order, and are mandatory for Worker indexes, whereas synchronous methods should only be used for small dataset initialization or critical sections requiring immediate blocking execution.**

The `nextapps-de/flexsearch` library provides both synchronous and asynchronous APIs for indexing and searching documents. While the async methods prevent UI freezing via an internal Runtime Balancer, they introduce specific constraints regarding latency, execution order, and compatibility with Worker threads. Understanding these limitations of async operations in FlexSearch ensures you select the appropriate API for your performance and architectural requirements.

## Understanding FlexSearch Async vs Sync APIs

FlexSearch exposes paired methods for every core operation:

| Sync method | Async counterpart | Returns |
|-------------|-------------------|---------|
| `add()` | `addAsync()` | The added document |
| `append()` | `appendAsync()` | Same as sync |
| `update()` | `updateAsync()` | Same as sync |
| `remove()` | `removeAsync()` | Same as sync |
| `search()` | `searchAsync()` | Search results array |
| `searchCache()` | `searchCacheAsync()` | Cached results (if enabled) |

Unlike typical async implementations that offload work to background threads, FlexSearch async methods utilize an **Asynchronous Runtime Balancer** that yields control to the JavaScript event loop after configurable time slices. This design prevents long-running indexing operations from blocking the main thread while remaining single-threaded.

## How the Asynchronous Runtime Balancer Works

The balancer logic resides in [`src/async.js`](https://github.com/nextapps-de/flexsearch/blob/main/src/async.js) and governs when async methods yield execution.

### Priority-Based Timing Calculation

Each index instance accepts a `priority` option (default `4` as defined in [`src/index.js`](https://github.com/nextapps-de/flexsearch/blob/main/src/index.js) and [`src/type.js`](https://github.com/nextapps-de/flexsearch/blob/main/src/type.js)). The balancer calculates a target execution window using the formula:

```

target = priority × priority × 3 ms

```

As implemented in [`src/async.js`](https://github.com/nextapps-de/flexsearch/blob/main/src/async.js) lines 52-55, this means:
- **Priority 1**: ~3ms cycles (high-frequency UI updates)
- **Priority 4** (default): ~45ms cycles (balanced responsiveness)
- **Priority 9**: ~250ms cycles (maximum throughput, noticeable latency)

### The Cycle Mechanism

When an async method executes, FlexSearch records the current timestamp. If the elapsed time exceeds the calculated target, the balancer **cycles** the operation:

1. The current execution context is postponed via `setTimeout(fn, 0)` (see [`src/async.js`](https://github.com/nextapps-de/flexsearch/blob/main/src/async.js) lines 58-66)
2. Control returns to the event loop immediately
3. The operation resumes in the next macrotask

If the duration remains below the target, the method executes immediately and returns a resolved `Promise` (lines 69-71).

## Limitations of Async Operations in FlexSearch

While the Runtime Balancer prevents blocking, it introduces specific constraints that affect application architecture and performance.

### Execution Delay and Latency

Each async operation may be deferred by up to the calculated target duration (maximum ~250ms at priority 9). For real-time applications requiring immediate consistency—such as search-as-you-type with zero-lag suggestions—this delay can introduce perceptible latency. According to [`doc/async.md`](https://github.com/nextapps-de/flexsearch/blob/main/doc/async.md), priority settings trade responsiveness for throughput, but even priority 1 introduces a minimum 3ms deferral potential.

### Non-Deterministic Completion Order

Async tasks run **concurrently** unless explicitly awaited. Firing multiple `addAsync` calls without `await` or `Promise.all` results in non-deterministic completion ordering, as each operation may cycle independently based on its execution duration. This creates race conditions where subsequent `searchAsync` calls may return partially populated indexes, producing inconsistent result sets.

### Lack of Automatic Batching

The Runtime Balancer yields the event loop but does **not** combine multiple operations into batches. Each `addAsync` or `searchAsync` call incurs individual Promise overhead and potential cycling logic. For bulk operations, manual batching using `for…await` loops or `Promise.all` is required to minimize context switches, as noted in the test suite ([`test/async.js`](https://github.com/nextapps-de/flexsearch/blob/main/test/async.js)).

### Worker and Persistent Store Constraints

When using `Worker` indexes or persistent storage via `db.mount()`, **all methods are always async**. The [`src/worker.js`](https://github.com/nextapps-de/flexsearch/blob/main/src/worker.js) implementation and [`doc/worker.md`](https://github.com/nextapps-de/flexsearch/blob/main/doc/worker.md) documentation confirm that sync APIs are unavailable in these contexts because operations must cross thread boundaries or I/O boundaries. Attempting to call `add()` on a Worker index will not execute synchronously and may throw errors or return undefined behavior.

### Callback and Promise Interference

FlexSearch allows passing a callback as the final argument to async methods while still returning a `Promise`. Mixing these patterns—using both the callback and `.then()`—can result in double-handling of results or swallowed errors if the callback throws but the Promise resolves, as implied by the dual interface in [`src/async.js`](https://github.com/nextapps-de/flexsearch/blob/main/src/async.js).

## When to Use Synchronous Methods in FlexSearch

Synchronous methods should be reserved for specific architectural constraints where blocking behavior is acceptable or required.

- **Initialization and one-off setup**: Building a small index during application startup where the data volume is negligible and blocking won't affect user experience.
- **Critical sections requiring immediate return**: Inside constructors or legacy codebases where `await` is syntactically unavailable and the return value must be available for the next line of execution.
- **Performance-critical micro-benchmarks**: Tight loops where Promise allocation overhead and balancer bookkeeping would skew measurements or consume unnecessary memory.
- **Synchronous dependency chains**: When integrating with synchronous third-party APIs that expect immediate results and cannot be refactored to async/await.

**Warning**: Synchronous methods execute on the main thread without yielding. For large datasets or complex search queries, this will freeze the UI in browsers and stall the event loop in Node.js.

## Code Examples

### Async Indexing with High Priority

For applications requiring near-real-time responsiveness, set `priority` to 2 (approximately 4ms cycles) and await each operation to ensure ordering:

```javascript
import FlexSearch from "flexsearch";

const index = new FlexSearch.Index({
  encode: "icase",
  tokenize: "forward",
  priority: 2  // ~4ms cycles for high-fps UI
});

async function populate(items) {
  // Await each call to maintain deterministic order
  for (const item of items) {
    await index.addAsync(item.id, item.text);
  }
}

```

### Parallel Search with Resolver

Use the Resolver for concurrent searching across multiple indexes with async balancing:

```javascript
import FlexSearch from "flexsearch";

const index = new FlexSearch.Index();
await index.addAsync(1, "hello world");
await index.addAsync(2, "flexsearch async");

const resolver = new FlexSearch.Resolver({
  async: true  // Enable parallel async processing
});

resolver.add(index);
const results = await resolver.search("flex");
console.log(results);  // [[2]]

```

Reference: [`doc/resolver.md`](https://github.com/nextapps-de/flexsearch/blob/main/doc/resolver.md) lines 351-404 describe the async resolver configuration.

### Synchronous Setup for Small Datasets

For trivial data volumes where blocking is acceptable:

```javascript
import FlexSearch from "flexsearch";

const index = new FlexSearch.Index({ encode: "icase" });

// Immediate execution, returns undefined or document
index.add(1, "short text");
index.add(2, "another example");

// Immediate search results
const hits = index.search("short");
console.log(hits);  // [1]

```

Use this pattern only when the dataset is small and the execution context permits blocking.

## Summary

- **Async methods** in FlexSearch use a Runtime Balancer that yields the event loop based on a `priority` calculation (priority² × 3ms), preventing UI freezing but introducing potential delays up to 250ms.
- **Key limitations** include non-deterministic completion order when not awaited, lack of automatic batching, mandatory async-only behavior in Worker and persistent storage contexts ([`src/worker.js`](https://github.com/nextapps-de/flexsearch/blob/main/src/worker.js)), and the inability to use sync methods across thread boundaries.
- **Sync methods** should be reserved for small dataset initialization, critical sections requiring immediate return values, or performance-critical loops where Promise overhead is unacceptable, understanding that they block the main thread entirely.
- The **priority option** (range 1-9, default 4) tunes the trade-off between responsiveness and throughput but only affects async operations, as documented in [`doc/async.md`](https://github.com/nextapps-de/flexsearch/blob/main/doc/async.md) and implemented in [`src/async.js`](https://github.com/nextapps-de/flexsearch/blob/main/src/async.js).

## Frequently Asked Questions

### What is the default priority setting in FlexSearch and how does it affect performance?

The default `priority` value is **4**, defined in [`src/index.js`](https://github.com/nextapps-de/flexsearch/blob/main/src/index.js) and [`src/type.js`](https://github.com/nextapps-de/flexsearch/blob/main/src/type.js). This calculates to approximately **45 milliseconds** per cycle (4 × 4 × 3ms). At this setting, async operations yield the event loop every 45ms of execution time, providing a balance between throughput and UI responsiveness. Lower values (1-2) reduce latency to ~3-12ms for high-frequency animations but increase context-switching overhead, while higher values (8-9) maximize throughput at ~192-250ms per cycle but risk perceptible UI freezing.

### Can I use synchronous methods with FlexSearch Worker indexes?

**No.** When using `Worker` indexes or persistent storage via `db.mount()`, all public methods are **exclusively asynchronous**. According to [`src/worker.js`](https://github.com/nextapps-de/flexsearch/blob/main/src/worker.js) and documented in [`doc/worker.md`](https://github.com/nextapps-de/flexsearch/blob/main/doc/worker.md), operations must cross thread boundaries or I/O boundaries, making synchronous execution impossible. Attempting to call `add()`, `search()`, or other sync methods on a Worker index will either throw errors or return undefined behavior. You must use `addAsync()`, `searchAsync()`, and other async variants with proper `await` or Promise handling.

### How does the async runtime balancer affect search consistency during bulk indexing?

The Runtime Balancer yields the JavaScript event loop based on elapsed time, not operation completion. If you fire multiple `addAsync` calls without awaiting each one (or using `Promise.all`), they execute **concurrently** and may complete in non-deterministic order depending on when each hits its priority-based cycle threshold. Consequently, issuing a `searchAsync` immediately after unawaited `addAsync` calls may return partially populated results, as the balancer might have yielded before all documents were processed. To ensure consistency, either `await` each indexing operation sequentially or batch them with `Promise.all` before executing searches.

### Should I use Promise.all with FlexSearch async operations to improve performance?

**Use `Promise.all` carefully.** While `Promise.all` allows concurrent execution of multiple async operations, FlexSearch's Runtime Balancer treats each call individually—it does **not** automatically batch operations. Each call still incurs its own Promise allocation and potential cycling overhead. For bulk indexing, `Promise.all` with `addAsync` can improve throughput compared to sequential awaits, but it risks non-deterministic completion order and higher memory usage from simultaneous Promises. For maximum control over the event loop yielding behavior, prefer `for…await` loops with individual awaits when order matters, or manually chunk your data into batches processed sequentially with `Promise.all` per chunk.