<?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: Dan Stephenson</title>
    <description>The latest articles on DEV Community by Dan Stephenson (@ispyhumanfly).</description>
    <link>https://dev.to/ispyhumanfly</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4172106%2Fcb7d1344-03b1-493c-b5e3-92fa84cf2f11.jpeg</url>
      <title>DEV Community: Dan Stephenson</title>
      <link>https://dev.to/ispyhumanfly</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ispyhumanfly"/>
    <language>en</language>
    <item>
      <title>One Actor, two stores: listing an Apify Actor on Bytewells without forking it</title>
      <dc:creator>Dan Stephenson</dc:creator>
      <pubDate>Fri, 23 Oct 2026 12:59:43 +0000</pubDate>
      <link>https://dev.to/ispyhumanfly/one-actor-two-stores-listing-an-apify-actor-on-bytewells-without-forking-it-2op6</link>
      <guid>https://dev.to/ispyhumanfly/one-actor-two-stores-listing-an-apify-actor-on-bytewells-without-forking-it-2op6</guid>
      <description>&lt;p&gt;&lt;em&gt;Dan Stephenson, ThoughtPivot&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Apify retired rental pricing for Actors on October 1, 2026. Rental Actors that were not moved to pay-per-event became free to use and stopped earning their developers anything. Some developers moved to pay-per-event and are doing well. Others had heavy, recurring renters that a flat monthly price suited better.&lt;/p&gt;

&lt;p&gt;Bytewells by ThoughtPivot is a second store for those Actors. It is in private beta now, and the public store opens on Monday, November 2. This post covers the question I get most from Actor developers: &lt;strong&gt;do I have to maintain a second codebase?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Short answer: no. Here is how it works, and what it does not do yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your Apify listing stays live
&lt;/h2&gt;

&lt;p&gt;There is no exclusivity. You keep your Apify listing and your Apify pricing. Bytewells is an extra channel for the same Actor. Renters run the built image and never receive your source.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code doesn't change
&lt;/h2&gt;

&lt;p&gt;Bytewells exposes an Apify-compatible API. Actors written with the Apify SDK and Crawlee, and clients using &lt;code&gt;apify-client&lt;/code&gt;, work unchanged; a client points at a different base URL instead of being rewritten.&lt;/p&gt;

&lt;p&gt;Inside the Actor, &lt;code&gt;Actor.init()&lt;/code&gt;, &lt;code&gt;Actor.getInput()&lt;/code&gt;, &lt;code&gt;Actor.pushData()&lt;/code&gt; and &lt;code&gt;Actor.exit()&lt;/code&gt; behave the way they do on Apify. One codebase ships to both stores.&lt;/p&gt;

&lt;h2&gt;
  
  
  Import instead of re-listing
&lt;/h2&gt;

&lt;p&gt;Import brings over your Actor's source, README, input schema and pricing, then builds the listing. There are two ways in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Console:&lt;/strong&gt; sign in with Apify, pick your Actors, and they come in as draft listings you can review before publishing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLI (for tokens and CI):&lt;/strong&gt; the package is &lt;a href="https://www.npmjs.com/package/@thoughtpivot/bytewells" rel="noopener noreferrer"&gt;&lt;code&gt;@thoughtpivot/bytewells&lt;/code&gt;&lt;/a&gt; on npm.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @thoughtpivot/bytewells
bw signup &lt;span class="nt"&gt;--username&lt;/span&gt; you
bw import apify you/your-actor &lt;span class="nt"&gt;--publish&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Waitlist developers get hands-on help with the import during the private beta.&lt;/p&gt;

&lt;h2&gt;
  
  
  How renters pay
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Monthly rental,&lt;/strong&gt; with a free trial you set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional passes&lt;/strong&gt; for bursty work: Flash (1 hour), Burst (24 hours) and Sprint (7 days). Pay-per-use charges inside a window are capped at the next tier's price, like a transit fare cap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute is billed separately&lt;/strong&gt; from the renter's prepaid credit at $0.15 per compute unit, against $0.20 on Apify Starter and $0.13 on Apify Business. That is a per-unit rate. What a run costs overall depends on how much compute it uses and on the Actor's own price.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Commission is 10% (20% on Apify), and 0% on renters you bring yourself. Payouts go out monthly through Stripe once your balance reaches $50; check the developer page for the countries Stripe payouts currently cover.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this fits
&lt;/h2&gt;

&lt;p&gt;Our September analysis of 291 former rental Actors put the median break-even at 7,411 results a month. Renters pulling more than that paid less with rental plus compute than with pay-per-event at Apify list prices; below it, pay-per-event was cheaper. If your heaviest renters run pipelines, schedules or agents every day, a monthly price may suit them. Full method: &lt;a href="https://bytewells.com/apify-rental-alternative" rel="noopener noreferrer"&gt;https://bytewells.com/apify-rental-alternative&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do yet
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No paying renters yet.&lt;/strong&gt; We are building supply and demand at the same time and would rather say so.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pay-per-usage pricing on Bytewells comes after launch.&lt;/strong&gt; We started with rentals because that is the model that lost its home on October 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent pass purchase via MCP is coming.&lt;/strong&gt; An MCP server that lets agents find Actors, run them and buy passes is on the roadmap. Today, rentals and passes are bought by people.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Get in early
&lt;/h2&gt;

&lt;p&gt;If you have Actors on Apify and want them live when the store opens, join the developer waitlist: &lt;a href="https://bytewells.com/developer-waitlist" rel="noopener noreferrer"&gt;https://bytewells.com/developer-waitlist&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If rentals were part of your income before October 1, I would like to hear what changed for you, in the comments or by reply.&lt;/p&gt;

&lt;h1&gt;
  
  
  thoughtpivot
&lt;/h1&gt;

</description>
      <category>thoughtpivot</category>
      <category>webscraping</category>
      <category>apify</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Streaming Koa server-sent events on Bun: the four things that broke, and what Flight 3.0.1 changed</title>
      <dc:creator>Dan Stephenson</dc:creator>
      <pubDate>Wed, 21 Oct 2026 13:10:02 +0000</pubDate>
      <link>https://dev.to/ispyhumanfly/streaming-koa-server-sent-events-on-bun-the-four-things-that-broke-and-what-flight-301-changed-2nnm</link>
      <guid>https://dev.to/ispyhumanfly/streaming-koa-server-sent-events-on-bun-the-four-things-that-broke-and-what-flight-301-changed-2nnm</guid>
      <description>&lt;p&gt;&lt;em&gt;Bytewells by ThoughtPivot&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Flight is ThoughtPivot's open-source web framework. Since 3.0.0 it ships &lt;code&gt;flight-bun&lt;/code&gt;, a server that runs on &lt;code&gt;Bun.serve&lt;/code&gt; and mounts existing Koa backends (&lt;code&gt;export default router.routes()&lt;/code&gt;) through a Koa-compat layer. On Oct 18 we pushed 3.0.1 to main. It fixes one thing that matters a lot if you stream: Koa server-sent events (SSE) on &lt;code&gt;flight-bun&lt;/code&gt; were buffered until &lt;code&gt;res.end()&lt;/code&gt;. The client connected, the server wrote events, and nothing arrived until the stream closed.&lt;/p&gt;

&lt;p&gt;The fix touched four separate parts of the stack. Each one was a reasonable choice for request-response traffic and a bug for a stream. This post walks through them, because the same four assumptions are probably sitting in your middleware too.&lt;/p&gt;

&lt;p&gt;Commit: &lt;a href="https://github.com/thoughtpivot/flight/commit/7db959993b20593e192fbeb599d164751c8ecaf1" rel="noopener noreferrer"&gt;https://github.com/thoughtpivot/flight/commit/7db959993b20593e192fbeb599d164751c8ecaf1&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The route we wanted to work
&lt;/h2&gt;

&lt;p&gt;This is the usual way to stream from Koa. Nothing Bun-specific:&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="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/events&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;respond&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;            &lt;span class="c1"&gt;// tell Koa we own the response&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&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="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/event-stream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cache-control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keep-alive&lt;/span&gt;&lt;span class="dl"&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;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`data: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;\n\n`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&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;On Node with real Koa, this streams. On &lt;code&gt;flight-bun&lt;/code&gt; 3.0.0, it did not.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The compat layer waited for &lt;code&gt;end()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;flight-bun&lt;/code&gt; has no Node &lt;code&gt;ServerResponse&lt;/code&gt;. The Koa-compat layer hands the route a mock response object, collects what the route writes, and builds a &lt;code&gt;Response&lt;/code&gt; for &lt;code&gt;Bun.serve&lt;/code&gt;. For a normal handler that is fine: collect the body, then return it.&lt;/p&gt;

&lt;p&gt;For a stream, "collect, then return" means "return never." In 3.0.1 the compat layer returns a &lt;code&gt;ReadableStream&lt;/code&gt; response as soon as the route starts a body: on &lt;code&gt;writeHead&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;flushHeaders&lt;/code&gt;, or when &lt;code&gt;ctx.respond = false&lt;/code&gt; is set. Later writes are pushed into that stream. A stream that never calls &lt;code&gt;end()&lt;/code&gt; also no longer hangs with an empty body.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; if your adapter turns a push-style API into a value, decide the response at the first byte, not at the last.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Matching was read too early, so live routes fell through to the SPA 404
&lt;/h2&gt;

&lt;p&gt;Flight serves a single-page app by default and answers unmatched paths with the SPA &lt;code&gt;404&lt;/code&gt;. To decide "matched or not", the compat layer read &lt;code&gt;ctx.matched&lt;/code&gt; after the middleware chain ran.&lt;/p&gt;

&lt;p&gt;Two things broke that. A route that finished the response inside its handler was dropped and replaced by the 404. And with several routers mounted, later routers append to &lt;code&gt;ctx.matched&lt;/code&gt; with &lt;code&gt;push&lt;/code&gt;, so reading it at the wrong moment missed a match from a router mounted after another one. In 3.0.1 the match is read when the stream opens or when the response finishes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; "did anything handle this request?" is a question about time. With streaming, the answer can arrive after your fallback logic has already run.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Client disconnect never reached the route
&lt;/h2&gt;

&lt;p&gt;In the example above, cleanup lives in &lt;code&gt;res.on('close')&lt;/code&gt;. On Node that fires when the client goes away. On the mock response it never fired, so heartbeats and subscribers kept running for clients that were gone.&lt;/p&gt;

&lt;p&gt;3.0.1 wires the request's abort signal to a &lt;code&gt;close&lt;/code&gt; event on the mock response. Routes written the normal Koa way now clean up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; every layer between the socket and your handler needs to forward cancellation. One layer that swallows it is enough to leak.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Gzip and the response cache waited on the stream
&lt;/h2&gt;

&lt;p&gt;Compression middleware buffers so it can compress efficiently. A response cache needs the full body to store it. Both are correct for a JSON response and both stall an event stream.&lt;/p&gt;

&lt;p&gt;In 3.0.1 gzip skips &lt;code&gt;text/event-stream&lt;/code&gt; and any body that is still streaming, and the response cache skips those bodies too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; check content type early and route streams around anything that needs the whole body.&lt;/p&gt;

&lt;h2&gt;
  
  
  The new setting: &lt;code&gt;FLIGHT_IDLE_TIMEOUT&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Bun's server closes idle connections after 10 seconds by default. A quiet SSE connection looks idle. 3.0.1 adds &lt;code&gt;FLIGHT_IDLE_TIMEOUT&lt;/code&gt; (seconds):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;default &lt;code&gt;10&lt;/code&gt; (Bun's own default)&lt;/li&gt;
&lt;li&gt;an explicit &lt;code&gt;0&lt;/code&gt; disables the server idle timeout&lt;/li&gt;
&lt;li&gt;positive fractions round up to at least &lt;code&gt;1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;negative or non-numeric values fall back to &lt;code&gt;10&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;values above &lt;code&gt;255&lt;/code&gt; are clamped to Bun's maximum&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Server-sent event responses disable the idle timeout for that request, so you do not have to turn it off globally to keep streams open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things that do not carry over from the Node server
&lt;/h2&gt;

&lt;p&gt;The 3.0.1 docs also spell out two differences that are easy to miss when moving a Flight app from Node to Bun:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No cluster.&lt;/strong&gt; &lt;code&gt;flight-bun&lt;/code&gt; is one process. It does not read &lt;code&gt;FLIGHT_MAX_WORKERS&lt;/code&gt; and does not use Node's &lt;code&gt;cluster&lt;/code&gt; module. Scale with replicas. (Our benchmark note: on 4 cores, a 4-worker Node cluster served 80.7k req/s against 53.8k for &lt;code&gt;flight-bun&lt;/code&gt;. Method and raw data are in &lt;code&gt;bench/BENCHMARKS.md&lt;/code&gt;.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate-limit storage is per process.&lt;/strong&gt; The Bun limiter keeps counters in an in-memory &lt;code&gt;Map&lt;/code&gt; and does not use Redis, even when &lt;code&gt;FLIGHT_REDIS_URL&lt;/code&gt; is set. Replicas do not share a window, so a limit of 100 across four processes allows 400 requests in the window.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Checklist for your own stack
&lt;/h2&gt;

&lt;p&gt;If you are adding SSE to a framework or adapter you did not write:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does the adapter return a streaming body at the first write?&lt;/li&gt;
&lt;li&gt;Is "was this request handled?" decided after the stream opens, not before?&lt;/li&gt;
&lt;li&gt;Does client disconnect reach &lt;code&gt;res.on('close')&lt;/code&gt; (or your equivalent)?&lt;/li&gt;
&lt;li&gt;Do compression and caching skip &lt;code&gt;text/event-stream&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;What is your server's idle timeout, and is it disabled for streams only?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Flight is at &lt;a href="https://github.com/thoughtpivot/flight" rel="noopener noreferrer"&gt;https://github.com/thoughtpivot/flight&lt;/a&gt;. 3.0.1 is on main; there is no GitHub release tag for it yet. Issues and PRs are welcome.&lt;/p&gt;

&lt;p&gt;We build Flight alongside Bytewells by ThoughtPivot, a store for monthly rentals and short passes on Apify-compatible Actors, opening Nov 2. Developer waitlist: &lt;a href="https://bytewells.com/developer-waitlist" rel="noopener noreferrer"&gt;https://bytewells.com/developer-waitlist&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  thoughtpivot
&lt;/h1&gt;

</description>
      <category>thoughtpivot</category>
      <category>bunjs</category>
      <category>koa</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Flight and Scribe on Bun: method, numbers, and where Node still wins</title>
      <dc:creator>Dan Stephenson</dc:creator>
      <pubDate>Thu, 15 Oct 2026 16:03:15 +0000</pubDate>
      <link>https://dev.to/ispyhumanfly/flight-and-scribe-on-bun-method-numbers-and-where-node-still-wins-4i9</link>
      <guid>https://dev.to/ispyhumanfly/flight-and-scribe-on-bun-method-numbers-and-where-node-still-wins-4i9</guid>
      <description>&lt;p&gt;On one pinned CPU core, Flight's Bun runtime served &lt;strong&gt;61,110 requests per second&lt;/strong&gt; on a hello route. Its Node runtime, at the same commit, served &lt;strong&gt;19,651&lt;/strong&gt;. Scribe served &lt;strong&gt;21,349 reads per second&lt;/strong&gt; against &lt;strong&gt;4,998&lt;/strong&gt;. Flight went from process start to first response in &lt;strong&gt;47 ms&lt;/strong&gt; instead of &lt;strong&gt;1.18 s&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Two caveats up front, because they change how you should read those numbers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;These are &lt;strong&gt;micro-benchmarks&lt;/strong&gt; (one hello route for Flight, one notes schema for Scribe). They are not application benchmarks.&lt;/li&gt;
&lt;li&gt;On &lt;strong&gt;4 cores&lt;/strong&gt;, a 4-worker Node cluster served &lt;strong&gt;80.7k req/s&lt;/strong&gt; on Flight's hello route, against &lt;strong&gt;53.8k&lt;/strong&gt; for one Bun process. The single-core wins do not carry over to a cluster.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This post covers what changed, how we measured, and how to reproduce it. &lt;a href="https://github.com/thoughtpivot/flight" rel="noopener noreferrer"&gt;Flight&lt;/a&gt; is a Koa-based app server for component-shaped Vue and React apps with Vite. &lt;a href="https://github.com/thoughtpivot/scribe" rel="noopener noreferrer"&gt;Scribe&lt;/a&gt; is a schema-driven data service on Postgres with version history and a Redis schema cache. Both are open source, and both now ship a Bun binary next to the unchanged Node one: &lt;code&gt;flight-bun&lt;/code&gt; and &lt;code&gt;scribe-bun&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was compared
&lt;/h2&gt;

&lt;p&gt;The same commit of each repo. In each case the Node binary is the original runtime path and the Bun binary is the port.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Commit&lt;/th&gt;
&lt;th&gt;Node side&lt;/th&gt;
&lt;th&gt;Bun side&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Flight 3.0.0&lt;/td&gt;
&lt;td&gt;&lt;code&gt;b0d8689&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;flight&lt;/code&gt;: Koa on &lt;code&gt;node:cluster&lt;/code&gt;, &lt;code&gt;dist/flight.js&lt;/code&gt; built by &lt;code&gt;tsc&lt;/code&gt;, &lt;code&gt;*.backend.ts&lt;/code&gt; loaded through tsx (the loader the README names). A ts-node variant also ran.&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;flight-bun&lt;/code&gt;: &lt;code&gt;Bun.serve&lt;/code&gt;, &lt;code&gt;src/bun/server.ts&lt;/code&gt; run directly as TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scribe 1.3.0&lt;/td&gt;
&lt;td&gt;&lt;code&gt;9a0239f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;scribe&lt;/code&gt;: Express on &lt;code&gt;node:cluster&lt;/code&gt;, &lt;code&gt;dist/scribe.cli.js&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;scribe-bun&lt;/code&gt;: &lt;code&gt;Bun.serve&lt;/code&gt; transport over the same &lt;code&gt;src/core.ts&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Method
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Hardware and versions.&lt;/strong&gt; An 8-vCPU Intel Xeon VM with 15 GB RAM, Debian 13, kernel 6.12. It is a shared box with other processes running, so expect noise. Node 20.19.2, Bun 1.4.2, PostgreSQL 17.11, Redis 8.0.2, wrk 4.1.0, hyperfine 1.19.0.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pinning.&lt;/strong&gt; Single-core runs: server on core 1, Postgres and Redis on cores 2-3, wrk on cores 4-7 (&lt;code&gt;-t4 -c100&lt;/code&gt;, and &lt;code&gt;-c50&lt;/code&gt; for Scribe writes). Four-core runs: server on cores 0-3, Postgres and Redis on 4-5, wrk on 6-7 for Scribe and on 4-7 for Flight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Holding Node to one worker.&lt;/strong&gt; Flight has a knob: &lt;code&gt;FLIGHT_MAX_WORKERS=1&lt;/code&gt;. Scribe does not, so the harness preloads a tiny script that makes &lt;code&gt;os.cpus()&lt;/code&gt; report one CPU, which makes &lt;code&gt;node:cluster&lt;/code&gt; fork one worker. The primary process still runs, as it does in production:&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="c1"&gt;// bench/cpus1.cjs&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;os&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="s1"&gt;os&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;one&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cpus&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;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cpus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;one&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Runs.&lt;/strong&gt; Every throughput row is a 10 s warmup followed by 3 x 20 s runs, and we report the mean. No non-2xx responses or socket errors appeared in any run. Memory is RSS of the whole server process tree (Node: primary plus workers), taken at idle after one request and again after load. Startup is the time from exec to the first HTTP 200 on a real route, measured with hyperfine (2 warmups, 15 runs).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workloads.&lt;/strong&gt; Flight: &lt;code&gt;GET /api/hello&lt;/code&gt; returning &lt;code&gt;{"message":"Hello from Flight!"}&lt;/code&gt;, in three variants: (a) a Node/Koa &lt;code&gt;router.routes()&lt;/code&gt; backend, (b) the same Koa file on &lt;code&gt;flight-bun&lt;/code&gt; through its compat layer, (c) a Bun-native route-map backend. Scribe: reads are &lt;code&gt;GET /notes/1&lt;/code&gt;. Writes are &lt;code&gt;POST /notes&lt;/code&gt;, which runs the shipped create path (two &lt;code&gt;CREATE TABLE IF NOT EXISTS&lt;/code&gt;, an &lt;code&gt;ALTER TABLE ... ADD COLUMN IF NOT EXISTS&lt;/code&gt;, the insert, and a history insert). Writes were run twice, the second time with tables truncated and the order reversed. Results matched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Middleware is not identical across the two sides.&lt;/strong&gt; Each runtime used its own shipped default for a non-dev, non-production mode. Node ran koa-logger (it cannot be turned off, output went to /dev/null), session, CORS and bodyparser. Bun ran security headers, CORS and compression, with no request logging. Neither side rate-limited. That is a real difference in work per request, and we are not claiming it is zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results on one core
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Node&lt;/th&gt;
&lt;th&gt;Bun&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Flight req/s, Bun-native route&lt;/td&gt;
&lt;td&gt;19,651 (tsx) / 19,342 (ts-node)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;61,110&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3.1x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flight req/s, unchanged Koa route&lt;/td&gt;
&lt;td&gt;19,651&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;38,826&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.0x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flight p50 / p99, native&lt;/td&gt;
&lt;td&gt;4.6-5.0 ms / 10-25 ms&lt;/td&gt;
&lt;td&gt;1.5-1.6 ms / 2.9-3.0 ms&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flight p99, Koa on Bun&lt;/td&gt;
&lt;td&gt;10-25 ms&lt;/td&gt;
&lt;td&gt;6.3-7.2 ms&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flight idle RSS&lt;/td&gt;
&lt;td&gt;218 MB (primary + 1 worker, tsx)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;27 MB&lt;/strong&gt; native / 47 MB Koa&lt;/td&gt;
&lt;td&gt;about 8x less&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scribe reads req/s&lt;/td&gt;
&lt;td&gt;4,998&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;21,349&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4.3x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scribe read p50 / p99&lt;/td&gt;
&lt;td&gt;17-21 ms / 52-65 ms&lt;/td&gt;
&lt;td&gt;3.9-4.2 ms / 12.2-12.5 ms&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scribe writes req/s&lt;/td&gt;
&lt;td&gt;1,273&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2,327&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.8x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scribe idle RSS&lt;/td&gt;
&lt;td&gt;258 MB (primary + 1 worker)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;68 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;about 3.8x less&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Startup to first 200 (hyperfine, 15 runs): Flight native 1.182 s (tsx) vs &lt;strong&gt;47 ms +/- 10&lt;/strong&gt;; Flight with Koa routes on Bun &lt;strong&gt;123 ms +/- 16&lt;/strong&gt;; Scribe 1.210 s (1 worker) vs &lt;strong&gt;319 ms +/- 34&lt;/strong&gt;. The Node side includes loading TypeScript through tsx at startup, so part of that gap is the loader, not the runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Node still wins: 4 cores
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Node cluster, 4 workers&lt;/th&gt;
&lt;th&gt;Bun, one process&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Flight hello route req/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;80,740&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;53,835 native / 41,526 Koa&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flight RSS (idle to after load)&lt;/td&gt;
&lt;td&gt;550 to 578 MB&lt;/td&gt;
&lt;td&gt;28 to 59 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scribe reads req/s&lt;/td&gt;
&lt;td&gt;21,455&lt;/td&gt;
&lt;td&gt;22,768 (a tie; Postgres on 2 cores is likely the wall)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scribe RSS (idle to after load)&lt;/td&gt;
&lt;td&gt;641 to 700 MB&lt;/td&gt;
&lt;td&gt;69 to 105 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The shipped Bun runtimes are one process and do not use four cores the way a four-worker cluster does. On Bun today you scale with replicas. We have not yet measured several Bun processes behind &lt;code&gt;reusePort&lt;/code&gt; against a Node cluster on the same core count.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in Flight
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Bun.serve with a native route table
&lt;/h3&gt;

&lt;p&gt;Instead of passing every request through a Koa middleware chain, &lt;code&gt;flight-bun&lt;/code&gt; hands &lt;code&gt;Bun.serve&lt;/code&gt; a route table built at startup. A Bun-native &lt;code&gt;*.backend.ts&lt;/code&gt; exports an object keyed by path:&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="c1"&gt;// hello.backend.ts (Bun-native contract)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello from Flight!&lt;/span&gt;&lt;span class="dl"&gt;'&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;p&gt;Discovery scans for &lt;code&gt;*.backend.ts&lt;/code&gt; and &lt;code&gt;*.backend.js&lt;/code&gt; with &lt;code&gt;Bun.Glob&lt;/code&gt;, imports each file, and sorts it by the shape of its default export: an object whose keys all start with &lt;code&gt;/&lt;/code&gt; is a route fragment, anything else goes down the Koa path (below). The server is then started with the route table plus a &lt;code&gt;fetch&lt;/code&gt; fallback:&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="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Bun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// native routes, each wrapped in the middleware chain&lt;/span&gt;
    &lt;span class="na"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// fallback: Koa-compat dispatch, then the SPA&lt;/span&gt;
    &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One detail worth knowing: when CORS is on, &lt;code&gt;OPTIONS&lt;/code&gt; has to be registered per path, because Bun answers 405 for an unregistered method before any of your middleware runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Koa compat layer
&lt;/h3&gt;

&lt;p&gt;We did not want anyone stuck mid-migration, so an existing &lt;code&gt;export default router.routes()&lt;/code&gt; file still boots. The compat layer stands up a private Koa app, adds &lt;code&gt;koa-bodyparser&lt;/code&gt;, mounts the discovered middleware, and calls &lt;code&gt;app.callback()&lt;/code&gt; with a Node-style request and a mock response built from the Fetch &lt;code&gt;Request&lt;/code&gt;:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;callbackToFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Readable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MockResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;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;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HEAD&lt;/span&gt;&lt;span class="dl"&gt;'&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="nf"&gt;alloc&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="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="k"&gt;from&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&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;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;requestFromFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// a Readable with method, url, headers&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&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;MockResponse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                &lt;span class="c1"&gt;// buffers status, headers, chunks&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;finish&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flightMatched&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;           &lt;span class="c1"&gt;// no Koa route matched: fall through to the SPA&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toResponse&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;p&gt;&lt;code&gt;res.flightMatched&lt;/code&gt; is set from &lt;code&gt;ctx.matched&lt;/code&gt;, so an unmatched path returns &lt;code&gt;null&lt;/code&gt; and the request falls through to static files. This path is still 2.0x the Node runtime on our test (38,826 req/s). Rewriting the route as a plain &lt;code&gt;(req) =&amp;gt; Response&lt;/code&gt; handler takes it to 3.1x.&lt;/p&gt;

&lt;h3&gt;
  
  
  Middleware rewrite
&lt;/h3&gt;

&lt;p&gt;The edge middleware (logging, security headers, CORS, gzip, rate limiting, sessions, response cache) was rewritten against &lt;code&gt;Request&lt;/code&gt; and &lt;code&gt;Response&lt;/code&gt; with no Koa packages on that path. The composition is a small onion where index 0 is outermost and a middleware may return without calling &lt;code&gt;next&lt;/code&gt;:&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;middlewares&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Middleware&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;core&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;core&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;dispatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;middleware&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;middlewares&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;core&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&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;return&lt;/span&gt; &lt;span class="nf"&gt;dispatch&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="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;Sessions and the response cache use Redis when &lt;code&gt;redisUrl&lt;/code&gt; is set and fall back to an in-memory store otherwise, so Redis is optional now. We did not run an ablation, so we cannot say how much of the gain comes from the route table, the middleware rewrite, or removing the loader. The two sides also run different default middleware, as noted above.&lt;/p&gt;

&lt;h3&gt;
  
  
  No build step
&lt;/h3&gt;

&lt;p&gt;Bun loads &lt;code&gt;.backend.ts&lt;/code&gt; directly, so production needs neither &lt;code&gt;tsc&lt;/code&gt; nor a TypeScript loader. That is part of the startup gap: on the Node side, &lt;code&gt;tsc&lt;/code&gt; took 3.5 s (Flight) and 4.4 s (Scribe) before anything ran, and the tsx loader is in the startup path.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in Scribe
&lt;/h2&gt;

&lt;p&gt;We split Scribe's core logic from its Express transport (&lt;code&gt;src/core.ts&lt;/code&gt;), then added &lt;code&gt;scribe-bun&lt;/code&gt;, a thin &lt;code&gt;Bun.serve&lt;/code&gt; transport over that core with a &lt;code&gt;GET /healthz&lt;/code&gt; that checks Postgres and Redis. One route suite runs on both runtimes in CI.&lt;/p&gt;

&lt;p&gt;Writes improved 1.8x while reads improved 4.3x, and we know why: the create path still runs &lt;code&gt;CREATE TABLE IF NOT EXISTS&lt;/code&gt; and &lt;code&gt;ALTER TABLE ... ADD COLUMN IF NOT EXISTS&lt;/code&gt; on every insert, on both runtimes. Writes are DDL-bound. Moving DDL off the hot path is the obvious next optimization. A second known cost: the Redis-cache and default-schema path builds a new Ajv instance and compiles on each request. Only discovered schema files are cached in-process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Toolchain timings
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Node toolchain&lt;/th&gt;
&lt;th&gt;Bun&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Flight install, cold cache&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;npm ci&lt;/code&gt; 6.11 s&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bun install&lt;/code&gt; 0.44 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scribe install, cold cache&lt;/td&gt;
&lt;td&gt;6.42 s&lt;/td&gt;
&lt;td&gt;0.86 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scribe test cycle&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;npm test&lt;/code&gt; 10.59 s (tsc build + &lt;code&gt;node --test&lt;/code&gt;, 40 tests)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bun test&lt;/code&gt; 0.87 s (51 tests: the same 3 shared files plus Bun-only checks)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;CI and the docs still run &lt;code&gt;npm ci&lt;/code&gt;, even in the Bun CI job, so &lt;code&gt;bun install&lt;/code&gt; is optional today. Flight's two test suites differ (30 Node tests vs 6 Bun tests), so we do not compare them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to reproduce
&lt;/h2&gt;

&lt;p&gt;The scripts and raw outputs are in each repo's &lt;code&gt;bench/&lt;/code&gt; folder: &lt;a href="https://github.com/thoughtpivot/flight/tree/main/bench" rel="noopener noreferrer"&gt;https://github.com/thoughtpivot/flight/tree/main/bench&lt;/a&gt; and &lt;a href="https://github.com/thoughtpivot/scribe/tree/master/bench" rel="noopener noreferrer"&gt;https://github.com/thoughtpivot/scribe/tree/master/bench&lt;/a&gt;. You need &lt;code&gt;wrk&lt;/code&gt;, &lt;code&gt;hyperfine&lt;/code&gt;, &lt;code&gt;taskset&lt;/code&gt; (Linux), and Bun. For Scribe you also need local Postgres and Redis and &lt;code&gt;SCRIBE_APP_DB_PASS&lt;/code&gt; set.&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;# Flight, one core: c = Bun-native, b = Koa file on Bun, t = Node + tsx, a = Node + ts-node&lt;/span&gt;
&lt;span class="nb"&gt;source &lt;/span&gt;bench/env.sh
bench/throughput.sh c 4003 1          &lt;span class="c"&gt;# server on core 1, wrk on cores 4-7&lt;/span&gt;
&lt;span class="nv"&gt;W&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4 bench/throughput.sh t 4004 0-3    &lt;span class="c"&gt;# Node cluster on 4 cores&lt;/span&gt;

&lt;span class="c"&gt;# Scribe, reads and writes on one core&lt;/span&gt;
bench/throughput.sh sb 5002 1 100 /notes/1
bench/throughput.sh sb 5002 1 50  /notes bench/post.lua
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;throughput.sh&lt;/code&gt; does the 10 s warmup, the three 20 s runs, and the idle and after-load RSS of the whole process tree. &lt;code&gt;startup.sh&lt;/code&gt; times exec to the first 200. Run it a few times and expect some spread on a shared machine. If your numbers differ, open an issue with them. We would like to see where they land.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we have not measured yet
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;End-to-end Flight to Scribe to Postgres with a real Vue or React app&lt;/li&gt;
&lt;li&gt;Several Bun processes (&lt;code&gt;reusePort&lt;/code&gt;, or replicas) against a Node cluster on equal cores&lt;/li&gt;
&lt;li&gt;Container image size and Kubernetes pod cold start&lt;/li&gt;
&lt;li&gt;Production-mode Flight with compression, Redis sessions, rate limiting and static SPA serving on both sides&lt;/li&gt;
&lt;li&gt;A soak of an hour or more for memory growth&lt;/li&gt;
&lt;li&gt;CI wall time if the Bun job switches to &lt;code&gt;bun install&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One known issue: &lt;code&gt;express-bunyan-logger records a request&lt;/code&gt; in &lt;code&gt;src/bun/dependencies.test.ts&lt;/code&gt; is flaky under Bun. It failed in 2 of about 11 local runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @thoughtpivot/flight   &lt;span class="c"&gt;# then run: flight-bun   (needs Bun on PATH)&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; @thoughtpivot/scribe   &lt;span class="c"&gt;# then run: scribe-bun   (needs Bun on PATH, plus Postgres and Redis)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;flight-bun&lt;/code&gt; needs Bun on your PATH and listens on port 3000 by default (set &lt;code&gt;FLIGHT_PORT&lt;/code&gt; to change it). &lt;code&gt;scribe-bun&lt;/code&gt; also needs Bun on your PATH, and it needs a running Postgres and Redis.&lt;/p&gt;

&lt;p&gt;The Node binaries (&lt;code&gt;flight&lt;/code&gt;, &lt;code&gt;scribe&lt;/code&gt;) are unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credit
&lt;/h2&gt;

&lt;p&gt;Early Bun research and benchmarks for Flight and Scribe by Vasilen Lyubomirov (&lt;a href="https://github.com/VasilenL" rel="noopener noreferrer"&gt;github.com/VasilenL&lt;/a&gt;), whose work helped start this.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/thoughtpivot/flight" rel="noopener noreferrer"&gt;github.com/thoughtpivot/flight&lt;/a&gt; and &lt;a href="https://github.com/thoughtpivot/scribe" rel="noopener noreferrer"&gt;github.com/thoughtpivot/scribe&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;#thoughtpivot&lt;/p&gt;

</description>
      <category>thoughtpivot</category>
      <category>bunjs</category>
      <category>typescript</category>
      <category>performance</category>
    </item>
    <item>
      <title>Proxy bills are up and AI scraping is coming. What that means if you sell Apify Actors</title>
      <dc:creator>Dan Stephenson</dc:creator>
      <pubDate>Fri, 09 Oct 2026 12:52:20 +0000</pubDate>
      <link>https://dev.to/ispyhumanfly/proxy-bills-are-up-and-ai-scraping-is-coming-what-that-means-if-you-sell-apify-actors-4pca</link>
      <guid>https://dev.to/ispyhumanfly/proxy-bills-are-up-and-ai-scraping-is-coming-what-that-means-if-you-sell-apify-actors-4pca</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: I work on Bytewells by ThoughtPivot, a second store for Apify Actors. Bytewells is an independent marketplace and is not affiliated with, endorsed by, or sponsored by Apify.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In December 2025, Apify and The Web Scraping Club surveyed hundreds of web scraping professionals. The report came out on Jan 29, 2026, and two of its findings matter if you build and sell Actors.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Running scrapers keeps getting more expensive
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;65.8% of respondents reported increased proxy usage.&lt;/li&gt;
&lt;li&gt;58.3% said their proxy spending went up year over year.&lt;/li&gt;
&lt;li&gt;More than 62% reported higher infrastructure spending, driven largely by stronger anti-bot protections.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you sell an Actor, that cost lands somewhere: in your price, in your margin, or on your renter's compute bill. A browser-based Actor going through residential proxies costs far more per run than a plain HTTP fetch, and a per-result price that ignores this can lose money on every run. Two habits help on any store. Measure compute per result before you set a price, and don't route traffic through a proxy the target doesn't require.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. AI-assisted scraping is growing, but trust isn't there yet
&lt;/h2&gt;

&lt;p&gt;45.8% of respondents use AI in their scraping workflows and 54.2% don't. The holdouts cited trust in results, cost, integration pain and unreliable performance on some sites. Still, 66.2% plan to try AI-assisted tools, and 72.7% of current AI users report productivity gains.&lt;/p&gt;

&lt;p&gt;For Actor developers, the practical change is a new kind of buyer: agents. An agent rarely wants a month of access. It wants an hour or a day, it wants a price it can read before it runs, and it needs a well-described input schema to call your Actor correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we're responding with Bytewells
&lt;/h2&gt;

&lt;p&gt;We're building Bytewells by ThoughtPivot around those two pressures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Commission:&lt;/strong&gt; 10% of each rental and pass charge, against 20% on Apify, and 0% on renters you bring yourself. Stripe's card and billing fees pass through at cost, so a typical $17.50 rental nets you about 85%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing models:&lt;/strong&gt; monthly rentals, plus optional Flash (1 hour), Burst (24 hours) and Sprint (7 days) passes for bursty and agent workloads. Apify retired rental pricing on Oct 1, 2026, so this brings that option back for your heavy renters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute:&lt;/strong&gt; $0.15 per compute unit, paid by the renter separately from the rental. That's below Apify Starter ($0.20) and above Apify Business ($0.13), so it isn't the cheapest option for every buyer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No exclusivity:&lt;/strong&gt; the Apify SDK and apify-client work unchanged, so one codebase can ship to both stores and your Apify listing stays as it is.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The reality check
&lt;/h2&gt;

&lt;p&gt;Bytewells is in private beta, and the public launch is planned for Nov 2, 2026. We have no paying renters yet. Pay-per-event pricing isn't available at launch, so if your Actor's business model depends on &lt;code&gt;Actor.charge()&lt;/code&gt;, Bytewells doesn't fit that model yet. If your buyers are recurring high-volume teams, or agents that work in short bursts, listing on Bytewells too may be worth it. If they're occasional long-tail users, pay-per-event on Apify probably still suits them better.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://blog.apify.com/web-scraping-report-2026/" rel="noopener noreferrer"&gt;State of web scraping report 2026&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you sell Actors and want in, developers on the waitlist are invited first: &lt;a href="https://bytewells.com/developer-waitlist" rel="noopener noreferrer"&gt;https://bytewells.com/developer-waitlist&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>apify</category>
      <category>ai</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
