<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Boris Barac</title>
    <description>The latest articles on DEV Community by Boris Barac (@boris9027).</description>
    <link>https://dev.to/boris9027</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3981588%2Fd6d8c3d6-8d00-4ac6-922c-5c86ffb1ae7b.jpeg</url>
      <title>DEV Community: Boris Barac</title>
      <link>https://dev.to/boris9027</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/boris9027"/>
    <language>en</language>
    <item>
      <title>When JavaScript Isn't Fast Enough</title>
      <dc:creator>Boris Barac</dc:creator>
      <pubDate>Fri, 12 Jun 2026 16:41:52 +0000</pubDate>
      <link>https://dev.to/boris9027/when-javascript-isnt-fast-enough-3ia2</link>
      <guid>https://dev.to/boris9027/when-javascript-isnt-fast-enough-3ia2</guid>
      <description>&lt;p&gt;500 requests per second. One minute. An Express endpoint computing BTC technical indicators over 3.6 million data points.&lt;/p&gt;

&lt;p&gt;Node.js folded. Average latency: 3,318 ms. p95: 8.4 seconds. 15.5% of requests failed. The median response crawled in at 1.6 seconds. The JavaScript computation pipeline couldn't keep up.&lt;/p&gt;

&lt;p&gt;Same server. Same load. Same data. One change: the &lt;code&gt;/price-rust&lt;/code&gt; endpoint, backed by a Rust addon compiled to a &lt;code&gt;.node&lt;/code&gt; binary via &lt;a href="//NAPI.RS"&gt;NAPI.RS&lt;/a&gt;. Average latency: 660 ms. p95: 2 seconds. Zero errors. Peak RSS: 283 MB — &lt;em&gt;less&lt;/em&gt; memory than the JS path.&lt;/p&gt;

&lt;p&gt;Same Express process, same port, same payload. The only difference was who did the math.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the API BENCHMARK actually does
&lt;/h2&gt;

&lt;p&gt;The benchmark is a BTC price analysis endpoint. We synthetically generate 360 000 datapoints.&lt;br&gt;
On that dataset the server computes SMA (25/50/100/200-day windows), RSI (14-period), MACD (12/26/9), Bollinger Bands (20-period), and a composite trading signal built from golden/death crosses, RSI divergence, MACD crossovers, and Bollinger squeeze detection.&lt;/p&gt;

&lt;p&gt;Branching, windowing, stateful accumulation, multi-indicator correlation. The kind of workload that makes a runtime earn its keep — or not.&lt;/p&gt;
&lt;h2&gt;
  
  
  The full picture
&lt;/h2&gt;

&lt;p&gt;The HTTP benchmark shows what happens under real load. The function benchmark isolates raw computation from HTTP overhead, request queuing, and GC pressure.&lt;/p&gt;
&lt;h3&gt;
  
  
  HTTP — k6, 500 req/s, 1 minute
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Avg latency&lt;/th&gt;
&lt;th&gt;p95&lt;/th&gt;
&lt;th&gt;Throughput&lt;/th&gt;
&lt;th&gt;Errors&lt;/th&gt;
&lt;th&gt;Peak RSS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/price&lt;/code&gt; (JS)&lt;/td&gt;
&lt;td&gt;3,318 ms&lt;/td&gt;
&lt;td&gt;8,437 ms&lt;/td&gt;
&lt;td&gt;62.4 rps&lt;/td&gt;
&lt;td&gt;15.5%&lt;/td&gt;
&lt;td&gt;292 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/price-rust&lt;/code&gt; (N-API)&lt;/td&gt;
&lt;td&gt;660 ms&lt;/td&gt;
&lt;td&gt;2,061 ms&lt;/td&gt;
&lt;td&gt;35.6 rps&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;283 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bun&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/price&lt;/code&gt; (JS)&lt;/td&gt;
&lt;td&gt;1,845 ms&lt;/td&gt;
&lt;td&gt;5,146 ms&lt;/td&gt;
&lt;td&gt;33.4 rps&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;470 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bun&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/price-rust&lt;/code&gt; (N-API)&lt;/td&gt;
&lt;td&gt;642 ms&lt;/td&gt;
&lt;td&gt;2,412 ms&lt;/td&gt;
&lt;td&gt;32.1 rps&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;259 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Node.js on JS is the only combination that failed requests. Every other configuration held — including Bun running the exact same JavaScript. Bun's JS path is 1.8x faster than Node's and dropped zero requests.&lt;/p&gt;

&lt;p&gt;The Rust paths are nearly identical across runtimes: 660 ms on Node, 642 ms on Bun. The runtime barely matters when Rust is doing the work.&lt;/p&gt;
&lt;h3&gt;
  
  
  Function — mitata, 366K data points
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variant&lt;/th&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;ops/sec&lt;/th&gt;
&lt;th&gt;Avg (ms)&lt;/th&gt;
&lt;th&gt;Speedup vs JS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JS&lt;/td&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;49.11&lt;/td&gt;
&lt;td&gt;1x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native (N-API)&lt;/td&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;td&gt;15.95&lt;/td&gt;
&lt;td&gt;3.1x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JS&lt;/td&gt;
&lt;td&gt;Bun&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;36.10&lt;/td&gt;
&lt;td&gt;1x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native (N-API)&lt;/td&gt;
&lt;td&gt;Bun&lt;/td&gt;
&lt;td&gt;69&lt;/td&gt;
&lt;td&gt;14.51&lt;/td&gt;
&lt;td&gt;2.5x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JS&lt;/td&gt;
&lt;td&gt;Browser (Chromium)&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;38.04&lt;/td&gt;
&lt;td&gt;1x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native (WASM)&lt;/td&gt;
&lt;td&gt;Browser (Chromium)&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;17.71&lt;/td&gt;
&lt;td&gt;2.2x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three runtimes, three JS baselines, all within a narrow band: 20–28 ops/sec. The Rust speedup is consistent — ~3x on Node, ~2.5x on Bun, ~2.2x on WASM.&lt;/p&gt;

&lt;p&gt;WASM in the browser hits 56 ops/sec — closer to native N-API (63–69) than to any JS baseline. That's worth sitting with for a moment.&lt;/p&gt;

&lt;p&gt;Sync vs async N-API made no measurable difference (63 vs 63 on Node, 69 vs 68 on Bun). The async variant offloads to a Tokio thread pool via &lt;code&gt;spawn_blocking&lt;/code&gt;, but the bottleneck is the math, not the calling convention.&lt;/p&gt;
&lt;h2&gt;
  
  
  How a Rust function becomes a Node endpoint
&lt;/h2&gt;

&lt;p&gt;The binding layer is thinner than you'd expect. napi-rs is the bridge: annotate a Rust function with &lt;code&gt;#[napi]&lt;/code&gt;, compile to a &lt;code&gt;.node&lt;/code&gt; binary, and Node can &lt;code&gt;require()&lt;/code&gt; it like any other module.&lt;/p&gt;

&lt;p&gt;Here's the JS moving average function — the one that runs in every request to &lt;code&gt;/price&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculateMovingAverages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;smaWindows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cutoffYears&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cutoffIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;cutoffYears&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;movingAverages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runningSums&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;smaWindows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;wi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;wi&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;smaWindows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;wi&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;smaWindows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;wi&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
      &lt;span class="nx"&gt;runningSums&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;wi&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;runningSums&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;wi&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nx"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;cutoffIndex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// ... build entry with date, price, SMA values&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;movingAverages&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's the Rust equivalent, exposed to Node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[napi]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;calculate_moving_averages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Float64Array&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sma_windows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cutoff_years&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Buffer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;cutoff_years&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cutoff_years&lt;/span&gt;&lt;span class="nf"&gt;.unwrap_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;dates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;utils&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;precompute_dates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calc_ma&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;sma_windows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cutoff_years&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;dates&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nn"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;serde_json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;to_vec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;#[napi]&lt;/code&gt; macro generates the C FFI glue Node's N-API runtime expects. It handles type conversion between V8 values and Rust types, and registers the function as a module export.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Float64Array&lt;/code&gt; comes in as a borrowed &lt;code&gt;&amp;amp;[f64]&lt;/code&gt; slice with no copy. &lt;code&gt;Buffer&lt;/code&gt; goes out as raw bytes, skipping V8's UTF-16 string conversion entirely.&lt;/p&gt;

&lt;p&gt;On the JS side, loading the addon is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;native&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./napibench-native.node&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;native.calculateMovingAverages(prices, [25, 50, 100, 200])&lt;/code&gt; calls directly into compiled Rust. No HTTP, no IPC, no child process. Same call stack, same thread.&lt;/p&gt;

&lt;p&gt;Need async? The async variant parks the work on a Tokio thread pool and returns a Promise. Same function, different concurrency model.&lt;/p&gt;

&lt;p&gt;The server wires it into Express the same way the JS endpoint is wired. Same &lt;code&gt;req → compute → res&lt;/code&gt; flow. The handler just calls a different function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same Rust, different planet
&lt;/h2&gt;

&lt;p&gt;The Cargo.toml has a feature gate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[features]&lt;/span&gt;
&lt;span class="py"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"napi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"napi-derive"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"rayon"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"napi-build"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;wasm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"wasm-bindgen"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"js-sys"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build with the default features, you get &lt;code&gt;napi_impl.rs&lt;/code&gt; — N-API bindings, rayon parallelism, tokio async. Build with &lt;code&gt;--no-default-features --features wasm&lt;/code&gt;, you get &lt;code&gt;wasm.rs&lt;/code&gt; — the same indicator logic compiled to WebAssembly via &lt;code&gt;wasm-bindgen&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The WASM build strips away some optimizations. No rayon, no async thread pool, no Buffer returns — the WASM function returns a JSON string. It's the simpler, less-tuned version of the Rust code.&lt;/p&gt;

&lt;p&gt;And it still hits 56 ops/sec in headless Chromium. That's 2.2x faster than Chromium's own JS engine running the same algorithms, and it's closer to native N-API performance (63–69 ops/sec) than to any JS baseline (20–28 ops/sec).&lt;/p&gt;

&lt;p&gt;The bulk of the speedup comes from Rust's compiler — LLVM optimizing tight loops, stack-allocated structs, no GC pauses, no hidden type checks — not from rayon or async thread pools. Those help, but the floor is already high.&lt;/p&gt;

&lt;p&gt;The browser benchmark ran in headless Chromium via Playwright: the WASM module and JS indicator code, same 366K-data-point pipeline, same 5-second window. The WASM binary is produced by &lt;code&gt;wasm-pack build --target web&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Because napi-rs makes Rust functions callable like JS functions, the existing JS test suite doubles as a parity test for Rust. The test imports the JS indicator functions alongside the native addon, runs the same input through both, and asserts the outputs match field by field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jsMa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateMovingAverages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pricesJs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rustMa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;native&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;calculateMovingAverages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rustMa&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jsMa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;jsMa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rustMa&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jsMa&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JS implementation becomes the test oracle for the Rust implementation. The test runner (vitest) doesn't know or care that one side is native.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to reach for Rust
&lt;/h2&gt;

&lt;p&gt;Three runtimes. Two languages. One workload. Here's what the numbers actually say.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reach for Rust when you're CPU-bound.&lt;/strong&gt; This workload is arithmetic-heavy, loop-heavy, and largely sequential — the kind of thing V8 and JavaScriptCore optimize well but can't match against LLVM.&lt;/p&gt;

&lt;p&gt;The 2.2–3x speedup appeared across every runtime, calling convention, and optimization level. If your server spends most of its time computing — parsing, transforming, aggregating, encrypting — a native addon pays for itself.&lt;/p&gt;

&lt;p&gt;But N-API has a floor. The FFI boundary between V8 and native code adds fixed overhead per call. On small datasets — a few hundred data points — that overhead erases the compute savings. Node/Bun JS outperforms Node/Bun + Rust N-API at that scale. The native path only wins once the dataset is large enough to amortize the crossing cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't reach for Rust for I/O.&lt;/strong&gt; The 3x raw compute speedup became a 5x latency reduction under HTTP load because the JS endpoint was past its breaking point. But the Rust endpoints on Node and Bun had nearly identical latencies (660 ms vs 642 ms).&lt;/p&gt;

&lt;p&gt;The runtime's HTTP stack, event loop, and memory management dominate when computation is fast. Rust made the compute a non-factor; then the runtime didn't matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bun is a cheaper upgrade than Rust.&lt;/strong&gt; If you're on Node.js and your JS code is too slow, switching to Bun gives you a 40% speedup with zero code changes. Same JavaScript, same endpoints, different binary.&lt;/p&gt;

&lt;p&gt;In this benchmark, Bun's JS path avoided errors entirely where Node.js failed 15.5% of requests. That might be enough. Rust is the bigger hammer, but Bun is the one you don't have to think about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WASM is viable.&lt;/strong&gt; Not a curiosity — genuinely competitive. 56 ops/sec in a browser tab, 2.2x faster than the browser's own JS, built from the same Rust codebase with a feature flag.&lt;/p&gt;

&lt;p&gt;Unlike NODE/BUN, WASM's speedup holds even at small workloads. The calling convention is lighter — no FFI boundary to cross, the module runs inside the same engine. Where N-API needs a large dataset to amortize its overhead, WASM delivers from the start.&lt;/p&gt;

&lt;p&gt;If you're running heavy computation in the browser — image processing, data analysis, simulations — WASM is the answer, and you don't need a separate codebase to get there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The binding layer is not the bottleneck.&lt;/strong&gt; napi-rs adds almost nothing to call overhead. Sync and async variants performed identically. The FFI boundary — V8 to Rust and back — is cheap enough that it doesn't appear in the numbers.&lt;/p&gt;

&lt;p&gt;What matters is what happens on the other side of that boundary.&lt;/p&gt;

&lt;p&gt;#rust #nodejs #bun #napi #webassembly #performance #benchmark&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>rust</category>
      <category>api</category>
      <category>benchmark</category>
    </item>
    <item>
      <title>You Might Not Need Docker: The Case for Lightweight, Headless VMs</title>
      <dc:creator>Boris Barac</dc:creator>
      <pubDate>Fri, 12 Jun 2026 16:38:04 +0000</pubDate>
      <link>https://dev.to/boris9027/you-might-not-need-docker-the-case-for-lightweight-headless-vms-2e9p</link>
      <guid>https://dev.to/boris9027/you-might-not-need-docker-the-case-for-lightweight-headless-vms-2e9p</guid>
      <description>&lt;p&gt;Lately, whenever I need to run a quick experiment or spin up a localized environment with multiple moving pieces, I've completely stopped using Docker. Instead, I've been running full, headless Ubuntu virtual machines directly from my host terminal using Canonical's Multipass. And I love it.&lt;/p&gt;

&lt;p&gt;It is using your native hypervisor (KVM on Linux, Hyper-V on Windows, or QEMU on macOS), it drops a clean Ubuntu environment onto your laptop with next to zero performance overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Zero-Friction Sandbox
&lt;/h2&gt;

&lt;p&gt;Instead of wrestling with installation mirrors and configuration screens, Multipass treats virtual machines as disposable command-line primitives.&lt;/p&gt;

&lt;p&gt;To spin up a basic machine (we'll name it &lt;code&gt;devbox&lt;/code&gt;) and jump inside, the standard baseline looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Spin up a fresh Ubuntu instance instantly&lt;/span&gt;
multipass launch &lt;span class="nt"&gt;--name&lt;/span&gt; devbox

&lt;span class="c"&gt;# Drop straight into the VM's interactive bash shell&lt;/span&gt;
multipass shell devbox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running Headless Commands Directly
&lt;/h2&gt;

&lt;p&gt;You don't actually have to open an interactive shell session just to execute a single task. If you want to fire off commands and capture the output straight back to your host terminal, you can leverage &lt;code&gt;exec&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check the VM's kernel version without entering the shell&lt;/span&gt;
multipass &lt;span class="nb"&gt;exec &lt;/span&gt;devbox &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt;

&lt;span class="c"&gt;# Remotely trigger an update and install packages&lt;/span&gt;
multipass &lt;span class="nb"&gt;exec &lt;/span&gt;devbox &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
multipass &lt;span class="nb"&gt;exec &lt;/span&gt;devbox &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nginx

&lt;span class="c"&gt;# this is usually needed to run custom tools out of the shell&lt;/span&gt;
multipass &lt;span class="nb"&gt;exec &lt;/span&gt;devbox &lt;span class="nt"&gt;--&lt;/span&gt; bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"source ~/.profile &amp;amp;&amp;amp; command"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Moving Files Between Host and VM
&lt;/h2&gt;

&lt;p&gt;When you are experimenting with local scripts or configuration files, moving data in and out of the isolated environment shouldn't require setting up network shares or SSH keys. Multipass handles this natively with a built-in transfer primitive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Copy a local configuration script from your host into the VM&lt;/span&gt;
multipass transfer ./setup.sh devbox:/home/ubuntu/setup.sh

&lt;span class="c"&gt;# Pull a generated log file or benchmark report back out to your host&lt;/span&gt;
multipass transfer devbox:/home/ubuntu/output.log ./output.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a testing session gets messy or you corrupt a system-level networking rule, you don't spend hours debugging. You simply nuke the instance and wipe the slate clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Obliterate the broken VM and purge its allocated storage&lt;/span&gt;
multipass delete devbox
multipass purge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Infrastructure as Code: Local Automation via Cloud-Init
&lt;/h2&gt;

&lt;p&gt;For more advanced experiments requiring multiple pre-installed binaries or specific user permissions, you can bypass manual setup entirely using &lt;strong&gt;cloud-init&lt;/strong&gt;. This is the exact same industry-standard engine used by major cloud providers to bootstrap instances at launch.&lt;/p&gt;

&lt;p&gt;If we need a repeatable baseline containing Redis and Git, we can define the required environment state inside a standard &lt;code&gt;cloud-config.yaml&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;#cloud-config&lt;/span&gt;
&lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;devuser&lt;/span&gt;
    &lt;span class="na"&gt;sudo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ALL=(ALL) NOPASSWD:ALL&lt;/span&gt;
    &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/bin/bash&lt;/span&gt;
&lt;span class="na"&gt;packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis-server&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;git&lt;/span&gt;
&lt;span class="na"&gt;runcmd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;systemctl enable redis-server&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;systemctl start redis-server&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To parse this configuration block directly during the VM's hardware initialization phase, pass the file using the &lt;code&gt;--cloud-init&lt;/code&gt; flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Launch a fully customized, automated instance using the configuration file&lt;/span&gt;
multipass launch &lt;span class="nt"&gt;--name&lt;/span&gt; cache-layer &lt;span class="nt"&gt;--cloud-init&lt;/span&gt; cloud-config.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of spending ten minutes manually running configuration commands every time you need a new sandbox, dropping this YAML into a directory gives you an identical baseline whenever you need it. If the configuration breaks or the environment gets contaminated, you just destroy the VM and re-run the exact same launch command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Breakdown: Multipass vs. Docker
&lt;/h2&gt;

&lt;p&gt;A common question that comes up is why use Multipass when you could just run a Docker container. The core distinction lies entirely in where the isolation boundaries are drawn across the system stack.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-----------------------------------+     +-----------------------------------+
|          DOCKER ARCHITECTURE      |     |       MULTIPASS ARCHITECTURE      |
|                                   |     |                                   |
|  [App A]   [App B]   (Containers) |     |  [Systemd] [Kernel]  (Full VM)    |
|  +-----------------------------+  |     |  +-----------------------------+  |
|  |    Shared Host Kernel       |  |     |  |     Native Hypervisor       |  |
+--+-----------------------------+--+     +--+-----------------------------+--+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker isolates user-space processes. The containers sit directly on top of your host machine's operating system kernel. This makes them incredibly fast and resource-efficient for deploying standardized applications or microservices, but they don't have true system autonomy.&lt;/p&gt;

&lt;p&gt;Multipass isolates the entire operating system block. It boots a completely independent Linux kernel, initializes a standalone virtual network loop, and runs its own init system (systemd).&lt;/p&gt;

&lt;p&gt;If your experiment requires testing firewall structures (&lt;code&gt;iptables&lt;/code&gt;), adjusting low-level network parameters, running background daemons orchestrated by &lt;code&gt;systemd&lt;/code&gt;, or managing separate kernel modules, a Docker container requires fragile workarounds. Multipass gives you a true server abstraction directly on your laptop. Or even if you just want to spin up 5 things.&lt;/p&gt;

&lt;p&gt;Hope it was interesting, I hope you learned something new. Thx for reading.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>virtualmachine</category>
      <category>docker</category>
      <category>multipass</category>
    </item>
    <item>
      <title>Linkloom - AIWebReader</title>
      <dc:creator>Boris Barac</dc:creator>
      <pubDate>Fri, 12 Jun 2026 16:24:17 +0000</pubDate>
      <link>https://dev.to/boris9027/linkloom-aiwebreader-7gl</link>
      <guid>https://dev.to/boris9027/linkloom-aiwebreader-7gl</guid>
      <description>&lt;h1&gt;
  
  
  LinkLoom
&lt;/h1&gt;

&lt;p&gt;A web scraping and content extraction toolkit for TypeScript/Bun.&lt;/p&gt;

&lt;p&gt;Pass a URL, get clean markdown. That's the core. But LinkLoom also handles the cases that break simple scrapers: JavaScript-heavy pages rendered through a stealth browser, PDFs parsed into structured text, iframes pulled from nested frames, HTML tables converted to markdown tables, links extracted and classified. It exposes a library API, a CLI, and an MCP server — so you can use it from code, from the terminal, or from an AI client like Claude Desktop or Cursor.&lt;/p&gt;

&lt;p&gt;The full list: URL-to-markdown conversion, HTML-to-markdown via Readability + Turndown, PDF-to-markdown via pdf.js, headless browser rendering through Camoufox (stealth Firefox on Playwright), iframe extraction with configurable wait strategies, link extraction and classification, table scraping, text embeddings via OpenAI or Gemini, a CLI for every feature, and an MCP server for AI tool-use workflows.&lt;/p&gt;

&lt;p&gt;Built with Bun, Camoufox, JSDOM, Readability, Turndown, and pdf.js-extract. Optional embedding support through LangChain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;convertLinkToMarkdown&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;linkloom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;markdown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;convertLinkToMarkdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. One import, one call. The function auto-detects whether the URL points to an HTML page or a PDF and routes it to the right converter. You get back a string of clean markdown — no boilerplate, no configuration objects, no setup ceremony.&lt;/p&gt;

&lt;p&gt;The CLI equivalent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bunx @boris.barac/linkloom scrape https://example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same result, different interface. Pipe it, redirect it, pass &lt;code&gt;-o output.md&lt;/code&gt; to write to a file.&lt;/p&gt;

&lt;p&gt;But plenty of pages don't hand you their content on the first request. They render everything with JavaScript — SPAs, dashboards, dynamically loaded articles. A simple fetch returns an empty shell. LinkLoom handles this through headless browser rendering via Camoufox, a stealth Firefox build on Playwright that avoids bot detection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;renderers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;linkloom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;renderers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;puppeterRendered&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;renderers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;puppeterRendered&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;renderPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;networkidle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1080&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;renderPage&lt;/code&gt; function loads the URL in a real browser, waits for the network to settle (or for a specific event), and returns the rendered HTML. The &lt;code&gt;frames&lt;/code&gt; option tells it to also extract content from nested iframes — with its own timeout, because iframes load on their own schedule and you don't want one slow frame to block everything.&lt;/p&gt;

&lt;p&gt;The CLI version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bunx @boris.barac/linkloom render https://example.com &lt;span class="nt"&gt;--wait-until&lt;/span&gt; networkidle &lt;span class="nt"&gt;--timeout&lt;/span&gt; 15000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;--selector "table.stats"&lt;/code&gt; to extract only a specific element instead of the full page. Useful when you know exactly what you're after.&lt;/p&gt;

&lt;p&gt;Then there are PDFs. Research papers, technical reports, product documentation — a surprising amount of the web's useful content lives in PDFs, not HTML pages. The same &lt;code&gt;convertLinkToMarkdown&lt;/code&gt; call handles both, but you can also convert PDFs directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pdfConverter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;linkloom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;document.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;markdown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pdfConverter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;convertPdfToMarkdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pdfConverter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;convertPdfToText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two output modes: &lt;code&gt;convertPdfToMarkdown&lt;/code&gt; preserves structure (headings, lists, formatting), while &lt;code&gt;convertPdfToText&lt;/code&gt; strips everything down to plain text. Pick whichever fits your pipeline.&lt;/p&gt;

&lt;p&gt;The CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bunx @boris.barac/linkloom pdf document.pdf &lt;span class="nt"&gt;-o&lt;/span&gt; output.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood it uses pdf.js-extract to parse the binary, so there's no external dependency on system tools like &lt;code&gt;pdftotext&lt;/code&gt;. It works out of the box.&lt;/p&gt;

&lt;p&gt;Content conversion is half the job. The other half is pulling structured data out of pages — links, tables, the things that aren't prose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Link extraction&lt;/strong&gt; finds and classifies URLs from plain text or HTML. Feed it a string and it returns every link, tagged as a PDF or a regular page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;linkExtraction&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;linkloom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;links&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;linkExtraction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extractLinks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;check https://example.com/doc.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pdfLinks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;linkExtraction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extractDownloadLinksFromHtml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;htmlContent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;extractLinks&lt;/code&gt; works on raw text — it finds URLs and classifies them. &lt;code&gt;extractDownloadLinksFromHtml&lt;/code&gt; parses an HTML document and pulls out links that point to downloadable files (PDFs, mostly). Useful when you're crawling a page and want to know which links lead to documents worth converting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Table extraction&lt;/strong&gt; renders a page in the headless browser and pulls out HTML tables as structured data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tableExtraction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;renderers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;linkloom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;renderers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;puppeterRendered&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tableExtraction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extractTableData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;table&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;md&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tableExtraction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tableDataToMarkdownTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The third argument is a CSS selector — pass &lt;code&gt;"table"&lt;/code&gt; for all tables, or &lt;code&gt;"table.stats"&lt;/code&gt; for a specific one. The output is a markdown table string, ready to drop into a document.&lt;/p&gt;

&lt;p&gt;The CLI shortcuts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bunx @boris.barac/linkloom links https://example.com
bunx @boris.barac/linkloom tables https://example.com/data &lt;span class="nt"&gt;--selector&lt;/span&gt; &lt;span class="s2"&gt;"table.stats"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All of this is also available as an MCP server. If you use Claude Desktop, Cursor, or any MCP-compatible client, you can expose LinkLoom's tools without writing code — the AI calls them directly.&lt;/p&gt;

&lt;p&gt;Six tools: &lt;code&gt;scrape&lt;/code&gt;, &lt;code&gt;html_to_markdown&lt;/code&gt;, &lt;code&gt;pdf_to_markdown&lt;/code&gt;, &lt;code&gt;render_page&lt;/code&gt;, &lt;code&gt;extract_links&lt;/code&gt;, &lt;code&gt;extract_tables&lt;/code&gt;. Same capabilities as the library and CLI, but surfaced as tool calls an AI agent can use autonomously.&lt;/p&gt;

&lt;p&gt;Configuration is a few lines of JSON. For Claude Desktop, edit &lt;code&gt;~/Library/Application Support/Claude/claude_desktop_config.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"linkloom"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bun"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@boris.barac/linkloom"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Cursor, add the same block to &lt;code&gt;.cursor/mcp.json&lt;/code&gt; in your project or &lt;code&gt;~/.cursor/mcp.json&lt;/code&gt; globally. Any MCP client — point it at &lt;code&gt;bun x @boris.barac/linkloom mcp&lt;/code&gt; and it works.&lt;/p&gt;

&lt;p&gt;The server communicates over stdio. It reads JSON-RPC from stdin and writes responses to stdout. You don't run it directly; MCP clients spawn it as a child process. If you want to test it interactively, there's the MCP Inspector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bunx @modelcontextprotocol/inspector bunx @boris.barac/linkloom mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That opens a web UI where you can browse the available tools, call them with custom parameters, and inspect the JSON-RPC messages going back and forth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun add @boris.barac/linkloom
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or skip the install and use it directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bunx @boris.barac/linkloom scrape https://example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No API keys needed for the core scraping pipeline. Only the optional text embedding feature requires an OpenAI or Gemini key.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>mcp</category>
      <category>productivity</category>
      <category>automation</category>
    </item>
    <item>
      <title>A Practical Guide to Local Lambda Debugging: Using SAM CLI with Terraform and VS Code</title>
      <dc:creator>Boris Barac</dc:creator>
      <pubDate>Fri, 12 Jun 2026 16:21:04 +0000</pubDate>
      <link>https://dev.to/boris9027/a-practical-guide-to-local-lambda-debugging-using-sam-cli-with-terraform-and-vs-code-5fcm</link>
      <guid>https://dev.to/boris9027/a-practical-guide-to-local-lambda-debugging-using-sam-cli-with-terraform-and-vs-code-5fcm</guid>
      <description>&lt;p&gt;A practical guide to debugging Lambda functions locally using SAM CLI and VS Code — no more deploying just to test a print() statement.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You'll Learn
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;How to invoke a Lambda function locally from the command line&lt;/li&gt;
&lt;li&gt;How to spin up a local API that mirrors your API Gateway&lt;/li&gt;
&lt;li&gt;How to attach a debugger in VS Code and step through your code&lt;/li&gt;
&lt;li&gt;How to hit a deployed Lambda from your machine&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Project
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Github repo&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This repo deploys a simple serverless stack:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lambda Function&lt;/td&gt;
&lt;td&gt;Python function that writes a timestamp to S3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 Bucket&lt;/td&gt;
&lt;td&gt;Stores timestamp files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API Gateway (HTTP)&lt;/td&gt;
&lt;td&gt;Exposes the Lambda at &lt;code&gt;GET /timestamp&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All infrastructure is defined in Terraform. The Lambda source lives in &lt;code&gt;lambda_function/lambda_function.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    AWS Lambda function that stores current timestamp in S3.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;current_timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="n"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;bucket_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S3_BUCKET_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;file_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;current_timestamp&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bucket_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;file_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_timestamp&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Timestamp saved successfully&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;current_timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;file_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Make sure these are installed and working before you start: Terraform, AWS SAM CLI, Docker, Python, VSCode. Verify everything is in place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform version
sam &lt;span class="nt"&gt;--version&lt;/span&gt;
docker ps
python3 &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;docker ps&lt;/code&gt; should run without error. If Docker isn't running, SAM CLI will fail when trying to build or invoke locally.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Project File Map
&lt;/h2&gt;

&lt;p&gt;Here's what matters for debugging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── main.tf                         # All Terraform infra (Lambda, S3, API Gateway, IAM)
├── lambda_function/
│   ├── lambda_function.py          # Your Lambda handler
│   └── requirements.txt            # Python dependencies
├── ev.json                         # Sample event payload for local invoke
├── .vscode/
│   └── launch.json                 # VS Code debug configurations
└── cli_helper.txt                  # Quick reference CLI commands
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Method 1: Debugging with SAM CLI (Command Line)
&lt;/h2&gt;

&lt;p&gt;SAM CLI can read your Terraform project directly using the &lt;code&gt;--hook-name terraform&lt;/code&gt; flag. No SAM template needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — Build
&lt;/h3&gt;

&lt;p&gt;SAM needs to prepare a local build of your function (it resolves dependencies and packages the code):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sam build &lt;span class="nt"&gt;--hook-name&lt;/span&gt; terraform
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see output like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Building codeuri: /Users/you/tf-sam-play-master/lambda_function runtime: python3.13
metadata: {} functions: my_lambda
Running PythonPipBuilder:ResolveDependencies
Running PythonPipBuilder:CopySource

Build Succeeded

Built Artifacts  : .aws-sam-iacs/build
Built Template   : .aws-sam-iacs/build/template.yaml
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Re-run this every time you change your Python code or dependencies.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 2 — Invoke Locally with a Custom Event
&lt;/h3&gt;

&lt;p&gt;The repo includes &lt;code&gt;ev.json&lt;/code&gt; — a sample S3 event payload. You can use it to simulate an invocation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sam &lt;span class="nb"&gt;local &lt;/span&gt;invoke &lt;span class="nt"&gt;--event&lt;/span&gt; ./ev.json &lt;span class="nt"&gt;--hook-name&lt;/span&gt; terraform &lt;span class="nt"&gt;--debug&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--event ./ev.json&lt;/code&gt; — feeds the event payload to your handler&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--hook-name terraform&lt;/code&gt; — tells SAM to read from Terraform, not a SAM template&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--debug&lt;/code&gt; — prints verbose logs (Docker commands, environment, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Timestamp saved successfully"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1745232000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"timestamp-1745232000.txt"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; The &lt;code&gt;s3 put_object&lt;/code&gt; call will fail locally unless you have real AWS credentials that can reach the S3 bucket. For offline-only testing, you'd mock &lt;code&gt;boto3&lt;/code&gt; (covered in the Troubleshooting section).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 3 — Run a Local API
&lt;/h3&gt;

&lt;p&gt;If you want to test through the API Gateway path (not just direct invoke):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sam &lt;span class="nb"&gt;local &lt;/span&gt;start-api &lt;span class="nt"&gt;--hook-name&lt;/span&gt; terraform &lt;span class="nt"&gt;--debug&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SAM starts a local HTTP server. You'll see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Mounting my_lambda at http://127.0.0.1:3000/{path+}
You can now browse to the above endpoints to invoke your functions.
Running on http://127.0.0.1:3000 (Press CTRL+C to quit)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now hit it from another terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://127.0.0.1:3000/timestamp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful for verifying that the API Gateway integration is wired correctly and that event routing works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 — Remote Invoke (Hit the Deployed Lambda)
&lt;/h3&gt;

&lt;p&gt;Once you've deployed with &lt;code&gt;terraform apply&lt;/code&gt;, you can invoke the live Lambda from your machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sam remote invoke my_lambda &lt;span class="nt"&gt;--stack-name&lt;/span&gt; terraform-2025 &lt;span class="nt"&gt;--event&lt;/span&gt; &lt;span class="s1"&gt;'{"test": "data"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sends a real event to the deployed function in AWS. Useful for comparing local vs. remote behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 2: Debugging in VS Code
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1 — Install the AWS Toolkit Extension
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Open VS Code&lt;/li&gt;
&lt;li&gt;Go to Extensions (&lt;code&gt;Cmd+Shift+X&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Search for &lt;strong&gt;AWS Toolkit&lt;/strong&gt; and install it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This extension understands SAM projects and provides the &lt;code&gt;aws-sam&lt;/code&gt; debug type.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 — Understand the Launch Config
&lt;/h3&gt;

&lt;p&gt;The repo ships with &lt;code&gt;.vscode/launch.json&lt;/code&gt; containing three configurations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"configurations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws-sam"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"direct-invoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lambda_function:lambda_function.lambda_handler (python3.13)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"invokeTarget"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"projectRoot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${workspaceFolder}/lambda_function"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"lambdaHandler"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lambda_function.lambda_handler"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"lambda"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"runtime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"python3.13"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"json"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                        &lt;/span&gt;&lt;span class="nl"&gt;"fake"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"event"&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"environmentVariables"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Attach to SAM Local"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"debugpy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"attach"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"listen"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"host"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"pathMappings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"localRoot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${workspaceFolder}/lambda_function/lambda_function"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"remoteRoot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/timestamp "&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Python: Current File"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"debugpy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"launch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"program"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${file}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"console"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integratedTerminal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"justMyCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what each one does:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Config Name&lt;/th&gt;
&lt;th&gt;When to Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Direct Invoke (&lt;code&gt;aws-sam&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Quick one-click invoke. Builds and runs in Docker.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attach to SAM Local (&lt;code&gt;debugpy&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Start API from CLI, then attach debugger. Full breakpoints.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python: Current File&lt;/td&gt;
&lt;td&gt;Run the Python file directly (no Lambda context).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 3 — Set a Breakpoint and Debug
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Option A — Direct Invoke (quickest)
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;code&gt;lambda_function/lambda_function.py&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click the gutter to the left of line 11 (&lt;code&gt;current_timestamp = ...&lt;/code&gt;) to set a breakpoint (red dot)&lt;/li&gt;
&lt;li&gt;Press &lt;code&gt;F5&lt;/code&gt; or go to &lt;strong&gt;Run → Start Debugging&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;"lambda_function:lambda_function.lambda_handler (python3.13)"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;VS Code builds a Docker container, invokes the handler, and pauses at your breakpoint&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can now inspect &lt;code&gt;event&lt;/code&gt;, &lt;code&gt;context&lt;/code&gt;, and all local variables in the Variables panel.&lt;/p&gt;

&lt;h4&gt;
  
  
  Option B — Attach to a Running Local API
&lt;/h4&gt;

&lt;p&gt;This is the most realistic debugging setup because it mirrors the full request flow:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Terminal 1&lt;/strong&gt; — Start SAM's local API with the debug port open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sam &lt;span class="nb"&gt;local &lt;/span&gt;start-api &lt;span class="nt"&gt;--hook-name&lt;/span&gt; terraform &lt;span class="nt"&gt;--debug-port&lt;/span&gt; 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;VS Code&lt;/strong&gt; — Attach the debugger:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set your breakpoints in &lt;code&gt;lambda_function.py&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Press &lt;code&gt;F5&lt;/code&gt; and select &lt;strong&gt;"Attach to SAM Local"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The debugger connects and waits&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Terminal 2&lt;/strong&gt; — Trigger a request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://127.0.0.1:3000/timestamp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;VS Code pauses execution at your breakpoints. Step through the handler line by line.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 — Customize the Event Payload
&lt;/h3&gt;

&lt;p&gt;In the Direct Invoke config, change the payload to match a real event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"json"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"queryStringParameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"value"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"pathParameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or point it to a file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"jsonPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${workspaceFolder}/ev.json"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Working with Events
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Generate Sample Events with SAM
&lt;/h3&gt;

&lt;p&gt;Don't hand-write event payloads. SAM can generate them for you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate an S3 Put event&lt;/span&gt;
sam &lt;span class="nb"&gt;local &lt;/span&gt;generate-event s3 put &lt;span class="nt"&gt;--bucket&lt;/span&gt; my-bucket &lt;span class="nt"&gt;--key&lt;/span&gt; my-key

&lt;span class="c"&gt;# Generate an API Gateway HTTP API event (proxy integration)&lt;/span&gt;
sam &lt;span class="nb"&gt;local &lt;/span&gt;generate-event apigateway http-api-proxy &lt;span class="nt"&gt;--method&lt;/span&gt; GET &lt;span class="nt"&gt;--path&lt;/span&gt; timestamp

&lt;span class="c"&gt;# Save to a file&lt;/span&gt;
sam &lt;span class="nb"&gt;local &lt;/span&gt;generate-event s3 put &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; my-event.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then use it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sam &lt;span class="nb"&gt;local &lt;/span&gt;invoke &lt;span class="nt"&gt;--event&lt;/span&gt; my-event.json &lt;span class="nt"&gt;--hook-name&lt;/span&gt; terraform &lt;span class="nt"&gt;--debug&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>aws</category>
      <category>lambda</category>
      <category>terraform</category>
    </item>
    <item>
      <title>5 AI Agents, 0 System Crashes: Secure Sandboxing for Free</title>
      <dc:creator>Boris Barac</dc:creator>
      <pubDate>Fri, 12 Jun 2026 16:13:51 +0000</pubDate>
      <link>https://dev.to/boris9027/5-ai-agents-0-system-crashes-secure-sandboxing-for-free-46c3</link>
      <guid>https://dev.to/boris9027/5-ai-agents-0-system-crashes-secure-sandboxing-for-free-46c3</guid>
      <description>&lt;p&gt;Everyone's hyped about running five agents at once, but hardly anyone talks about how to keep them secure without crashing your system. Daytona is a cool option, but it's paid. Here's a way to do it for free while leveling up your existing dev skills.&lt;/p&gt;

&lt;p&gt;Docker Sandboxes can run AI agents or code inside isolated microVMs with their own Docker daemon, while mounting your local project into the sandbox at the same absolute path as on your host. That gives the agent real access to your code without exposing your host Docker environment directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Main points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Isolation:&lt;/strong&gt; each sandbox has its own filesystem, network, and private Docker daemon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local machine link:&lt;/strong&gt; your project is mounted directly, and host services are reachable from the sandbox via &lt;code&gt;host.docker.internal&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Networking:&lt;/strong&gt; outbound traffic is routed through host-controlled proxy/policy layers, and services inside the sandbox must be explicitly published to be reachable from your browser (easy to control and change).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistence:&lt;/strong&gt; installed packages, images, and config changes stay until you remove the sandbox.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customization:&lt;/strong&gt; you can extend an agent base template, add tools like Bun, push the image to an OCI registry, and run the sandbox from that template.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Minimal workflow
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Note this is not gonna pick up the global config but just the local one&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;docker/tap/sbx
sbx login
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/my-project
sbx run claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Shell connect to Sandbox
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Agent sandbox&lt;/span&gt;
sbx &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &amp;lt;sandbox-name&amp;gt; bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Run sandbox
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sbx run shell
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Run command in sandbox without connecting
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sbx &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &amp;lt;sandbox-name&amp;gt; &amp;lt;your-command&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Template example (Bun)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Can not be used with local templates&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can also use the &lt;code&gt;opencode&lt;/code&gt; keyword instead of Claude Code.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/ai/sandboxes/agents/opencode/" rel="noopener noreferrer"&gt;https://docs.docker.com/ai/sandboxes/agents/opencode/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.docker.com/ai/sandboxes/agents/claude-code/" rel="noopener noreferrer"&gt;https://docs.docker.com/ai/sandboxes/agents/claude-code/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; docker/sandbox-templates:opencode&lt;/span&gt;

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; root&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; curl &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; agent&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://bun.sh/install | bash

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PATH="/home/agent/.bun/bin:${PATH}"&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;bun &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Log in to Docker Hub (if you haven't already)&lt;/span&gt;
docker login docker.io

&lt;span class="c"&gt;# 2. Build the image locally&lt;/span&gt;
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; docker.io/my-org/my-bun-template:v1 &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# 3. Push the image to your registry&lt;/span&gt;
docker push docker.io/my-org/my-bun-template:v1

&lt;span class="c"&gt;# 4. Run your sandbox environment&lt;/span&gt;
&lt;span class="c"&gt;# add -name variable if you do not want to use the name of the folder&lt;/span&gt;
sbx run &lt;span class="nt"&gt;--template&lt;/span&gt; docker.io/my-org/my-bun-template:v1 claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is how to use a custom template. You can also just install stuff while in the sandbox — the sandbox has root access, and the changes are gonna stay in it.&lt;/p&gt;

&lt;p&gt;More docs at: &lt;a href="https://docs.docker.com/ai/sandboxes/" rel="noopener noreferrer"&gt;https://docs.docker.com/ai/sandboxes/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;#AIAgents #Docker #CyberSecurity #SoftwareEngineering #DevOps #OpenSource #AIInfrastructure #LLMs #ClaudeCode #Sandboxing&lt;/p&gt;

</description>
      <category>agents</category>
      <category>claude</category>
      <category>sandbox</category>
      <category>docker</category>
    </item>
  </channel>
</rss>
