<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Async on MongrelDB</title><link>https://www.mongreldb.com/articles/tags/async/</link><description>Recent content in Async on MongrelDB</description><image><title>MongrelDB</title><url>https://www.mongreldb.com/assets/og-mongreldb.png</url><link>https://www.mongreldb.com/assets/og-mongreldb.png</link></image><generator>Hugo</generator><language>en-US</language><lastBuildDate>Wed, 30 Sep 2026 09:00:00 -0500</lastBuildDate><atom:link href="https://www.mongreldb.com/articles/tags/async/index.xml" rel="self" type="application/rss+xml"/><item><title>Non-Blocking Async I/O in TypeScript: putAsync, queryAsync, countAsync</title><link>https://www.mongreldb.com/articles/2026/09/non-blocking-async-io-in-typescript-putasync-queryasync-countasync/</link><pubDate>Wed, 30 Sep 2026 09:00:00 -0500</pubDate><guid>https://www.mongreldb.com/articles/2026/09/non-blocking-async-io-in-typescript-putasync-queryasync-countasync/</guid><description>A synchronous native database call that takes forty milliseconds stalls every request your Node process is serving, so MongrelDB Kit ships genuinely non-blocking async twins for the hot read and write paths, and this post covers which ones are real and which ones are not.</description><content:encoded><![CDATA[<p>Node gives you one thread to run JavaScript on, and everything your server is doing at any given moment, every in-flight HTTP request, every open WebSocket, every timer, shares that thread, so a single synchronous call into a native addon that takes forty milliseconds is not a forty-millisecond problem, it is a forty-millisecond freeze across the whole process. That distinction barely matters when the call is a point read that hits the page cache and comes back in four microseconds, but it matters enormously when the call is a condition query that has to walk runs on disk, or a put that lands right as the memtable decides to spill, because embedded databases do occasionally touch storage and storage occasionally takes real time. The classic answer in the Node ecosystem is libuv&rsquo;s thread pool, which is how <code>fs</code> and <code>crypto</code> hide their blocking work, and the napi-rs answer is <code>spawn_blocking</code>, which hands the call to a blocking thread and resolves a <code>Promise</code> when it finishes, and Kit&rsquo;s async surface is built on the second of those, wired straight into the same Rust core the sync API uses.</p>
<h2 id="which-calls-actually-leave-the-event-loop">Which calls actually leave the event loop</h2>
<p>The genuinely non-blocking calls are the async variants implemented as native <code>spawn_blocking</code> methods on the addon&rsquo;s <code>TableHandle</code>, and Kit re-exposes them on the database handle with the table name as the first argument, delegating straight through, so the Rust work runs on a blocking thread while your event loop keeps pumping:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">put</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">db</span>.<span style="color:#a6e22e">putAsync</span>(<span style="color:#e6db74">&#34;events&#34;</span>, <span style="color:#a6e22e">cells</span>);        <span style="color:#75715e">// Promise&lt;PutResult&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">row</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">db</span>.<span style="color:#a6e22e">getAsync</span>(<span style="color:#e6db74">&#34;events&#34;</span>, <span style="color:#a6e22e">rowId</span>);        <span style="color:#75715e">// Promise&lt;RowJs | null&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">rows</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">db</span>.<span style="color:#a6e22e">queryAsync</span>(<span style="color:#e6db74">&#34;events&#34;</span>, <span style="color:#a6e22e">conds</span>);     <span style="color:#75715e">// Promise&lt;RowJs[]&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">total</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">db</span>.<span style="color:#a6e22e">countAsync</span>(<span style="color:#e6db74">&#34;events&#34;</span>);           <span style="color:#75715e">// Promise&lt;bigint&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">n</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">db</span>.<span style="color:#a6e22e">countWhereAsync</span>(<span style="color:#e6db74">&#34;events&#34;</span>, <span style="color:#a6e22e">conds</span>);   <span style="color:#75715e">// Promise&lt;bigint&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">arrow</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">db</span>.<span style="color:#a6e22e">queryArrowAsync</span>(<span style="color:#e6db74">&#34;events&#34;</span>, <span style="color:#a6e22e">conds</span>); <span style="color:#75715e">// Promise&lt;Buffer&gt;, Arrow IPC
</span></span></span></code></pre></div><p>The conditions for <code>queryAsync</code> and <code>countWhereAsync</code> are the same <code>ConditionSpec</code> objects the sync query path takes, which means you can build them with the <code>ConditionKind</code> constants or let <code>compilePredicate</code> turn a Kit <code>Predicate</code> into them, and <code>setSimilarityAsync</code> rides on top of <code>queryAsync</code> for its MinHash candidate fetch, so at least the I/O half of the sparse-vector similarity path stays off the loop even though the scoring that follows runs in JavaScript. Because these return ordinary promises, the usual concurrency tricks apply, and firing a count and a query together genuinely overlaps them instead of serializing on the JS thread:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">const</span> [<span style="color:#a6e22e">rows</span>, <span style="color:#a6e22e">total</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">Promise</span>.<span style="color:#a6e22e">all</span>([
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">db</span>.<span style="color:#a6e22e">queryAsync</span>(<span style="color:#e6db74">&#34;events&#34;</span>, <span style="color:#a6e22e">conds</span>),
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">db</span>.<span style="color:#a6e22e">countWhereAsync</span>(<span style="color:#e6db74">&#34;events&#34;</span>, <span style="color:#a6e22e">conds</span>),
</span></span><span style="display:flex;"><span>]);
</span></span></code></pre></div><p>Transactions get the same treatment through <code>runTxn</code>, which is the async twin of <code>runSyncTxn</code> and commits through the native <code>Transaction.commitAsync()</code>, so the commit itself, fsync and all, happens on the blocking pool rather than freezing your request handlers while the WAL hits disk.</p>
<h2 id="why-the-whole-api-is-not-async">Why the whole API is not async</h2>
<p>Making every call async is the fashionable choice and it would have been the wrong one here, because most embedded database calls are measured in microseconds and the overhead of scheduling onto a thread pool, crossing the language boundary twice, and resolving a promise costs more than the work itself when the answer is already sitting in the page cache. We went through a version of this once before in a different life with PHP&rsquo;s <code>mysql_*</code> functions, which were synchronous end to end and nobody minded, because PHP ran process-per-request and a blocking call only ever stalled the one request that issued it; a single-threaded Node server inverts that assumption, one blocking call stalls every request at once, so the calls that can actually touch disk have to earn their async form while the cheap ones stay plain. So the sync API stays the default for the hot, cheap operations, the <code>Async</code> siblings exist for the operations that can block, and you pick per call site with full knowledge of which is which, which is a less tidy story than &ldquo;everything is a <code>Promise</code>&rdquo; but a faster program at the end of it.</p>
<h2 id="the-fine-print-because-there-is-always-fine-print">The fine print, because there is always fine print</h2>
<p>Two caveats matter and both are documented in the API reference, but they deserve repeating because they are exactly the kind of thing you discover in production otherwise. First, the maintenance twins are not all real: <code>compactAllAsync</code>, <code>compactTableAsync</code>, <code>approxAggregateAsync</code>, and <code>snapshotEpochAsync</code> have no native async variant, so they wrap the synchronous call in a <code>Promise</code> and still block the loop while they run, matching the signature without matching the behavior. Second, the Kit-level async methods on <code>TableHandle</code> are raw I/O paths that bypass Kit&rsquo;s constraint enforcement, meaning defaults, unique guards, and foreign-key checks do not fire on <code>putAsync</code>, which is the same contract the other high-throughput surfaces like <code>WriteBuffer</code> and <code>bulkLoadTyped</code> carry, so if you want constrained writes you use the transactional <code>insert*</code> builders and if you want speed you accept the trade with your eyes open.</p>
<h2 id="when-to-reach-for-which">When to reach for which</h2>
<p>The rule of thumb we use internally is simple enough to fit on an index card: request-scoped server code, where the process is juggling other work, defaults to the async variants for anything that queries or commits, and scripts, migrations, and CLI tooling, where one operation at a time is the whole point, stay synchronous because the promise machinery buys nothing there. The desktop app that ships MongrelDB embedded follows the same split, keeping its UI thread clear with the async paths during scans and compactions while its import wizards call the sync API in a straight line, and neither side had to change a schema, a file format, or a transaction to do it, because both surfaces are the same engine all the way down. That is the actual feature here, not that async exists, since every database client eventually grows async methods, but that async is a per-call choice on one code path rather than a second API you migrate to and then maintain forever.</p>
]]></content:encoded></item></channel></rss>