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

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 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 and src/type.js). The balancer calculates a target execution window using the formula:


target = priority × priority × 3 ms

As implemented in 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 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, 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).

Worker and Persistent Store Constraints

When using Worker indexes or persistent storage via db.mount(), all methods are always async. The src/worker.js implementation and 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.

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:

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:

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 lines 351-404 describe the async resolver configuration.

Synchronous Setup for Small Datasets

For trivial data volumes where blocking is acceptable:

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), 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 and implemented in 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 and 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 and documented in 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →