<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <!-- Source: https://blog.platformatic.dev/rss.xml -->
  <channel>
    <title><![CDATA[Platformatic Blog]]></title>
    <description><![CDATA[Platformatic Blog]]></description>
    <link>https://siftrss.com/f/3XL6Qd79bM</link>
    <image>
      <url>https://cdn.hashnode.com/res/hashnode/image/upload/v1727183405468/9f1d8161-aee0-4422-af77-111e9ea87aef.png</url>
      <title>Platformatic Blog</title>
      <link>https://blog.platformatic.dev</link>
    </image>
    <generator>RSS for Node</generator>
    <lastBuildDate>Tue, 30 Jun 2026 18:10:17 GMT</lastBuildDate>
    <atom:link href="https://siftrss.com/f/3XL6Qd79bM" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <ttl>60</ttl>
    <item>
      <title><![CDATA[Stop Request Stampedes at the Gateway with Platformatic Deduplication]]></title>
      <description><![CDATA[Picture an online store launching a new product and sending out a mailing list campaign. Thousands of users click the same link at once. The product page, built with a Node.js app like Next.js, needs ]]></description>
      <link>https://blog.platformatic.dev/gateway-request-deduplication-nodejs</link>
      <guid isPermaLink="true">https://blog.platformatic.dev/gateway-request-deduplication-nodejs</guid>
      <category><![CDATA[Node.js]]></category>
      <category><![CDATA[api]]></category>
      <category><![CDATA[platformatic]]></category>
      <category><![CDATA[performance]]></category>
      <category><![CDATA[Next.js]]></category>
      <category><![CDATA[caching]]></category>
      <category><![CDATA[Devops]]></category>
      <category><![CDATA[scalability]]></category>
      <dc:creator><![CDATA[Paolo Insogna]]></dc:creator>
      <pubDate>Tue, 30 Jun 2026 14:38:38 GMT</pubDate>
      <enclosure url="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/36bb6375-cd30-4934-b2dc-3c7ed2cce591.png" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p>Picture an online store launching a new product and sending out a mailing list campaign. Thousands of users click the same link at once. The product page, built with a Node.js app like Next.js, needs to fetch the same product details, inventory, recommendations, and pricing for each request.</p>
<p>If the page is cached, everything works smoothly. Problems begin when the cache is empty, expired, or being refreshed. Then, the first group of users all miss the cache together, and every request asks the app to generate the same response. (This same thing would also happen with a trending news story, a search crawler, frontend prefetching, or after a cache reset.)</p>
<p>This situation is known as the “thundering herd” problem. Every user request is valid, but the work gets repeated. Your app uses CPU, database, and network resources to calculate the same result over and over, just when response times are already strained.</p>
<p>Rather than sending all traffic straight to your app, wouldn’t it be great if you could place a gateway in front that spots duplicate in-flight reads and combines them before they hit Node.js? </p>
<p>Platformatic Gateway now does exactly this. With request deduplication, it merges concurrent requests for the same data. Only one goes upstream, while the others wait and then get the same response.</p>
<p>This is not a replacement for caching. Instead, it acts as a short-term coordination layer for requests in progress. The cache handles future requests, while deduplication shields your app while the first response is still being generated.</p>
<p>This is especially important for self-hosted Next.js apps. A popular route can cause heavy server rendering, React Server Component processing, image metadata checks, or backend API calls. When many users hit that route at once, or during cache refreshes, deduplication stops the gateway from sending the same work to Next.js over and over.</p>
<hr />
<h2><strong>A Local Benchmark</strong></h2>
<p>To see how much this helps, we ran a simple local test with a purposely slow route behind a proxy. The upstream route waited 100 ms before responding, simulating a page or API call that needs backend work. The test used the same leader/waiter pattern as gateway deduplication and sent 100 requests at once to the same URL.</p>
<p>These are the median numbers from three runs:</p>
<table style="width:685px"><colgroup><col style="width:142px"></col><col style="width:101px"></col><col style="width:114px"></col><col style="width:114px"></col><col style="width:123px"></col><col style="width:91px"></col></colgroup><tbody><tr><td><p><strong>Scenario</strong></p></td><td><p><strong>Client requests</strong></p></td><td><p><strong>Upstream requests</strong></p></td><td><p><strong>Average latency</strong></p></td><td><p><strong>p99 latency</strong></p></td><td><p><strong>Errors</strong></p></td></tr><tr><td><p>Without deduplication</p></td><td><p>1,000</p></td><td><p>1,000</p></td><td><p>111.31 ms</p></td><td><p>134 ms</p></td><td><p>0</p></td></tr><tr><td><p>With deduplication</p></td><td><p>1,000</p></td><td><p>10</p></td><td><p>104.88 ms</p></td><td><p>127 ms</p></td><td><p>0</p></td></tr><tr><td><p>Without deduplication</p></td><td><p>10,000</p></td><td><p>10,000</p></td><td><p>104.80 ms</p></td><td><p>122 ms</p></td><td><p>0</p></td></tr><tr><td><p>With deduplication</p></td><td><p>10,000</p></td><td><p>100</p></td><td><p>102.91 ms</p></td><td><p>106 ms</p></td><td><p>0</p></td></tr></tbody></table>

<p>The key result isn’t the slight change in average latency, but the number of upstream requests. With deduplication, the proxy still answers every client, but the upstream app only processes one response per wave of requests. In a test with 10,000 requests, that meant just 100 upstream responses instead of 10,000. In real situations, this can mean the difference between a burst that overwhelms your app and one that the gateway handles smoothly.</p>
<hr />
<h2><strong>How It Works</strong></h2>
<p>Gateway deduplication uses a leader/waiter model that is easy to reason about in production.</p>
<p>The first matching request becomes the leader. It acquires a lock, goes to the upstream application, buffers the response, stores it for a short time, and notifies any waiters. Concurrent requests with the same key become waiters. They do not call the upstream service immediately; instead, they wait for the leader response and replay it when it becomes available.</p>
<p>In a production gateway, deduplication often sits next to caching. You can use separate Valkey instances or separate key prefixes in the same Valkey deployment, but the two stores serve different purposes: cache storage keeps reusable responses, while deduplication storage keeps short-lived locks and response buffers for in-flight requests.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/87504c18-e5e8-4763-a428-95b8508848ea.png" alt="" style="display:block;margin:0 auto" />

<p>To prevent deadlocks, every coordination point has an expiration. The leader lock uses a <code>lockTtl</code>, waiters have a <code>timeout</code>, and retries are limited. If the leader fails, the lock expires, a waiter times out, or retries run out, the request switches back to normal proxying. This fallback is intentional—deduplication is meant to lower load, not cause requests to get stuck during traffic spikes.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/9fe7db00-4342-4464-b99c-f16306f36587.png" alt="" style="display:block;margin:0 auto" />

<p>For a single request, the decision path looks like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/60845246-b8d2-4ce5-8d12-2707ea319098.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2><strong>The Simplest Configuration</strong></h2>
<p>Enable deduplication globally under <code>gateway.deduplication:</code></p>
<pre><code class="language-json">{
  "gateway": {
    "deduplication": {
      "enabled": true
    },
    "applications": [
      {
        "id": "frontend",
        "proxy": {
          "prefix": "/"
        }
      }
    ]
  }
}
</code></pre>
<p>By default, deduplication works for <code>GET</code> and <code>HEAD</code> requests and uses <code>memory</code> for storage.</p>
<p>This default is intentionally cautious. <code>GET</code> and <code>HEAD</code> are the safest types of requests to deduplicate. Write requests often need custom rules before they can be safely coordinated.</p>
<hr />
<h2><strong>Per-Application Overrides</strong></h2>
<p>You can also configure deduplication per proxied application with <code>gateway.applications[].proxy.deduplication</code>. Application-level options override the global options.</p>
<pre><code class="language-json">{
 "gateway": {
   "deduplication": {
     "enabled": true,
     "methods": ["GET"]
   },
   "applications": [
     {
       "id": "frontend",
       "proxy": {
         "prefix": "/",
         "deduplication": {
           "enabled": true,
           "routes": [{ "method": "GET", "path": "/blog/*" }]
         }
       }
     }
   ]
 }
}
</code></pre>
<p>This approach lets you begin where the benefits are clear. You can deduplicate public catalogue pages, blog posts, product details, or framework prefetch routes, while keeping endpoints with strict per-user behaviour unchanged.</p>
<hr />
<h2><strong>Choosing The Deduplication Key</strong></h2>
<p>The default key is computed from:</p>
<ul>
<li><p>the configured application origin</p>
</li>
<li><p>the HTTP method</p>
</li>
<li><p>the rewritten proxy URL, including the query string</p>
</li>
<li><p>selected request headers</p>
</li>
</ul>
<p>The default headers are:</p>
<pre><code class="language-plaintext">["authorization", "cookie", "accept", "accept-language"]
</code></pre>
<p>Including headers matters because many read responses are not only a function of the URL. A localized page can depend on accept-language. A user-specific page can depend on <code>cookie</code> or <code>authorization</code>. If those headers were ignored, unrelated callers could incorrectly share a response.</p>
<p>You can adjust the headers included in the key for a deduplication configuration:</p>
<pre><code class="language-json">{
 "gateway": {
   "deduplication": {
     "enabled": true,
     "headers": ["authorization", "cookie", "x-tenant-id"]
   }
 }
}
</code></pre>
<p>Currently, you can’t set headers per route. If you need different header behaviour for different routes, use separate deduplication settings for each application or create a custom key function.</p>
<p>For full control, provide a synchronous key function:</p>
<pre><code class="language-json">{
 "gateway": {
   "deduplication": {
     "enabled": true,
     "key": "./deduplication-key.js"
   }
 }
}
</code></pre>
<pre><code class="language-javascript">export function computeDeduplicationKey(request, context) {
 return `\({context.origin}:\){context.method}:${context.url}`
}
</code></pre>
<p>The function receives the request and a context object containing the origin, method, rewritten URL, parsed query, selected headers, and application configuration. It must return the key synchronously.</p>
<hr />
<h2><strong>Route Whitelisting</strong></h2>
<p>For tighter control, configure a route whitelist. Routes use <a href="https://github.com/delvedor/find-my-way">find-my-way</a> syntax.</p>
<pre><code class="language-json">{
 "gateway": {
   "deduplication": {
     "enabled": true,
     "routes": [
       { "method": "GET", "path": "/blog/*" },
       { "methods": ["GET", "HEAD"], "path": "/products/:id" }
     ]
   }
 }
}
</code></pre>
<p>When <code>routes</code> are configured, route matching decides whether deduplication applies. When <code>routes</code> are not configured, the <code>methods</code> list decides.</p>
<hr />
<h2><strong>Storage: Memory Or Valkey</strong></h2>
<p>TBy default, memory is used for storage. It handles duplicate requests within a single gateway instance and doesn’t need any external service. This setup is great for local development, single-instance deployments, and easy rollouts.</p>
<pre><code class="language-json">{
 "gateway": {
   "deduplication": {
     "enabled": true,
     "storage": {
       "adapter": "memory"
     }
   }
 }
}
</code></pre>
<p>For deployments that scale horizontally, use the valkey adapter. It stores locks, response pointers, and buffered responses in a Redis-compatible Valkey server, allowing multiple gateway workers, instances, or pods to coordinate. This way, you get the same deduplication benefits even when traffic is spread across several replicas.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/860d66e5-8f7d-4f7a-a2e3-c04094af5abf.png" alt="" style="display:block;margin:0 auto" />

<pre><code class="language-json">{
 "gateway": {
   "deduplication": {
     "enabled": true,
     "storage": {
       "adapter": "valkey",
       "url": "redis://127.0.0.1:6379",
       "prefix": "my-application"
     }
   }
 }
}
</code></pre>
<p>Use a <code>prefix</code> if several applications share the same Valkey instance and need separate key spaces.</p>
<hr />
<h2><strong>Operational Behavior</strong></h2>
<p>Deduplication is a best-effort feature, not a guarantee of exactly-once processing.</p>
<p>Duplicate upstream requests can still occur if the in-flight lock expires before the upstream response is ready, if a gateway instance fails while handling the leader request, if a waiter times out, or if retries run out. In these cases, the gateway just switches back to normal proxying.</p>
<p>The main timing options are:</p>
<ul>
<li><p><code>timeout</code>: how long a duplicate request waits for the leader response before retrying lock acquisition</p>
</li>
<li><p><code>retries</code>: how many additional deduplication attempts are made before falling back to normal proxying</p>
</li>
<li><p><code>ttl</code>: how long stored responses remain available for waiting requests</p>
</li>
<li><p><code>lockTtl</code>: how long an in-flight lock can live before it expires</p>
</li>
</ul>
<p>Defaults are:</p>
<pre><code class="language-json">{
 "timeout": 1000,
 "retries": 3,
 "ttl": 10000,
 "lockTtl": 500
}
</code></pre>
<p>Responses are fully buffered before being replayed. This works well for short bursts of duplicate reads, but large responses can use more gateway memory and, with Valkey, add some storage overhead. It’s best to start with routes where responses are small and where repeated upstream work is already costing you in money, speed, or capacity.</p>
<hr />
<h2><strong>Custom Gateway Handlers</strong></h2>
<p>Deduplication comprises custom gateway handlers. When both a custom handler and deduplication are configured, <code>deduplication</code> runs first, and the leader request is delegated to the handler.</p>
<p>Handlers that use <code>reply.from()</code> do not need special handling. Platformatic Gateway uses <code>reply.from()</code> from <a href="https://github.com/fastify/fastify-reply-from">@fastify/reply-from</a> to proxy upstream requests.</p>
<pre><code class="language-javascript">export function handler(request, reply, dest, options) {
 return reply.from(dest, options)
}
</code></pre>
<p>If a handler overrides <code>onResponse</code> or <code>onError</code>, it can call the helper functions provided in <code>options</code>, so waiting requests still receive the correct signal:</p>
<pre><code class="language-javascript">export function handler(request, reply, dest, options) {
 return reply.from(dest, {
   ...options,
   async onResponse(request, reply, res) {
     reply.header('x-custom-handler', 'true')
     return options.deduplicateResponse(request, reply, res)
   },
   async onError(reply, error) {
     return options.deduplicateError(reply, error)
   }
 })
}
</code></pre>
<p>Handlers that send responses directly without <code>reply.from()</code> cannot be replayed by gateway deduplication.</p>
<hr />
<h2><strong>Metrics</strong></h2>
<p>The feature also adds Gateway metrics so you can prove whether deduplication is helping in production:</p>
<ul>
<li><p><code>gateway_deduplication_leader_count</code></p>
</li>
<li><p><code>gateway_deduplication_waiter_count</code></p>
</li>
<li><p><code>gateway_deduplication_replay_count</code></p>
</li>
<li><p><code>gateway_deduplication_fallback_count</code></p>
</li>
<li><p><code>gateway_deduplication_error_count</code></p>
</li>
</ul>
<p>These counters help answer real-world questions: how many requests became leaders, how many waited, how many were replayed, and how often the gateway had to switch back to normal proxying.</p>
<hr />
<h2><strong>Conclusion</strong></h2>
<p>Gateway deduplication works best when lots of clients request the same resource at once, and the upstream response can be safely reused for matching keys. This is just like the product launch or hot-news example from earlier: many users show up at once, the cache is cold or refreshing, and your app is about to generate the same page over and over.</p>
<p>The best places to start are public, read-heavy routes, framework prefetch endpoints, cache refresh paths, and expensive upstream reads with limited response sizes. Turn it on for a small set of routes first. Keep an eye on the leader, waiter, replay, and fallback metrics. Then, expand to other routes where you see duplicate work causing the most trouble.</p>
<p>You’ll see results right away: the gateway handles traffic spikes, your services do less repeated work, and users get more consistent response times during busy periods.</p>
<p>This kind of optimization adds up over time. You protect your upstream resources without changing your app code. You cut down on unnecessary backend load before it hits your databases, APIs, or rendering services. You also get metrics to see if the feature is delivering value. And if deduplication can’t help in a certain case, the request just goes through as usual.</p>
<p>For teams using Platformatic Gateway in front of modern web apps, especially self-hosted Next.js apps, request deduplication is a practical way to make read-heavy traffic more manageable. It gives you a safety net for traffic bursts, an easier way to scale with Valkey, and a rollout approach that doesn’t require changing your backend services.</p>
]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Run Medusa on Kubernetes with Watt
as a Monorepo]]></title>
      <description><![CDATA[Medusa stands out as a flexible open source commerce platform for Node.js. It offers teams a customizable backend, admin tools, and a modern storefront, all without locking you into a strict SaaS mode]]></description>
      <link>https://blog.platformatic.dev/run-medusa-kubernetes-watt-monorepo</link>
      <guid isPermaLink="true">https://blog.platformatic.dev/run-medusa-kubernetes-watt-monorepo</guid>
      <category><![CDATA[medusa]]></category>
      <category><![CDATA[Node.js]]></category>
      <category><![CDATA[Kubernetes]]></category>
      <category><![CDATA[platformatic]]></category>
      <category><![CDATA[monorepo]]></category>
      <category><![CDATA[Next.js]]></category>
      <category><![CDATA[watt]]></category>
      <dc:creator><![CDATA[Paolo Insogna]]></dc:creator>
      <pubDate>Tue, 28 Apr 2026 14:30:00 GMT</pubDate>
      <enclosure url="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/03fd7b26-e637-4b53-bf00-378d94dbf22d.png" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p><a href="https://medusajs.com">Medusa</a> stands out as a flexible open source commerce platform for Node.js. It offers teams a customizable backend, admin tools, and a modern storefront, all without locking you into a strict SaaS model. This makes it ideal for teams who want to move quickly and keep control over their architecture.</p>
<p>Running Medusa in production is more than just starting a single process. The real challenge is keeping the entire commerce stack fast, organized, and easy to update, especially when you have a backend, storefront, admin UI, image optimization, internal networking, and Kubernetes involved.</p>
<p>This is where using a Watt monorepo really helps.</p>
<p><a href="https://www.platformatichq.com/watt">Watt</a> is Platformatic’s tool for combining multiple Node.js apps into one deployable unit by running them as worker threads under a single process.</p>
<p>Medusa can be deployed in a Kubernetes environment. To manage, monitor, and optimize your application in this setting, you can use the <a href="https://icc.platformatic.dev">Intelligent Command Center (ICC)</a>. ICC is a sophisticated cloud control plane that provides intelligent management, monitoring, and optimization of cloud-native applications deployed in Kubernetes environments. ICC offers enterprise-grade features for application lifecycle management, intelligent autoscaling, compliance monitoring, and comprehensive observability.</p>
<p>For basic deployment, simply running Watt on Kubernetes is sufficient.</p>
<p>Rather than spreading complexity across multiple repos, custom Dockerfiles, and manual service connections, you can keep everything in one workspace and let Watt manage it as a single platform. This gives you one dependency graph, one build process, one deployment artifact, and a single place to manage the rules that keep your system running smoothly.</p>
<p>In this post, we will look at a working <a href="https://medusajs.com">Medusa</a> setup deployed on <a href="https://icc.platformatic.dev">ICC</a> with:</p>
<ul>
<li><p><code>web/backend</code>: Medusa backend via <code>@platformatic/node</code></p>
</li>
<li><p><code>web/frontend</code>: Medusa Next.js starter via <code>@platformatic/next</code></p>
</li>
<li><p><code>web/gateway</code>: public routing via <code>@platformatic/gateway</code></p>
</li>
<li><p><code>image-server</code>: a dedicated <code>@platformatic/next</code> image optimizer application that reuses the same codebase as <code>web/frontend</code></p>
</li>
</ul>
<p>This set-up can be both far easier to manage <em>and</em> more performant. Let’s explore.</p>
<h2><strong>Why a monorepo is a good fit for Medusa</strong></h2>
<p>Medusa already pushes you toward a multi-application architecture. Even in a relatively standard deployment, you are dealing with:</p>
<ul>
<li><p>a backend API</p>
</li>
<li><p>an admin UI</p>
</li>
<li><p>a storefront</p>
</li>
<li><p>image optimization</p>
</li>
<li><p>environment variables shared across services</p>
</li>
<li><p>public and internal URLs that must stay aligned</p>
</li>
</ul>
<p>You can spread these parts across different repositories and deployment pipelines, but as soon as you do, even simple changes become complicated.</p>
<p>For example, changing a base path means updating several repos. Keeping React versions consistent gets harder. Coordinating Docker changes turns into a big release task. Even figuring out if the storefront is calling the right backend can take more effort than it should.</p>
<p>With Watt, the monorepo becomes the control plane for the whole stack.</p>
<ul>
<li><p>Each application stays isolated as a worker thread with Watt.</p>
</li>
<li><p>The whole platform is configured in one place.</p>
</li>
<li><p>Internal service discovery comes for free.</p>
</li>
<li><p>Deployment stays a single build and a single runtime entry point.</p>
</li>
</ul>
<p>This approach gives you the best of both worlds: separation where it matters, and simplicity where you want it.</p>
<h2><strong>The workspace layout</strong></h2>
<p>The sample project is structured like this:</p>
<pre><code class="language-plaintext">.
|-- package.json
|-- pnpm-workspace.yaml
|-- watt.json
`-- web
    |-- backend
    |   |-- medusa-config.ts
    |   |-- package.json
    |   |-- url-handler.js
    |   `-- watt.json
    |-- frontend
    |   |-- next.config.js
    |   |-- package.json
    |   |-- watt.image-optimizer.json
    |   |-- watt.json
    |   `-- src
    `-- gateway
        |-- package.json
        `-- watt.json
</code></pre>
<p>At the root, <code>watt.json</code> autoloads the <code>web/*</code> applications, sets <code>gateway</code> as the public entrypoint, and adds an extra application called <code>image-server</code> that reuses the frontend codebase with a different config.</p>
<p>This is where the monorepo model really shines. You can easily reuse the same codebase for different runtime roles. There’s no need to create a second Next.js project just to separate <code>/_next/image</code>. Instead, you keep one frontend codebase and let Watt run it in two different ways.</p>
<h2><strong>pnpm workspace setup: one dependency graph, fewer surprises</strong></h2>
<p>If you use pnpm, make the workspace explicit with <code>pnpm-workspace.yaml</code>:</p>
<pre><code class="language-plaintext">packages:
 - web/*
</code></pre>
<p>Then pin the React family at the root in <code>package.json</code>:</p>
<pre><code class="language-json">{
 "pnpm": {
   "overrides": {
     "react": "19.0.4",
     "react-dom": "19.0.4",
     "@types/react": "19.0.4",
     "@types/react-dom": "19.0.4"
   }
 }
}
</code></pre>
<p>This is a clear reason why using a monorepo matters. The Medusa storefront, Next.js, and related tools all rely on React. In a multi-repo setup, versions can easily get out of sync. With a Watt monorepo, you set the version once at the root, and every app benefits right away.</p>
<p>This makes building more predictable and keeps maintenance costs much lower.</p>
<h2><strong>One .env, clear public and internal boundaries</strong></h2>
<p>The root <code>.env</code> needs a few shared values:</p>
<ul>
<li><p><code>REDIS_HOST</code></p>
</li>
<li><p><code>MEDUSA_PUBLIC_BACKEND_URL</code></p>
</li>
<li><p><code>MEDUSA_BACKEND_URL</code></p>
</li>
</ul>
<p>The key distinction is this:</p>
<ul>
<li><p><code>MEDUSA_PUBLIC_BACKEND_URL</code> is for the externally visible backend URL</p>
</li>
<li><p><code>MEDUSA_BACKEND_URL</code> is for server-side calls from the frontend</p>
</li>
</ul>
<p>On ICC, this is the ideal setup:</p>
<pre><code class="language-plaintext">MEDUSA_PUBLIC_BACKEND_URL=https://medusa.plt/backend
MEDUSA_BACKEND_URL=http://backend.plt.local
</code></pre>
<p>Why it matters:</p>
<ul>
<li><p>browsers and the admin UI use the public backend URL</p>
</li>
<li><p>The frontend server uses <code>http://backend.plt.local</code> and stays on the Platformatic mesh.</p>
</li>
</ul>
<p>It’s worth emphasizing that second point, since it provides both great DevEx and a substantial performance boost. Thanks to Watt and inter-thread communication, server-side requests skip the public gateway and stay within the process’s internal network.</p>
<p>Once again, the monorepo helps here. The internal service name and public URL strategy are side-by-side in the same workspace, making them much harder to misconfigure.</p>
<h2><strong>Backend: run Medusa as a Watt application</strong></h2>
<p>In <code>web/backend/package.json</code>, add <code>@platformatic/node</code>:</p>
<pre><code class="language-json">{
 "dependencies": {
   "@platformatic/node": "^3.44.0"
 }
}
</code></pre>
<p>Then configure <code>web/backend/watt.json</code>:</p>
<pre><code class="language-json">{
 "$schema": "https://schemas.platformatic.dev/@platformatic/node/3.44.0.json",
 "application": {
   "basePath": "/backend",
   "commands": {
     "development": "npm run dev",
     "build": "npm run build",
     "production": "npm run start"
   },
   "changeDirectoryBeforeExecution": false,
   "entrypointPort": 3000
 },
 "node": {
   "disableBuildInDevelopment": true,
   "dispatchViaHttp": true,
   "absoluteUrl": true
 },
 "watch": false
}
</code></pre>
<p>This setup gives Medusa a clear application boundary within the workspace, while still allowing the gateway to publish it under <code>/backend</code>.</p>
<p>The companion change in <code>web/backend/medusa-config.ts</code> is just as important:</p>
<pre><code class="language-typescript">import { defineConfig, loadEnv } from '@medusajs/framework/utils'

loadEnv(process.env.NODE_ENV || 'development', process.cwd())

module.exports = defineConfig({
 projectConfig: {
   databaseUrl: process.env.DATABASE_URL,
   http: {
     storeCors: process.env.STORE_CORS!,
     adminCors: process.env.ADMIN_CORS!,
     authCors: process.env.AUTH_CORS!,
     jwtSecret: process.env.JWT_SECRET || 'supersecret',
     cookieSecret: process.env.COOKIE_SECRET || 'supersecret'
   },
   cookieOptions: {
     sameSite: 'lax',
     secure: false
   }
 },
 admin: {
   path: (new URL(process.env.MEDUSA_PUBLIC_BACKEND_URL!).pathname + '/app') as `/string`,
   backendUrl: process.env.MEDUSA_PUBLIC_BACKEND_URL,
   vite: config =&gt; {
     config.server.allowedHosts ??= []
     config.server.allowedHosts.push('.plt.local')
   }
 }
})
</code></pre>
<p>The admin path comes from the public backend URL. So, if ICC publishes the backend at <code>/backend</code>, the admin will automatically be available at <code>/backend/app</code>.</p>
<p>You should also keep <code>web/backend/url-handler.js</code> in place. Medusa’s API and admin UI do not behave identically when you put them behind a prefixed public path, so Watt’s gateway uses this file to rewrite requests correctly.</p>
<p>The implementation used in the sample project looks like this:</p>
<pre><code class="language-javascript">const basePath = process.env.PLT_BASE_PATH ?? ''
const adminPath = new URL(process.env.MEDUSA_PUBLIC_BACKEND_URL).pathname.replace(/\/$/, '')
const adminUiPath = adminPath + '/app'
const adminMatcher = new RegExp(`^${adminPath}`)

export default {
 preRewrite(url) {
   if (basePath &amp;&amp; !url.startsWith(basePath)) {
     url = `\({basePath}\){url}`
   }

   url = url.startsWith(adminUiPath) ? url : url.replace(adminMatcher, '')
   return url
 }
}
</code></pre>
<p>This file may be small, but it does important work. It keeps the admin UI path intact while removing the backend prefix for API routes that Medusa expects to serve from the root.</p>
<h2><strong>Frontend: one codebase, two runtime roles</strong></h2>
<p>In <code>web/frontend/package.json</code>, add <code>@platformatic/next</code>:</p>
<pre><code class="language-json">{
 "dependencies": {
   "@platformatic/next": "^3.44.0"
 }
}
</code></pre>
<p>The standard frontend config in <code>web/frontend/watt.json</code> is simple:</p>
<pre><code class="language-json">{
 "$schema": "https://schemas.platformatic.dev/@platformatic/next/3.44.0.json",
 "application": {
   "basePath": "{PLT_BASE_PATH}",
   "changeDirectoryBeforeExecution": true
 },
 "next": {
   "trailingSlash": true
 }
}
</code></pre>
<p>And in <code>web/frontend/next.config.js</code>, set:</p>
<pre><code class="language-javascript">const nextConfig = {
 reactStrictMode: true,
 logging: {
   fetches: {
     fullUrl: true
   }
 },
 eslint: {
   ignoreDuringBuilds: true
 },
 typescript: {
   ignoreBuildErrors: true
 }
}
</code></pre>
<p>Here’s where it gets interesting: the monorepo lets you reuse the same frontend codebase as a dedicated image optimization service, with almost no extra work.</p>
<h2><strong>Split image optimization without splitting the repo</strong></h2>
<p>We recently covered why this architecture matters in our post on <a href="https://blog.platformatic.dev/scale-nextjs-image-optimization-platformatic">scaling Next.js image optimization with a dedicated Platformatic application</a>: image optimization is CPU-heavy and can become a noisy neighbour for SSR traffic.</p>
<p>That is exactly why this Medusa setup runs <code>/_next/image</code> separately.</p>
<p>Create <code>web/frontend/watt.image-optimizer.json</code>:</p>
<pre><code class="language-json">{
 "$schema": "https://schemas.platformatic.dev/@platformatic/next/3.44.0.json",
 "logger": {
   "level": "trace"
 },
 "application": {
   "basePath": "/",
   "changeDirectoryBeforeExecution": true
 },
 "next": {
   "trailingSlash": true,
   "imageOptimizer": {
     "enabled": true,
     "fallback": "frontend",
     "timeout": 30000,
     "ttl": 3600000,
     "maxAttempts": 3,
     "storage": {
       "type": "valkey",
       "url": "{REDIS_HOST}"
     }
   }
 }
}
</code></pre>
<p>This is a great example of why Watt monorepos work so well.</p>
<ul>
<li><p>You reuse the same frontend app.</p>
</li>
<li><p>You keep one source tree.</p>
</li>
<li><p>You give it a second runtime role.</p>
</li>
<li><p>You isolate a CPU-heavy path without creating a second frontend project.</p>
</li>
</ul>
<p>This setup improves both maintainability and performance, which is exactly what you want from your platform architecture.</p>
<p>The <code>fallback: "frontend"</code> setting is especially nice here: relative image URLs are resolved through the main storefront service over the runtime network, so the optimizer stays tightly integrated without being coupled to the frontend worker pool.</p>
<h2><strong>Next.js build-time pragmatism: force dynamic where it helps</strong></h2>
<p>Because the Medusa backend is not available during the <code>wattpm build</code>, the storefront cannot pre-generate some pages safely.</p>
<p>For these files:</p>
<ul>
<li><p><code>web/frontend/src/app/[countryCode]/(main)/products/[handle]/page.tsx</code></p>
</li>
<li><p><code>web/frontend/src/app/[countryCode]/(main)/categories/[...category]/page.tsx</code></p>
</li>
<li><p><code>web/frontend/src/app/[countryCode]/(main)/collections/[handle]/page.tsx</code></p>
</li>
</ul>
<p>comment out <code>generateStaticParams</code> and add:</p>
<pre><code class="language-javascript">export const dynamic = 'force-dynamic'
</code></pre>
<p>This uses Next.js <a href="https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config">Route Segment Config</a> to force runtime rendering instead of static generation.</p>
<p>In a typical Next.js app, this might seem like a compromise. But in this setup, it’s the right choice. The storefront relies on live Medusa data, and Watt provides that backend at runtime.</p>
<p>This is another area where the monorepo helps. The build behaviour is clear because the backend and frontend are in the same workspace, and their dependencies are easy to see.</p>
<h2><strong>Gateway: one public surface for the whole stack</strong></h2>
<p>Add <code>@platformatic/gateway</code> in <code>web/gateway/package.json</code>:</p>
<pre><code class="language-json">{
 "dependencies": {
   "@platformatic/gateway": "^3.44.0"
 }
}
</code></pre>
<p>Then define <code>web/gateway/watt.json</code> like this:</p>
<pre><code class="language-json">{
 "$schema": "https://schemas.platformatic.dev/@platformatic/gateway/3.44.0.json",
 "gateway": {
   "applications": [
     {
       "id": "backend",
       "proxy": {
         "prefix": "/backend",
         "custom": {
           "path": "../backend/url-handler.js"
         }
       }
     },
     {
       "id": "frontend",
       "proxy": {
         "prefix": "/"
       }
     },
     {
       "id": "image-server",
       "proxy": {
         "prefix": "/",
         "routes": ["/_next/image", "/_next/image/*"],
         "methods": ["GET"]
       }
     }
   ]
 }
}
</code></pre>
<p>This is where the monorepo approach really starts to feel smooth and efficient.</p>
<ul>
<li><p><code>/backend</code> goes to Medusa</p>
</li>
<li><p><code>/</code> goes to the storefront</p>
</li>
<li><p><code>GET /_next/image</code> goes to the image optimizer</p>
</li>
</ul>
<p>Thanks to <code>@platformatic/gateway</code>, you get one public entry point, but the traffic still lands on the right internal application.</p>
<p>This setup is easier to understand, change, and scale than trying to connect separate services outside the repo.</p>
<h2><strong>A small middleware detail that improves the experience</strong></h2>
<p>There is another subtle optimization in the storefront middleware <code>(web/frontend/src/middleware.ts)</code>.</p>
<p>When the request already contains a country code in the URL but does not yet have the <code>medusacache_id</code> cookie, the middleware sets that cookie and returns <code>NextResponse.next()</code> instead of forcing another redirect.</p>
<p>It’s a small detail, but it’s the kind of optimization that’s easier to maintain in a monorepo. Storefront routing, Medusa region lookups, and platform-level caching thanks to Watt HTTP caching handling are all managed together.</p>
<p>In practice, this helps the storefront set up its region-aware state smoothly, without extra steps.</p>
<p>The change is small enough to think of as a focused patch:</p>
<pre><code class="language-typescript"> if (urlHasCountryCode &amp;&amp; !cacheIdCookie) {
+    const response = NextResponse.next()

   response.cookies.set('_medusa_cache_id', cacheId, {
     maxAge: 60 * 60 * 24
   })

   return response
 }
</code></pre>
<p>This is the kind of practical improvement that’s easier to maintain when routing logic, storefront behaviour, and platform deployment are all in the same repo.</p>
<h2><strong>ICC environment values</strong></h2>
<p>In <code>.env.icc</code>, the main settings to align are:</p>
<pre><code class="language-plaintext">MEDUSA_PUBLIC_BACKEND_URL=https://medusa.plt/backend
STORE_CORS=https://docs.medusajs.com,https://medusa.plt
ADMIN_CORS=https://docs.medusajs.com,https://medusa.plt
AUTH_CORS=https://docs.medusajs.com,https://medusa.plt
NEXT_PUBLIC_BASE_URL=https://medusa.plt
</code></pre>
<p>They all reflect the same core rule: the whole application is published under <code>/medusa</code>, so both Medusa and Next.js need to agree on that public shape.</p>
<p>Since these settings are in one workspace and one deployment artifact, keeping them in sync is much easier than with a split-repo setup.</p>
<h2><strong>The Docker build is simple because the repo is simple</strong></h2>
<p>The container image is straightforward:</p>
<pre><code class="language-plaintext">FROM node:22-alpine

# Environment setup
ENV APP_HOME=/home/app/node/
ENV PLT_BASE_PATH="/medusa"
ENV PLT_ICC_URL="http://icc.platformatic.svc.cluster.local"
WORKDIR $APP_HOME

# Install dependencies
RUN npm install -g pnpm wattpm-utils "@platformatic/watt-extra@latest"
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml $APP_HOME
RUN pnpm install --frozen-lockfile --node-linker=hoisted

# Copy application
COPY web $APP_HOME/web
COPY .env.icc watt.json $APP_HOME
RUN mv .env.icc .env
RUN pnpm run build

# Final setup
EXPOSE 3042
EXPOSE 9090
CMD ["watt-extra", "start"]
</code></pre>
<p>There are two details worth mentioning.</p>
<p>First, using <code>--node-linker=hoisted</code> with pnpm installs dependencies in a flatter layout, instead of the usual symlink-heavy structure. In a workspace with Medusa, Next.js, shared React versions, and several Watt apps, this makes module resolution more predictable and helps avoid compatibility issues during container builds.</p>
<p>Second, <code>@platformatic/watt-extra</code> is a helper CLI that starts Watt smoothly in container environments like ICC. It adds the operational support you need at runtime, so your container entrypoint remains simple.</p>
<p>This is another area where the monorepo pays off right away: you have one install step, one build step, and one runtime command.</p>
<h2><strong>Why does this feel better to maintain</strong></h2>
<p>The main advantage of this Medusa setup isn’t any single config file. It’s the overall structure:</p>
<ul>
<li><p>One repo for backend, frontend, gateway, and optimizer</p>
</li>
<li><p>One dependency strategy</p>
</li>
<li><p>One place to define public and internal URLs</p>
</li>
<li><p>One deployment artifact for Kubernetes and ICC</p>
</li>
<li><p>One runtime that still preserves application boundaries</p>
</li>
</ul>
<p>Since Watt sees the platform as a group of coordinated apps, you can make performance improvements without making the system harder to manage.</p>
<p>You can send image optimization to a dedicated service, keep frontend-to-backend calls on the mesh network, mount everything under a base path, and update all these rules in one place.</p>
<p>That’s the real value of running Medusa in a Watt monorepo on ICC: convenience and performance work together, instead of getting in each other’s way. Because ICC provides a <strong>Kubernetes (K8S)</strong>-native environment, your monorepo and its services benefit from K8s's inherent scalability, resilience, and orchestration capabilities. This integration ensures that deploying and managing Medusa within the Watt monorepo is seamless, leveraging the enterprise-grade infrastructure of ICC (which is built on K8S) for optimal operational efficiency.</p>
<p>If you’re building commerce systems with lots of moving parts, this is the kind of platform setup you want.</p>
]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Introducing Regina: Stateful AI Agent Orchestration for Platformatic Watt]]></title>
      <description><![CDATA[We’re excited to share Regina, a production-ready agent orchestration layer built on Platformatic Watt.
Regina lets you go from single-agent demos to real systems you can run and scale confidently. Yo]]></description>
      <link>https://blog.platformatic.dev/introducing-regina-stateful-ai-agent-orchestration-watt</link>
      <guid isPermaLink="true">https://blog.platformatic.dev/introducing-regina-stateful-ai-agent-orchestration-watt</guid>
      <category><![CDATA[AI]]></category>
      <category><![CDATA[ai agents]]></category>
      <category><![CDATA[Node.js]]></category>
      <category><![CDATA[platformatic]]></category>
      <dc:creator><![CDATA[Paolo Insogna]]></dc:creator>
      <pubDate>Tue, 14 Apr 2026 14:30:00 GMT</pubDate>
      <enclosure url="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/b492a6aa-6050-460c-be39-465c2c437bad.png" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p>We’re excited to share Regina, a production-ready agent orchestration layer built on <a href="https://docs.platformatic.dev/">Platformatic Watt</a>.</p>
<p>Regina lets you go from single-agent demos to real systems you can run and scale confidently. You define agents in Markdown, start instances over HTTP, and get built-in lifecycle management, persistence, and recovery, all by running and managing agents in Watt as isolated worker threads.</p>
<h2>Why Regina, why now</h2>
<p>Most AI projects hit the same wall after the first demo:</p>
<ul>
<li><p>Prompts are not versioned in a clean, operational way.</p>
</li>
<li><p>Sessions disappear on restart.</p>
</li>
<li><p>Scaling introduces routing and state headaches.</p>
</li>
<li><p>Tool-heavy workflows are hard to observe and control.</p>
</li>
</ul>
<p>Regina solves these problems directly, so your team can focus on building your product and not re-inventing the wheel when it comes to complex orchestration and state management.</p>
<h2>What you get on day one</h2>
<p>Regina comes as three packages:</p>
<ul>
<li><p><code>@platformatic/regina</code>: per-pod agent manager</p>
</li>
<li><p><code>@platformatic/regina-agent</code>: per-agent runtime</p>
</li>
<li><p><code>@platformatic/regina-storage</code>: pluggable backup adapters (<code>fs, s3, redis</code>)</p>
</li>
</ul>
<p>With this stack, you’ll have:</p>
<ul>
<li><p>stateful agent instances with per-instance SQLite VFS <em>(“Virtual File System”)</em></p>
</li>
<li><p>suspend/resume lifecycle management with idle timeout control</p>
</li>
<li><p>NDJSON streaming events for full run visibility</p>
</li>
<li><p>steerable agentic loops via <code>POST /instances/:id/steer</code></p>
</li>
<li><p>storage-backed restore for resilient multi-pod operation</p>
</li>
</ul>
<h2><strong>Markdown-native agent definitions</strong></h2>
<p>Regina uses Markdown with YAML frontmatter as the main source for each agent.</p>
<pre><code class="language-plaintext">---
name: support-agent
description: Customer support assistant
model: anthropic/claude-sonnet-4-5
provider: vercel-gateway
tools:
 - ./tools/search-docs.ts
temperature: 0.3
maxSteps: 10
---
You are a helpful support agent.
</code></pre>
<p>This setup keeps prompt and runtime configuration together, so it’s easy to review in pull requests and update across teams.</p>
<h2><strong>Built for real runtime behaviour</strong></h2>
<p>Regina keeps management and execution separate:</p>
<ul>
<li><p><code>@platformatic/regina</code> discovers definitions, spawns instances, and proxies instance APIs</p>
</li>
<li><p><code>@platformatic/regina-agent</code> runs each instance in isolation</p>
</li>
<li><p>message history is persisted at <code>/.session/messages.jsonl</code> in each instance VFS</p>
</li>
</ul>
<p>This design gives you reliable performance, even under heavy load:</p>
<ul>
<li><p><strong>Idle suspension</strong> to free resources automatically</p>
</li>
<li><p><strong>Auto-resume</strong> on next request</p>
</li>
<li><p><strong>State continuity</strong> across restarts</p>
</li>
<li><p><strong>Rich streaming</strong> <code>(text-delta, tool-call, tool-result, step-finish</code>)</p>
</li>
</ul>
<p>In practice, running your agents with Regina and Watt gives you agents that act like durable workflows backed by persistent state instead of ephemeral, one-off chat sessions.</p>
<h2><strong>Start simple and scale smoothly</strong></h2>
<p>Regina works great in single-pod mode with no Redis, no external storage, and minimal setup.</p>
<p>As your traffic grows, you can add Redis or Valkey for member and instance mapping, and add shared storage for state restore if needed. The API stays the same, so clients don’t need to change as your setup evolves.</p>
<h2><strong>Getting started</strong></h2>
<p>The Regina demo app is small on purpose, but it shows the full production pattern in a single repo:</p>
<ul>
<li><p><code>watt.json</code> at the root defines a single entrypoint service for Regina</p>
</li>
<li><p><code>services/regina/watt.json</code> enables <code>@platformatic/regina</code> and points to the shared <code>agents/</code> directory</p>
</li>
<li><p>Each file in <code>agents/</code> is a full agent definition <em>(prompt + model + provider + tools)</em></p>
</li>
<li><p>Custom tools sit alongside agents in <code>agents/tools/*</code></p>
</li>
</ul>
<p>Here’s how the demo app is set up.</p>
<p>Root <code>watt.json</code>:</p>
<pre><code class="language-json">{
 "$schema": "https://schemas.platformatic.dev/wattpm/3.50.0.json",
 "server": {
   "port": 3042
 },
 "management": true,
 "entrypoint": "regina",
 "services": [
   {
     "id": "regina",
     "path": "./services/regina",
     "management": {
       "operations": ["addApplications", "removeApplications", "getApplications", "getApplicationDetails", "inject"]
     }
   }
 ]
}
</code></pre>
<p>Service <code>services/regina/watt.json</code>:</p>
<pre><code class="language-json">{
 "$schema": "https://schemas.platformatic.dev/@platformatic/regina/0.1.0.json",
 "regina": {
   "agentsDir": "../../agents"
 }
}
</code></pre>
<p>Example agent definition (<code>agents/assistant.md</code>):</p>
<pre><code class="language-plaintext">---
name: assistant
description: A general-purpose assistant with file and shell access
model: anthropic/claude-sonnet-4-5
provider: vercel-gateway
greeting: "Hi! I'm a general-purpose assistant. I can read and write files, run commands, and help with any task."
temperature: 0.3
maxSteps: 15
---
You are a helpful assistant. You can read, write, and edit files, run bash commands, and help with any task.
</code></pre>
<p>Here’s a typical flow in the demo:</p>
<ol>
<li><p>Start Watt (<code>wattpm start</code>).</p>
</li>
<li><p>Create an instance from an agent definition (<code>POST /agents/:defId/instances</code>).</p>
</li>
<li><p>Chat with that instance (<code>POST /instances/:instanceId/chat or /chat/stream</code>).</p>
</li>
<li><p>Resume the same instance later with history already available.</p>
</li>
</ol>
<p>This is important because it shows Regina’s core value from start to finish: agents are defined as code, run as managed instances, and keep their state across requests without extra orchestration work.</p>
<h2><strong>Storage options for state backup</strong></h2>
<p>For multi-pod setups, configure <a href="http://regina.storage">regina.storage</a> so you can restore on another pod.</p>
<p><strong>Filesystem (</strong><code>fs</code><strong>)</strong></p>
<pre><code class="language-json">{
 "module": "@platformatic/regina",
 "regina": {
   "storage": {
     "type": "fs",
     "basePath": "/mnt/shared/regina-state"
   }
 }
}
</code></pre>
<p><strong>Object storage (</strong><code>s3</code><strong>)</strong></p>
<pre><code class="language-json">{
 "module": "@platformatic/regina",
 "regina": {
   "storage": {
     "type": "s3",
     "bucket": "regina-state",
     "prefix": "backups/",
     "endpoint": "https://s3.amazonaws.com"
   }
 }
}
</code></pre>
<p><strong>Redis (</strong><code>redis</code><strong>)</strong></p>
<pre><code class="language-json">{
 "module": "@platformatic/regina",
 "regina": {
   "redis": "redis://valkey:6379",
   "storage": {
     "type": "redis"
   }
 }
}
</code></pre>
<p>All adapters use the same interface (<code>put, get, delete, list, close</code>), so you can switch backends without changing how your clients work.</p>
<h2><strong>Get started</strong></h2>
<ul>
<li><p><a href="https://github.com/platformatic/regina">Regina repository</a></p>
</li>
<li><p><a href="https://github.com/platformatic/regina/tree/main/packages/regina">Regina package docs</a></p>
</li>
<li><p><a href="https://github.com/platformatic/regina/tree/main/packages/regina-agent">Agent runtime docs</a></p>
</li>
<li><p><a href="https://github.com/platformatic/regina/tree/main/packages/regina-storage">Storage adapters docs</a></p>
</li>
</ul>
<p>Regina is built for teams shipping serious AI systems on Node.js. If you need agents that are reliable, observable, and stateful in production, Regina is ready for you.</p>
]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[@platformatic/kafka Now Supports Confluent Schema Registry ]]></title>
      <description><![CDATA[If you run Kafka in production, you can’t skip schema evolution. Teams need clear data types, compatibility checks, and a safe way to update contracts without breaking consumers or downstream services]]></description>
      <link>https://blog.platformatic.dev/platformatic-kafka-confluent-schema-registry-support</link>
      <guid isPermaLink="true">https://blog.platformatic.dev/platformatic-kafka-confluent-schema-registry-support</guid>
      <category><![CDATA[kafka]]></category>
      <category><![CDATA[Node.js]]></category>
      <category><![CDATA[Devops]]></category>
      <category><![CDATA[json]]></category>
      <dc:creator><![CDATA[Paolo Insogna]]></dc:creator>
      <pubDate>Tue, 07 Apr 2026 14:30:00 GMT</pubDate>
      <enclosure url="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/ef50f002-fb0e-47aa-aeac-23d7be4891f5.png" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p>If you run Kafka in production, you can’t skip schema evolution. Teams need clear data types, compatibility checks, and a safe way to update contracts without breaking consumers or downstream services.</p>
<p>Before now, using <code>@platformatic/kafka</code> with Confluent Schema Registry meant writing extra code to connect the pieces. With <code>@platformatic/kafka</code> <strong>v1.27.0</strong>, that’s no longer needed.</p>
<p><code>@platformatic/kafka</code> now has built-in support for Confluent Schema Registry, including:</p>
<ul>
<li><p>AVRO</p>
</li>
<li><p>Protocol Buffers</p>
</li>
<li><p>JSON Schema</p>
</li>
<li><p>Basic and Bearer authentication</p>
</li>
<li><p>Automatic schema fetch and caching</p>
</li>
<li><p>Integrated Producer and Consumer hooks</p>
</li>
</ul>
<p>You get schema-aware messaging, and the project still focuses on being fast and predictable for Node.js Kafka clients.</p>
<h2><strong>Why This Matters</strong></h2>
<p>Most schema registry integrations add complexity where you don’t want it: in the message serialization and deserialization paths. Fetching remote schemas is asynchronous, but encoding and decoding should stay synchronous for speed and consistency.</p>
<p>Put simply, network I/O and cache coordination should happen before the main data processing, not during it. Keeping these steps separate helps maintain stable throughput and latency as traffic increases.</p>
<p>This release introduces a two-layer architecture to keep that separation clear:</p>
<ol>
<li><p><strong>Low-level hooks</strong> for async pre-processing:</p>
<ul>
<li><p><a href="https://github.com/platformatic/kafka/blob/main/docs/producer.md">beforeSerialization</a></p>
</li>
<li><p><a href="https://github.com/platformatic/kafka/blob/main/docs/consumer.md">beforeDeserialization</a></p>
</li>
</ul>
</li>
<li><p><strong>High-level registry API</strong> via <a href="https://github.com/platformatic/kafka/blob/main/docs/confluent-schema-registry.md">ConfluentSchemaRegistry</a></p>
</li>
</ol>
<p>In practice, this means schemas are fetched and cached before encode/decode happens, so your serializers and deserializers stay synchronous when messages are processed.</p>
<p>This gives application teams a simpler way to think about things: do the asynchronous prep first, then keep codec behavior predictable during main processing.</p>
<p>At a high level, the flow is:</p>
<ul>
<li><p>Extract schema ID from message metadata (producer) or wire payload (consumer).</p>
</li>
<li><p>Resolve schema from local cache when available.</p>
</li>
<li><p>On cache miss, fetch asynchronously via <code>beforeSerialization/beforeDeserialization</code> hooks and cache the schema.</p>
</li>
<li><p>Run synchronous serialization/deserialization with the resolved schema.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/043ca54f-3808-4811-8e47-b772feb30666.png" alt="" style="display:block;margin:0 auto" />

<p>In multi-instance deployments, that cache layer can be backed by Redis or Valkey, so workers share schema state across nodes while keeping encode/decode synchronous in the hot path.</p>
<h2><strong>What You Can Do Now</strong></h2>
<p>You can connect a registry directly to both the Producer and Consumer, letting <code>@platformatic/kafka</code> handle schema-aware serialization from start to finish.</p>
<p>This is especially helpful when several services publish and consume the same topics on different deployment cycles, since consistent schema handling is a must.</p>
<pre><code class="language-javascript">import { Consumer, Producer } from '@platformatic/kafka'
import { ConfluentSchemaRegistry } from '@platformatic/kafka/registries'

const registry = new ConfluentSchemaRegistry({
  url: 'http://localhost:8081'
})

const producer = new Producer({
  clientId: 'orders-producer',
  bootstrapBrokers: ['localhost:9092'],
  registry
})

const consumer = new Consumer({
  groupId: 'orders-consumers',
  clientId: 'orders-consumer',
  bootstrapBrokers: ['localhost:9092'],
  registry
})
</code></pre>
<p>When producing, pass schema IDs in message metadata:</p>
<pre><code class="language-javascript">await producer.send({
  messages: [
    {
      topic: 'orders',
      key: { orderId: 101 },
      value: { customerId: 'cust-44', total: 129.99 },
      metadata: {
        schemas: {
          key: 10,
          value: 11
        }
      }
    }
  ]
})
</code></pre>
<p>When consuming, payloads are automatically decoded with the cached schema. If a schema isn’t found, the registry fetches it before deserialization continues.</p>
<p>This makes it easy to move from custom codec code to a single registry integration in your client setup.</p>
<h2><strong>Authentication and Enterprise Scenarios</strong></h2>
<p>Schema Registry deployments are often protected. The new integration includes:</p>
<ul>
<li><p>Basic auth (<code>username</code> + <code>password</code>)</p>
</li>
<li><p>Bearer token auth (<code>token</code>)</p>
</li>
<li><p>Dynamic credentials via providers</p>
</li>
</ul>
<p>This makes it easier to connect to managed or secured registry instances without writing custom transport code. It also makes credential rotation simpler when you use providers.</p>
<p>If your setup uses short-lived credentials, provider functions let you refresh tokens and secrets without having to rebuild your producer or consumer logic.</p>
<h2><strong>Performance and Reliability Considerations</strong></h2>
<p>One main design goal was to avoid unnecessary overhead to message processing.</p>
<p>The implementation focuses on cache locality and step-by-step pre-processing:</p>
<ul>
<li><p>Schema IDs are extracted from the wire format <em>(or message metadata).</em></p>
</li>
<li><p>Unknown schemas are fetched once and cached.</p>
</li>
<li><p>Repeated schema IDs in a batch are resolved from the cache.</p>
</li>
<li><p>Encode/decode continues in synchronous paths.</p>
</li>
</ul>
<p>This setup cuts down on unnecessary async work while still supporting remote schema registries safely. It also helps keep throughput and performance steady, as you’d expect from a Node.js client.</p>
<p>Operationally, this also makes failures easier to understand. Schema resolution errors happen during fetch or preparation, while codec errors are still linked to payload and schema compatibility.</p>
<h2><strong>Also Included in This Release</strong></h2>
<p>The v1.27.0 release also shipped quality improvements around consumer behaviour and protocol handling, with broad test coverage and new playground clients for:</p>
<ul>
<li><p>AVRO</p>
</li>
<li><p>Protobuf</p>
</li>
<li><p>JSON Schema</p>
</li>
<li><p>Authenticated Schema Registry setups</p>
</li>
</ul>
<p>The end result is a production-ready integration you can try out quickly, starting in local development and moving to secure production registries.</p>
<h2><strong>Experimental API Notice</strong></h2>
<p><code>ConfluentSchemaRegistry</code> and its related hooks are currently <strong>experimental</strong>. They may change in minor or patch releases as we keep improving them based on real-world use and feedback.</p>
<p>If you plan to use this in production, make sure to pin your versions and check the release notes. We’ll keep refining the API based on feedback from real deployments.</p>
<p>If your team is rolling this out, here’s a practical way to start:</p>
<ol>
<li><p>Start with one topic and one schema format <em>(typically AVRO or JSON Schema)</em></p>
</li>
<li><p>Validate serialization/deserialization behaviour in staging with real payloads.</p>
</li>
<li><p>Expand topic coverage and introduce auth/credential providers as needed.</p>
</li>
</ol>
<h2><strong>Getting Started</strong></h2>
<p>Install the package:</p>
<pre><code class="language-plaintext">npm install @platformatic/kafka
</code></pre>
<p>For Protobuf support, also install:</p>
<pre><code class="language-plaintext">npm install protobufjs
</code></pre>
<p>Next, follow the full integration guide in the documentation:</p>
<ul>
<li><p><a href="https://github.com/platformatic/kafka/blob/main/docs/confluent-schema-registry.md">Confluent Schema Registry docs</a></p>
</li>
<li><p><a href="https://github.com/platformatic/kafka/releases/tag/v1.27.0">v1.27.0 release notes</a></p>
</li>
</ul>
<p>If you give it a try, we’d love to hear your feedback at <a href="mailto:hello@platformatic.dev">hello@platformatic.dev</a>. Real-world schema workflows will help shape the next version of this API and guide our priorities for future improvements.</p>
<p>Thanks for building with us! 🚀</p>
]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[React SSR Framework Showdown: TanStack Start, React Router, and Next.js Under Load]]></title>
      <description><![CDATA[Performance benchmarks capture a moment, not a final judgment. Results depend on a specific workload, scale, and constraints; they do not rank frameworks by value. Next.js stands out for its widesprea]]></description>
      <link>https://blog.platformatic.dev/react-ssr-framework-benchmark-tanstack-start-react-router-nextjs</link>
      <guid isPermaLink="true">https://blog.platformatic.dev/react-ssr-framework-benchmark-tanstack-start-react-router-nextjs</guid>
      <category><![CDATA[Next.js]]></category>
      <category><![CDATA[react router]]></category>
      <category><![CDATA[Node.js]]></category>
      <category><![CDATA[Kubernetes]]></category>
      <dc:creator><![CDATA[Matteo Collina]]></dc:creator>
      <pubDate>Tue, 17 Mar 2026 14:30:00 GMT</pubDate>
      <enclosure url="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/33f199bf-9079-481b-85b2-d0cc6421f29d.png" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<blockquote>
<p>Performance benchmarks capture a moment, not a final judgment. Results depend on a specific workload, scale, and constraints; they do not rank frameworks by value. Next.js stands out for its widespread adoption, strong compatibility, and vast ecosystem trusted by millions. TanStack, as a newcomer, made bold architectural choices. React Router is positioned differently along the maturity curve. Each framework wins in its own context.</p>
<p>The numbers matter less than the response: every team addressed our shared data and delivered fixes. This collaboration with open data, shared flamegraphs, and upstream fixes makes Node.js a safe, long-term choice for enterprise teams.</p>
</blockquote>
<p><strong><mark class="bg-yellow-200 dark:bg-yellow-500/30">We updated our Benchmarks! View the new numbers </mark></strong> <a href="https://blog.platformatic.dev/ssr-framework-benchmarks-v2-corrected-results"><strong><mark class="bg-yellow-200 dark:bg-yellow-500/30">Here</mark></strong></a></p>
<h2>TL;DR</h2>
<p>With help from Claude Code, we built the same eCommerce app in three SSR frameworks and tested them at 1,000 requests per second on AWS EKS. We ran each framework both on Watt and directly on Kubernetes.</p>
<p>The results revealed big performance differences and highlighted a few key themes:</p>
<ol>
<li><p>Running Node services on Watt improves average latency.</p>
</li>
<li><p>The TanStack team is doing excellent work. Their framework outperformed the others we tested by a wide margin.</p>
</li>
<li><p>The Next.js team has made impressive performance improvements. Upgrading from v15 to v16 canary more than doubled throughput and reduced latency by six times. Their collaboration also led to a 75% speedup in React’s RSC deserialization, which benefits everyone using React.</p>
</li>
</ol>
<p>Both the TanStack and Next.js team used <a href="https://github.com/platformatic/flame">platformatic/flame</a> to find and resolve critical performance bottlenecks the benchmark uncovered - more on that below.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/6cedd01d-637a-4f66-8fab-2a95866621ce.png" alt="" style="display:block;margin:0 auto" />

<p>TanStack Start outperformed React Router by 25% in throughput and had 35% lower latency. Both frameworks achieved a 100% success rate, meaning every request got an HTTP 200 response within our 10-second timeout. This strict definition makes the comparison fair and matches real-world SLA expectations. Next.js struggled under our benchmark load, but upgrading from v15.5.5 to v16.2.0-canary.66 more than doubled its throughput (from 322 to 701 requests per second) and reduced average latency by six times.</p>
<p>To mirror common enterprise eCommerce scenarios, no caching was used in this test, as it’s often avoided due to aggressive personalization and A/B testing. In many large-scale e-commerce deployments, personalization strategies ensure that individual user views have minimal overlap, often less than 5%,which means that cache hits provide minimal benefit compared to the invalidation overhead. This explicit trade-off reflects real-world scenarios, where companies choose to prioritize dynamic user experiences over the potential gains from caching.</p>
<p><strong>Collaboration note:</strong> We shared benchmark data and flamegraphs (via <a href="https://github.com/platformatic/flame">platformatic/flame</a>) with both the TanStack and Next.js teams. The TanStack team fixed a critical bottleneck, delivering a <strong>252x improvement</strong> in response times. The Next.js team’s <a href="https://x.com/timneutkens">Tim Neutkens</a> used our flamegraphs to identify a JSON.parse reviver overhead in React Server Components, resulting in a <a href="https://github.com/facebook/react/pull/35776">75% speedup in RSC deserialization</a> merged into React itself.</p>
<blockquote>
<p><em>While we run these benchmarks on a canary release of</em> <a href="http://next.js"><em>Next.js</em></a><em>, all the advantages are part of</em> <a href="http://next.js"><em>Next.js</em></a> <em>16.2.0, which is coming out very soon.</em></p>
</blockquote>
<hr />
<h2>The Challenge: Apples-to-Apples Framework Comparison</h2>
<p>Comparing SSR performance (or performance generally) across frameworks is notoriously tricky because teams tend to only write and deploy their apps to a single framework, so it’s rare to get a reasonable “like-for-like” comparison.</p>
<p>Luckily for us, we live in an era where writing code is as cheap as however many tokens it costs to generate your favorite LLM. So we made 3 (more-or-less) identical eCommerce sample applications with the help of our dear friend Claudio (feel free to check out the code for yourself <a href="https://github.com/platformatic/k8s-watt-performance-demo/tree/ecommerce">here</a>).</p>
<h3>The Application: CardMarket</h3>
<p>For these benchmarks, we built a trading card marketplace app, similar to a simpler version of <a href="https://www.tcgplayer.com/">TCGPlayer</a> or <a href="https://www.cardmarket.com/">CardMarket</a>. The data model includes 5 games (Pokémon, Magic: The Gathering, Yu-Gi-Oh!, Digimon, and One Piece), 50 card sets (10 per game), 10,000 cards (200 per set), 100 sellers with ratings and locations, and 50,000 listings with prices, conditions, and quantities.</p>
<p>The app includes several types of pages and routes to create a realistic e-commerce experience, all generated by Claude Code:</p>
<ul>
<li><p>The <strong>homepage</strong> shows featured games, trending cards, and new releases.</p>
</li>
<li><p>There’s a <strong>search page</strong> with full-text search, filtering, and pagination.</p>
</li>
<li><p><strong>Game detail</strong> pages show info about each game and its sets, while set detail pages list cards with pagination.</p>
</li>
<li><p><strong>Card detail</strong> pages display card info and seller listings.</p>
</li>
<li><p>The <strong>sellers’ list page</strong> shows all sellers with their ratings, and each seller has a profile and listings page.</p>
</li>
<li><p>There’s also a <strong>cart page</strong> with a static shopping cart.</p>
</li>
</ul>
<p><strong>We made several design choices to keep the implementations consistent:</strong></p>
<ul>
<li><p>All data comes from JSON files, and every framework uses the same data.</p>
</li>
<li><p>We added a random 1-5ms delay to simulate real database latency.</p>
</li>
<li><p>Every route uses full SSR with no client-side data fetching.</p>
</li>
<li><p>All versions share the same UI components, layouts, and Tailwind CSS styling.</p>
</li>
</ul>
<h3><strong>The Frameworks</strong></h3>
<p>We implemented this application in three frameworks:</p>
<ol>
<li><p><strong>TanStack Start</strong> (v1.157.16) - The newest entrant, built on TanStack Router with Vite for SSR</p>
</li>
<li><p><strong>React Router</strong> (v7) - The classic routing library, now with first-class SSR support.</p>
</li>
<li><p><strong>Next.js</strong> (v15, updated to v16 canary) - The established leader in React SSR</p>
</li>
</ol>
<p>Each implementation uses the framework’s idiomatic patterns:</p>
<ul>
<li><p><strong>TanStack Start</strong>: createFileRoute with loader functions</p>
</li>
<li><p><strong>React Router</strong>: Route modules with loader exports</p>
</li>
<li><p><strong>Next.js</strong>: App Router with Server Components</p>
</li>
</ul>
<h3><strong>The Runtimes</strong></h3>
<p>For each framework, we tested two runtime configurations:</p>
<ol>
<li><p><strong>Node.js</strong> - Single-threaded, 6 pods with 1 CPU allocated for each</p>
</li>
<li><p><strong>Watt</strong> - Multi-worker with SO_REUSEPORT, 3 pods with 2 CPUs allocated, with 2 workers per pod to use those 6 CPUs to the fullest</p>
</li>
</ol>
<p>All configurations received identical total CPU allocation (6 cores) for fair comparison.</p>
<hr />
<h2>Test Methodology</h2>
<h3><strong>Infrastructure</strong></h3>
<ul>
<li><p><strong>EKS Cluster:</strong> 4 nodes running m5.2xlarge instances (8 vCPUs, 32GB RAM each)</p>
</li>
<li><p><strong>Load Testing Instance:</strong> c7gn.2xlarge (8 vCPUs, 16GB RAM, network-optimized)</p>
</li>
<li><p><strong>Region:</strong> us-west-2</p>
</li>
<li><p><strong>Load Testing Tool:</strong> Grafana k6</p>
</li>
</ul>
<h3><strong>Software Versions</strong></h3>
<p>All versions are locked in package.json for reproducible benchmarks:</p>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/df858d83-3ee5-4e8a-b040-c6eca97fa21c.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>Load Test Configuration</strong></h3>
<p>Each test followed this protocol:</p>
<ol>
<li><p><strong>NLB Warm-up:</strong> 60 seconds ramping from 10 to 500 req/s</p>
</li>
<li><p><strong>Pre-test Warm-up:</strong> 20 seconds at moderate load</p>
</li>
<li><p><strong>Cool-down:</strong> 60 seconds before the main test</p>
</li>
<li><p><strong>Main Test:</strong> 60 seconds ramp-up to 1,000 req/s, then 120 seconds sustained</p>
</li>
<li><p><strong>Between Tests:</strong> 480 seconds cooldown</p>
</li>
</ol>
<h3><strong>Realistic Traffic Distribution</strong></h3>
<p>The load test simulated realistic e-commerce traffic patterns:</p>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/a05c1ae4-8bcd-494f-a46e-b2348bb82ae0.png" alt="" style="display:block;margin:0 auto" />

<h2>Results</h2>
<h3>TanStack Start: The Performance Leader</h3>
<p>After Update (v1.157.16)</p>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/5c3a7ddc-0ef2-49ef-8c28-2f301f57b4b4.png" alt="" style="display:block;margin:0 auto" />

<p>TanStack Start delivered exceptional performance, the highest throughput and lowest latency of all frameworks tested. With Watt, average response times stayed under 13ms even at 1,000 requests per second.</p>
<h3>React Router: Solid and Reliable</h3>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/d72846a9-c017-4232-a096-1a3f6cd7c200.png" alt="" style="display:block;margin:0 auto" />

<p>React Router managed the load well and had zero failures. Using Watt made response times 38% faster compared to standalone Node.js.</p>
<h3>Next.js: Struggling Under Load, but Making Progress</h3>
<p>Initial Benchmark (Next.js 15.5.5, Watt 3.32.0)</p>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/c7a1abbd-2fc1-4798-b352-d7bb305277aa.png" alt="" style="display:block;margin:0 auto" />

<p>Next.js couldn’t handle 1,000 requests per second. Response times averaged 8 to 11 seconds, and about 40% of requests failed. Even with Watt’s optimizations, Next.js lagged behind the lighter frameworks.</p>
<h3>Updated Benchmark (Next.js 16.2.0-canary.66, Watt 3.39.0)</h3>
<p>We re-ran the benchmarks after upgrading to the latest Next.js canary and Watt 3.39.0 to see if the situation had improved:</p>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/f304a268-f7ce-4526-8d7d-f270e8d29926.png" alt="" style="display:block;margin:0 auto" />

<p>Next.js Version Improvement (Watt runtime)</p>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/21e0b39a-8212-41bb-93be-c66bed3ddcd7.png" alt="" style="display:block;margin:0 auto" />

<p>Upgrading from Next.js 15.5.5 to 16.2.0-canary.66, along with Watt 3.39.0, brought a big improvement:</p>
<ul>
<li><p>Throughput more than doubled</p>
</li>
<li><p>Average response times dropped by over six times</p>
</li>
<li><p>We saw an 83% reduction in latency.</p>
</li>
</ul>
<p>The success rate only improved a little (about 36% of requests still failed), but the successful requests were served much faster, with the median response time dropping from seconds to 431ms.</p>
<p>This is real progress. Next.js is still the slowest of the three frameworks at this load, but the gap is closing, and more improvements are on the way.</p>
<hr />
<h2>Framework Collaborations: Benchmarks as a Catalyst</h2>
<p>One of the best parts of this project was working directly with the framework teams. Sharing real-world benchmark data, especially flamegraphs that show where time is spent, helped turn abstract performance talks into real fixes. (If you are on a web performance team, we’d love to talk.)</p>
<h3>The Next.js Collaboration: Fixing RSC Deserialization</h3>
<p>After our initial Next.js benchmarks showed multi-second response times, we shared flamegraphs from our load tests with<a href="https://x.com/timneutkens">Tim Neutkens</a> from the Next.js team. The flamegraphs revealed a clear hotspot: <code>initializeModelChunk</code>. This function calls <code>JSON.parse</code> with a reviver callback in React Server Components (RSC) chunk deserialization.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/a6e398e0-0b0f-4cb2-b9f4-2d020f70b5b6.png" alt="" style="display:block;margin:0 auto" />

<p>The root cause was a well-known V8 performance characteristic: <code>JSON.parse</code> is implemented in C++, and passing a reviver callback forces a <strong>C++ → JavaScript boundary crossing for every key-value pair</strong> in the parsed JSON. Even a trivial no-op reviver <code>(k, v)</code> =&gt; <code>v</code> makes <code>JSON.parse</code> roughly 4x slower than bare <code>JSON.parse</code> without one. Since <code>initializeModelChunk</code> is called for every RSC chunk during SSR, this overhead compounds rapidly on pages with many server components.</p>
<p>Tim identified the fix and submitted it directly to React:<a href="https://github.com/facebook/react/pull/35776">facebook/react#35776</a> (merged Feb 19, 2026). The change replaces the reviver callback with a two-step approach—plain <code>JSON.parse()</code> followed by a recursive tree walk in pure JavaScript—yielding a <strong>~75% speedup</strong> in RSC chunk deserialization:</p>
<img alt="" style="display:block;margin:0 auto" />

<p>This fix helps every React framework that uses Server Components, not just Next.js. It shows how profiling with real workloads can reveal optimization opportunities that microbenchmarks might miss.</p>
<p>The improvement is already reflected in our updated Next.js benchmarks (v16.2.0-canary.66), and we expect further gains as this optimization and others land in stable releases.</p>
<h3>The TanStack Turnaround: A Case Study in Rapid Optimization</h3>
<p>Interestingly enough, we had a similar journey with the TanStack team. Our initial benchmarks used TanStack Start v1.150.0, and the results were concerning: requests timing out, 75% success rates, and average response times exceeding 3 seconds. We shared these findings with the TanStack team, who quickly identified the critical bottlenecks (also via <a href="https://github.com/platformatic/flame">@platformatic/flame</a>) in their SSR request handling pipeline.</p>
<p>Within 7 minor versions, they shipped a fix. We re-ran the benchmarks on v1.157.16, and the transformation was extraordinary:</p>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/719cb4d5-f97f-4c87-9ce9-35c35355da5e.png" alt="" style="display:block;margin:0 auto" />

<p>The v1.150 numbers tell the story of a framework under distress. The p(95) latency hitting exactly 10,001ms wasn’t a coincidence, as the requests were slamming into our 10-second timeout limit. One in four requests failed entirely.</p>
<p>At 1,000 req/s, the framework was drowning.</p>
<p>After the fix, TanStack Start became the fastest framework in our benchmark. Response times dropped from seconds to milliseconds,the timeout cliff vanished, and every single request succeeded.</p>
<p>What makes this improvement even more notable is that it was <strong>runtime-agnostic</strong>. Both Watt and Node.js saw virtually identical gains: Watt improved from 3,228ms to 12.79ms average response time, while Node.js improved from 3,171ms to 13.73ms. This confirms that the bottleneck was purely in the framework’s code and that the fix benefited all users equally, regardless of their deployment strategy.</p>
<hr />
<h2>Runtime Comparison: Watt vs Node.js</h2>
<h3>Watt’s SO_REUSEPORT Advantage</h3>
<p>Watt uses Linux kernel’s SO_REUSEPORT to let workers accept connections directly:</p>
<ol>
<li><p>Kernel distributes the connection to the worker.</p>
</li>
<li><p>The worker processes the request.</p>
</li>
</ol>
<p>No master coordination, no IPC overhead. The kernel handles load distribution efficiently.</p>
<h3>When Does Watt Help Most?</h3>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/85617211-e4de-4034-bf4e-b880d9e81c88.png" alt="" style="display:block;margin:0 auto" />

<h2>Framework Rankings</h2>
<h3>With Watt Runtime</h3>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/c3aa5238-a425-4b20-8a39-bcdcc4595d9f.png" alt="" style="display:block;margin:0 auto" />

<p><strong>With Node.js Runtime</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/7fcfc3d9-25fa-43b2-81c5-31003f8191eb.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>Reproducing These Benchmarks</h2>
<p>The complete benchmark infrastructure is available at:</p>
<p><a href="https://github.com/platformatic/k8s-watt-performance-demo/tree/ecommerce"><strong>https://github.com/platformatic/k8s-watt-performance-demo/tree/ecommerce</strong></a></p>
<p>To run the benchmarks:</p>
<pre><code class="language-plaintext"># Benchmark TanStack Start
AWS_PROFILE=&lt;profile-name&gt; FRAMEWORK=tanstack ./benchmark.sh

# Benchmark React Router
AWS_PROFILE=&lt;profile-name&gt; FRAMEWORK=react-router ./benchmark.sh

# Benchmark Next.js
AWS_PROFILE=&lt;profile-name&gt; FRAMEWORK=next ./benchmark.sh

# Benchmark all frameworks
AWS_PROFILE=&lt;profile-name&gt; ./benchmark-all.sh
</code></pre>
<p>The script creates an ephemeral EKS cluster, deploys all three runtime configurations (Node, PM2, Watt), executes the load tests, and tears down the infrastructure automatically. The results for PM2 were omitted from the blog post because they align with previously reported findings (read <a href="https://blog.platformatic.dev/93-faster-nextjs-in-your-kubernetes">93% Faster Next.js in (your) Kubernetes</a>).</p>
<p>The script creates an ephemeral EKS cluster, deploys all three runtime configurations (Node, PM2, Watt), executes the load tests, and tears down the infrastructure automatically. The results for PM2 were omitted from the blog post because they align with previously reported findings (read<a href="https://blog.platformatic.dev/93-faster-nextjs-in-your-kubernetes">93% Faster Next.js in (your) Kubernetes</a>).</p>
<hr />
<h2>Key Takeaways</h2>
<ol>
<li><p><strong>Watt Provides Consistent Improvements</strong><br />Watt improved performance for all frameworks compared to standalone Node.js. The gains ranged from 7% for TanStack to 38% for React Router. It’s a low-risk optimization that helps in every case.</p>
</li>
<li><p><strong>TanStack Start is Production-Ready</strong><br />Despite being the newest framework, TanStack Start delivered the best performance. The team’s rapid response to performance issues (a 252x improvement across 7 versions) demonstrates an active focus on development and optimization.</p>
</li>
<li><p><strong>Keep Dependencies Updated</strong><br />The results from TanStack and Next.js both show how important it is to keep your dependencies up to date. TanStack improved from 75% to 100% success in 7 versions. Next.js doubled its throughput between v15 and v16 canary. <strong>You only get these performance improvements if you update.</strong></p>
</li>
<li><p><strong>Framework Choice Matters More Than Runtime</strong><br />The difference between TanStack Start and Next.js (3x throughput, 690x latency difference) far exceeds the difference between Watt and Node.js on the same framework. Choose your framework wisely.</p>
</li>
<li><p><strong>Next.js Needs Caching</strong><br />At 1,000 req/s, Next.js struggled. For high-volume SSR workloads, users should consider adopting aggressive cache strategies (ISR, edge caching, component caching). Next.js has great primitives for these, and <a href="https://blog.platformatic.dev/watt-v318-unlocks-nextjs-16s-revolutionary-use-cache-directive-with-redisvalkey">you can use them in Watt.</a> We did not implement any caching solution for Next.js because, in most e-commerce (or enterprise) scenarios, caching is a no-go: companies want to implement aggressive personalization strategies and A/B testing, running thousands of experiments in parallel. That said, the jump from v15 to v16 Canary shows meaningful improvement, and if this trajectory continues, the gap will keep closing.</p>
</li>
</ol>
<p>If you want performance to be a key part of your technology choices, try setting clear latency budgets for each route before you start building or picking a framework. Setting concrete performance goals early helps guide decisions about architecture and tools, and makes sure your stack meets real-world needs. Planning for latency by route can also show when caching, framework choice, or runtime tweaks will have the biggest impact on user experience.</p>
<hr />
<h2>Conclusion</h2>
<p>These benchmarks show there are big performance differences between SSR frameworks when running the same app under load:</p>
<ul>
<li><p><strong>TanStack Start</strong> emerged as the performance leader, handling 1,000 req/s with 13ms average latency.</p>
</li>
<li><p><strong>React Router</strong> delivered reliable performance with zero failures.</p>
</li>
<li><p><strong>Next.js</strong> struggled at this load, but improved a lot after upgrading to v16 canary. Throughput doubled and latency dropped by six times.</p>
</li>
</ul>
<p>Beyond the numbers, this project showed that you can’t fix what you can’t see. We use <a href="https://github.com/platformatic/flame">platformatic/flame</a> for our own internal performance testing, and <strong>sharing benchmark data with framework teams led to real improvements</strong>. The TanStack team’s 252x improvement in 7 versions, and the Next.js team’s work that led to a <a href="https://github.com/facebook/react/pull/35776">75% speedup in React’s RSC deserialization</a>, both show that open performance data helps the whole ecosystem, not just one framework or project.</p>
<p>For teams choosing an SSR framework, these results suggest:</p>
<ul>
<li><p><strong>High-throughput requirements:</strong> Consider TanStack Start or React Router</p>
</li>
<li><p><strong>If you have an existing Next.js project, upgrade to the latest version for major performance gains</strong>. Use Watt to get the best throughput.</p>
</li>
<li><p><strong>Runtime optimization:</strong> Watt provides consistent improvements across all frameworks</p>
</li>
</ul>
<p>We’re actively looking to speak with web performance teams at the moment. If that’s you, please send me a DM on LinkedIn, Twitter, <a href="mailto:hello@platformatic.dev">hello@platformatic.dev</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Scale Next.js Image Optimization with a Dedicated Platformatic Application]]></title>
      <description><![CDATA[Image optimization with Next.js is a popular feature, but one that quietly causes instability (in the form of latency spikes) for your frontend.  This is because image resizing and encoding are very C]]></description>
      <link>https://blog.platformatic.dev/scale-nextjs-image-optimization-platformatic</link>
      <guid isPermaLink="true">https://blog.platformatic.dev/scale-nextjs-image-optimization-platformatic</guid>
      <category><![CDATA[Next.js]]></category>
      <category><![CDATA[Node.js]]></category>
      <category><![CDATA[Kubernetes]]></category>
      <dc:creator><![CDATA[Paolo Insogna]]></dc:creator>
      <pubDate>Tue, 10 Mar 2026 14:46:21 GMT</pubDate>
      <enclosure url="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/2ef7031f-64e1-4f37-83e6-943d22b043b4.png" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p>Image optimization with <a href="http://next.js">Next.js</a> is a popular feature, but one that quietly causes instability (in the form of latency spikes) for your frontend.  This is because image resizing and encoding are very CPU and memory-intensive, especially when traffic is highest, and users expect fast pages. During real launches, 95th percentile render times often rise from about 600ms to over 2 seconds when there are many image requests, even if the app code stays the same. If image processing shares workers with Server-Side Rendering (SSR), React Server Components (RSC), and API routes, a spike in image requests can slow down everything else, and all of a sudden, you’ve got a cascading failure on your hands.</p>
<p>That’s why teams often notice the same pattern during launches and campaigns: <code>/_next/image</code> traffic increases, CPU usage maxes out, render times get longer, and the whole frontend slows down even though the app logic hasn’t changed. In short, image optimization starts to interfere with your most important user flows.</p>
<p><a href="https://github.com/platformatic/platformatic/">Watt</a> is our open-source Node.js application server that orchestrates frontend frameworks (Next.js, Astro, Remix) and backend services (Node.js, Fastify, Express, Hono, etc) into a single system, with built-in logging, tracing, and multithreading. It leverages the Linux kernel's SO_REUSEPORT to distribute connections across workers with zero coordination overhead. In our <a href="https://blog.platformatic.dev/93-faster-nextjs-in-your-kubernetes">production benchmarks on AWS EKS</a>, Watt delivered 93.6% faster median latency and a 99.8% success rate under a sustained load of 1,000 requests per second. After investigating component rendering, it was only a question of time before we looked into images.</p>
<p>By moving image optimization into its own Watt Application, you create a clear microservice boundary. The optimizer becomes a focused service in your setup, with an API that only exposes what’s needed for safe and efficient image delivery. This keeps media processing separate from your main frontend. You can then scale image capacity on its own, let rendering workers focus on rendering, and adjust retries, timeouts, and storage for media processing without having to over-provision your whole frontend.</p>
<p><code>@platformatic/next</code> is the official Platformatic package for running Next.js inside a Watt Application. It’s fully maintained and supported by the Platformatic team, so you get long-term compatibility with Next.js updates, regular security patches, and best-practice defaults for production. Teams can count on ongoing updates and quick fixes, which lowers maintenance risk and avoids the downsides of custom or community-maintained solutions. The package now includes an Image Optimizer mode, letting you run <code>/_next/image</code> as a dedicated Watt Application, scale it separately, and keep your frontend fast even when image traffic increases.</p>
<p>This capability was introduced in <a href="https://github.com/platformatic/platformatic/pull/4605">PR #4605</a>, and it builds on top of <a href="https://github.com/platformatic/image-optimizer">@platformatic/image-optimizer</a>, our dedicated optimization engine. Our image optimizer is built on top of sharp, leveraging <a href="https://blog.platformatic.dev/job-queue-reliable-background-jobs">@platformatic/job-queue,</a> which adds flexible storage, job deduplication with caching, and producer/consumer decoupling.</p>
<p>If you are self-hosting Next.js and want the same kind of operational separation that mature platforms use internally, this is the missing building block.</p>
<p>In short, you can keep using Next.js as you always have, but with a cleaner architecture that handles high traffic more efficiently</p>
<h2>Why split image optimization from your frontend?</h2>
<p>If your frontend handles page rendering, API routes, and image resizing as a single service, any slowdown in one will cascade to the others. This means that when traffic is highest, like during product launches, campaigns, or social media spikes, this architecture causes performance to suffer the most</p>
<p>And it goes without saying (although it’s a blog, so yes, we will say it anyway…) that page performance isn’t just a technical issue - even a 100 ms delay can lower conversion rates by up to 7%, making slowdowns expensive during launches and campaigns.</p>
<p>The reason comes down to architecture: resizing and re-encoding images is bursty, CPU-heavy, and often I/O bound, while SSR and API routes usually need lower latency and more consistent resources. Running both in one service means you have to use the same autoscaling and resource pool for two very different types of work.</p>
<p>Splitting these responsibilities and running them as worker threads using Watt eliminates this ‘noisy neighbour’ effect and lets you apply the right scaling strategy to each path: scale optimizer replicas (or threads) when media demand rises, and keep frontend replicas sized for rendering throughput and tail latency.</p>
<p>Platformatic’s dedicated image optimizer, Watt Application, gives you:</p>
<ul>
<li><p><strong>Independent scaling</strong>: add replicas for image workloads without scaling the whole frontend stack.</p>
</li>
<li><p><strong>Operational isolation</strong>: image spikes do not starve SSR/RSC rendering.</p>
</li>
<li><p><strong>Centralized controls</strong>: enforce width/quality validation, timeout, retry behaviour, and storage in one place.</p>
</li>
<li><p><strong>Flexible queue storage</strong>: choose memory, filesystem, or Redis/Valkey depending on your topology.</p>
</li>
</ul>
<p>This setup is especially useful for platform engineering and SRE teams who need predictable performance without over-provisioning the whole frontend. Clear ownership lets these teams align this approach with their KPIs for reliability, scalability, and cost efficiency.</p>
<h2>What shipped in Platformatic Next</h2>
<p>The new <code>next.imageOptimizer</code> configuration lets you turn on optimizer-only mode in <code>@platformatic/next</code>, so you can run a Watt Application focused just on image processing. In other words: flip one flag and route only <code>/_next/image</code>, making adoption fast and low-friction.</p>
<p>When enabled, the service:</p>
<ol>
<li><p>Exposes only the Next.js image endpoint (<code>/_next/image</code>, respecting base path).</p>
</li>
<li><p>Validates image parameters using Next.js rules.</p>
</li>
<li><p>Resolves relative URLs through a fallback target (URL or runtime service name).</p>
</li>
<li><p>Fetches and optimizes images through a queue-backed pipeline; if the same image is requested by multiple users at the same time, it would be processed only once.</p>
</li>
<li><p>Returns optimized image bytes and cache headers.</p>
</li>
</ol>
<p>Under the hood, this relies on <a href="https://github.com/platformatic/image-optimizer">@platformatic/image-optimizer</a>, which provides a robust processing pipeline with:</p>
<ul>
<li><p>image type detection from magic bytes</p>
</li>
<li><p>optimization for <code>jpeg</code>, <code>png</code>, <code>webp</code>, and <code>avif</code></p>
</li>
<li><p>animation-aware safeguards</p>
</li>
<li><p>URL fetch + optimize helpers</p>
</li>
<li><p>queue APIs powered by <a href="https://github.com/platformatic/job-queue">@platformatic/job-queue</a></p>
</li>
</ul>
<p>The queue can run as a distributed state on Redis/Valkey, so retries, workload distribution, and resilience remain consistent across multiple optimizer replicas.</p>
<p>The main idea is to keep frontend rendering and image optimization separate, while still using the usual Next.js image features.</p>
<h2>What this means for teams</h2>
<ul>
<li><p><strong>Frontend teams</strong> keep using <code>next/image</code> as usual, without rewriting application code.</p>
</li>
<li><p><strong>Platform teams</strong> get explicit controls for retries, timeout budgets, and queue storage.</p>
</li>
<li><p><strong>Ops teams</strong> can scale optimizer replicas independently from the frontend tier.</p>
</li>
<li><p><strong>Product teams</strong> get a smoother user experience during peak traffic windows.</p>
</li>
</ul>
<p>The result is a platform that feels (and… is) faster to end users and more controllable to engineering teams. In recent internal benchmarks, shifting image optimization to a dedicated Watt Application reduced 95th-percentile response times during peak traffic by up to 40%, turning previously unpredictable slowdowns into consistently fast delivery even under heavy load.</p>
<h2>Choose the right runtime blueprint</h2>
<p>The easiest setup is a three-application Watt setup:</p>
<ul>
<li><p><strong>gateway</strong>: Watt’s gateway service, receive and routeincoming traffic.</p>
</li>
<li><p><strong>frontend</strong>: your standard Next.js application</p>
</li>
<li><p><strong>optimizer</strong>: <code>@platformatic/next</code> running in Image Optimizer mode</p>
</li>
</ul>
<p>Watt’s Gateway sends only <code>GET /_next/image</code> requests to the optimizer, while everything else goes to the <code>frontend</code>. This gives you a clear separation without needing a complicated network setup.</p>
<p>For relative image URLs (for example /<code>hero.jpg</code>), the optimizer fetches originals from <code>frontend</code> via runtime service discovery (<code>http://frontend.plt.local</code>). For absolute URLs, it fetches upstream directly.</p>
<p>If you are deploying on Kubernetes, your best bet is to configure your K8s ingress controller to route <code>GET /_next/image</code> to separate pods running the image optimizer. This configuration is supported and documented at <a href="https://docs.platformatic.dev/docs/guides/next-image-optimizer#10-kubernetes-ingress-example-nginx-ingress-controller">https://docs.platformatic.dev/docs/guides/next-image-optimizer#10-kubernetes-ingress-example-nginx-ingress-controller</a>.</p>
<h3><strong>How to set this up</strong></h3>
<p>Start by creating a Watt workspace with three applications: Gateway, frontend, and optimizer. The frontend remains your existing Next.js app; the optimizer is another <code>@platformatic/next</code> app with <code>next.imageOptimizer.enabled: true</code>; Gateway routes image traffic to the optimizer and everything else to the frontend.</p>
<p>Use this structure as a baseline:</p>
<pre><code class="language-plaintext">my-runtime/
 watt.json
 web/
   gateway/
     platformatic.json
   frontend/
     platformatic.json
     package.json
     next.config.js
     app/
   optimizer/
     next.config.js
     platformatic.json
     package.json
</code></pre>
<p>Then configure it in this order:</p>
<ol>
<li><p>Enable image optimizer mode in the <code>optimizer</code> Watt Application.</p>
</li>
<li><p>Set <code>optimizer.next.imageOptimizer.fallback</code> to <code>frontend</code> so relative image URLs are fetched from <code>http://frontend.plt.local</code>.</p>
</li>
<li><p>In Gateway, route only <code>GET /_next/image</code> to <code>optimizer</code> and keep all other routes on <code>frontend</code>.</p>
</li>
<li><p>Pick queue storage for your topology:</p>
<ul>
<li><p>memory for local/dev</p>
</li>
<li><p>filesystem for single-node persistent disk</p>
</li>
<li><p>Redis/Valkey for distributed replicas</p>
</li>
</ul>
</li>
<li><p>Tune <code>timeout</code> and <code>maxAttempts</code> using your target SLO and expected image profile.</p>
</li>
</ol>
<p>With this setup, app teams can keep using n<code>ext/image</code> as usual, while platform teams get independent scaling and more control over operations.</p>
<h2>Configuration example</h2>
<p>In your optimizer application config:</p>
<pre><code class="language-plaintext">{
 "$schema": "https://schemas.platformatic.dev/@platformatic/next/3.38.1.json",
 "next": {
   "imageOptimizer": {
     "enabled": true,
     "fallback": "frontend",
     "timeout": 30000,
     "maxAttempts": 3,
     "storage": {
       "type": "valkey",
       "url": "redis://localhost:6379",
       "prefix": "next-image:"
     }
   }
 }
}
</code></pre>
<p>And in your Gateway config, route only the image endpoint:</p>
<pre><code class="language-plaintext">{
 "$schema": "https://schemas.platformatic.dev/@platformatic/gateway/3.0.0.json",
 "gateway": {
   "applications": [
     {
       "id": "frontend",
       "proxy": {
         "prefix": "/",
         "routes": ["/*"]
       }
     },
     {
       "id": "optimizer",
       "proxy": {
         "prefix": "/",
         "routes": ["/_next/image"],
         "methods": ["GET"]
       }
     }
   ]
 }
}
</code></pre>
<h2>Storage choices: what to use and when</h2>
<ul>
<li><p><strong>memory</strong>: local development or simple single-instance setups.</p>
</li>
<li><p><strong>filesystem</strong>: single-node deployment with persistent disk.</p>
</li>
<li><p><strong>redis/valkey</strong>: distributed production environments with shared queue state.</p>
</li>
</ul>
<p>If you do not specify storage, memory is used by default.</p>
<p>For production multi-instance deployments, Redis/Valkey is usually the best default because it gives shared queue state and predictable behaviour across replicas.</p>
<h2>Failure handling and reliability</h2>
<p>Optimization runs through a queue with explicit timeout and retry controls:</p>
<ul>
<li><p><code>timeout</code> sets the fetch/optimization budget per job.</p>
</li>
<li><p><code>maxAttempts</code> controls the automatic retry count.</p>
</li>
</ul>
<p>When retries are exhausted, the service returns a <code>502 Bad Gateway</code> response, keeping failure behaviour explicit, observable, and easier to alert on.</p>
<h2>Try it today</h2>
<p>If you are self-hosting Next.js and want predictable image performance under load, this capability gives you a practical path that does not require re-architecting your app:</p>
<ol>
<li><p>keep your frontend app unchanged,</p>
</li>
<li><p>stand up a dedicated optimizer Watt Application,</p>
</li>
<li><p>route only <code>/_next/image</code> through Watt’s Gateway service,</p>
</li>
<li><p>pick the storage backend that matches your deployment model.</p>
</li>
</ol>
<p>This is a small architectural change with a big benefit: better frontend stability, simpler operations, and image performance that scales when you need it.</p>
<p>If you want to deliver faster and more reliable user experiences as your traffic grows, dedicated image optimization is one of the best upgrades you can make with minimal disruption.</p>
<p>Read more:</p>
<ul>
<li><p><a href="https://github.com/platformatic/platformatic/pull/4605">PR #4605: Added image optimizer capability</a></p>
</li>
<li><p><a href="https://github.com/platformatic/platformatic/blob/next-image/docs/guides/next-image-optimizer.md">Run Next.js Image Optimizer as a Dedicated Service</a></p>
</li>
<li><p><a href="https://github.com/platformatic/platformatic/blob/next-image/docs/reference/next/image-optimizer.md">Next.js Image Optimizer reference in Platformatic docs</a></p>
</li>
<li><p><a href="https://github.com/platformatic/image-optimizer">@platformatic/image-optimizer</a></p>
</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[We brought Skew Protection to your Kubernetes]]></title>
      <description><![CDATA[We're excited to share a new experimental feature for Platformatic: Skew Protection in the Intelligent Command Center (ICC). This brings Vercel-style deployment safety to Kubernetes, letting you deplo]]></description>
      <link>https://blog.platformatic.dev/skew-protection-for-kubernetes</link>
      <guid isPermaLink="true">https://blog.platformatic.dev/skew-protection-for-kubernetes</guid>
      <category><![CDATA[Kubernetes]]></category>
      <category><![CDATA[Node.js]]></category>
      <category><![CDATA[Devops]]></category>
      <dc:creator><![CDATA[Marco Piraccini]]></dc:creator>
      <pubDate>Thu, 05 Mar 2026 15:00:00 GMT</pubDate>
      <enclosure url="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/c5641a09-c34f-490b-a878-425b317c25b3.png" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p>We're excited to share a new experimental feature for Platformatic: <strong>Skew Protection</strong> in the Intelligent Command Center (ICC). This brings Vercel-style deployment safety to Kubernetes, letting you deploy without downtime and avoid version-mismatch problems.</p>
<p>You can think of this as akin to Vercel’s Skew Protection functionality, but running right in your existing Kubernetes setup: no migration or changes to your CI/CD pipeline or security policies needed, just out-of-the-box version pinning for your frontend applications.</p>
<h2>The Problem: Version Skew in Kubernetes</h2>
<p>When you update a web application, users who loaded the old frontend might send requests to the new backend. This is called “version skew,” and it can cause problems if APIs, assets, or data schemas have changed. For example, if you rename a form field, old clients might still send data using the old field name.</p>
<p>This problem matters even more for modern frontend apps, where the same codebase runs on both the client and server. Frameworks like Next.js, Remix, and monorepos often share TypeScript types, API definitions, or business logic between frontend and backend. If these shared parts change between versions, it can cause serious issues:</p>
<ul>
<li><p><strong>Hydration Errors and Broken UI: React Server Components</strong> tightly couples client and server in a single deployment; when a new version goes live, the server produces updated RSC payloads that older client bundles still in users' browsers cannot reconcile, causing hydration errors and broken UI</p>
</li>
<li><p><strong>API contract violations</strong>: OpenAPI or protobuf definitions change between versions, leading to serialization/deserialization failures</p>
</li>
<li><p><strong>Type discrepancies</strong>: Shared TypeScript interfaces or zod schemas break when frontend and backend versions diverge, causing runtime errors.</p>
</li>
<li><p><strong>Codependent features</strong>: Frontend components that rely on backend-specific functionality fail when that functionality changes or is removed</p>
</li>
</ul>
<p>The implications for your users are fairly straightforward: some might see API errors, missing fields, or broken features if their client and server versions don’t match; others might see data loss or corruption when schemas change across app versions. All this ultimately puts a load on support teams, who often need to coordinate across multiple feature teams to effectively untangle and ultimately resolve these issues.</p>
<p>Outside of the obvious impact on users (and revenue), k8s version skew is another example of how distributed systems, if not operated with the proper guardrails, actually impede developer velocity. In a world that is increasingly reliant on using AI to write code, the bottleneck is no longer the ability to write lines of code (if it ever was), but what happens between when your code is written and when it actually gets to production.</p>
<p>Version Skew in Kubernetes is a perfect example of such a problem - you have teams that are capable of shipping much faster, but without the right guardrails, the entire system actually moves slower and fails more often: fear of committing breaking changes leads to larger, less-frequent deployments that carry more risk and slow down your time-to-market.</p>
<h2>The Solution: ICC Skew Protection</h2>
<p>Platformatic’s new skew protection feature, built into the Intelligent Command Center, makes sure users stay on the version they started their session with, even when new versions are deployed. If a user starts a session on version N, all their requests during that session go to version N.</p>
<h3><strong>How It Works</strong></h3>
<p>Skew protection uses the <a href="https://gateway-api.sigs.k8s.io/">Kubernetes Gateway API</a> for version-aware routing, with ICC acting as the control plane. Each application version runs as a separate, immutable Kubernetes Deployment that users create themselves using standard Kubernetes workflows.</p>
<p>When applications run, ICC automatically detects new versions via label-based discovery and manages routing rules. ICC creates and maintains HTTPRoute resources that route requests based on session cookies, using a  <code>__plt_dpl</code>  cookie to pinusers to their deployment version.</p>
<p>When a new version is deployed, the previous version transitions to “draining” mode: existing sessions continue to work, while new sessions go to the active version. ICC monitors traffic activity and automatically cleans up old versions after configured grace periods.</p>
<h3>Key Platformatic Components</h3>
<p><strong>Platformatic Watt</strong> is the Node.js application server that runs your application as a worker thread inside of Kubernetes . This allows for improved performance, resiliency, and compute efficiency, as well as providing out-of-the-box features such as hot reloading, health checks, and metrics collection.</p>
<p><strong>watt-extra</strong> is an extension layer that sits on top of Platformatic Watt and serves as the bridge between your application and ICC. On startup, watt-extra connects to ICC and registers the application with its metadata (pod ID, app name, version). This registration enables ICC to:</p>
<ul>
<li><p>Discover the application’s Kubernetes labels (<code>app.kubernetes.io/name, plt.dev/version</code>)</p>
</li>
<li><p>Manage autoscaling using real-time, Node.js-specific metrics</p>
</li>
<li><p>Implement version-aware routing for skew protection</p>
</li>
<li><p>Monitor health and performance,</p>
</li>
</ul>
<p><strong>System Architecture</strong></p>
<p>The skew protection system consists of four layers. Each application version is a completely separate K8s Deployment, and the Kubernetes Gateway API handles routing at the ingress level based on <code>HTTPRoute</code> rules managed by ICC.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/36fc310d-5bdb-475a-9ca9-f6597b1e440c.png" alt="" style="display:block;margin:0 auto" />

<h3>Component Breakdown</h3>
<p><strong>Client Layer</strong></p>
<ul>
<li><p><strong>Browser Session A (cookie: __plt_dpl=dep-v42)</strong>: A user who started their session on version 42. The <code>__plt_dpl</code> cookie pins their requests to that version, making sure the requests are routed to the correct backend even after newer versions are deployed.</p>
</li>
<li><p><strong>Browser Session B (cookie: __plt_dpl=dep-v43)</strong>: A user who started their session on version 43. Their requests are routed to the active version based on their cookie.</p>
</li>
<li><p><strong>New Visitor (no deployment cookie)</strong>: A first-time user or someone without a version cookie. Their first request is routed to the current active version, and they receive a cookie that pins them to that version.</p>
</li>
</ul>
<p><strong>Gateway API Layer</strong></p>
<ul>
<li><p><strong>GatewayClass</strong>: Defines a template or class of gateways (e.g., Envoy Gateway, Contour, or Cilium) that can process Gateway API resources. Each cluster operator configures this with their preferred controller.</p>
</li>
<li><p><strong>Gateway Resource</strong>: The actual gateway instance that listens on HTTP/HTTPS ports and processes incoming traffic. It contains listener configurations for TLS termination and routing.</p>
</li>
<li><p><strong>HTTPRoute</strong>: Managed by ICC, this is the key routing rule that implements version-aware routing. It contains multiple rules: cookie-based matches for draining versions and a default rule that sets a cookie for new visitors and routes to the active version.</p>
</li>
</ul>
<p><strong>ICC (Intelligent Command Center) - Namespace: platformatic</strong></p>
<ul>
<li><p><strong>Control Plane Service</strong>: The core component responsible for version detection, HTTPRoute management, and lifecycle decisions. When watt-extra registers a new pod, the control plane discovers the application name and version. It holds the version registry and creates/updates/deletes HTTPRoute resources as needed.</p>
</li>
<li><p><strong>PostgreSQL</strong>: Stores the persistent state for skew protection, including the version registry with full metadata about each deployment (version string, timestamps, K8s resources), deployment history for audit trails, and per-application skew protection policies.</p>
</li>
</ul>
<p><strong>App Versions - Namespace: myapp</strong></p>
<ul>
<li><p><strong>Deployment: myapp-v42 (draining)</strong>: A Kubernetes Deployment for the previous version (42) that is being phased out. It has its own Service and pods running Watt with watt-extra. Traffic only routes here for users whose cookies match this version.</p>
</li>
<li><p><strong>Deployment: myapp-v43 (active)</strong>: The current active version deployment. It has multiple replicas for high availability. New visitors and users without matching cookies are routed here. ICC’s autoscaler works across all deployed versions, provisioning the correct amount of resources for each version based on actual traffic.</p>
</li>
<li><p><strong>Service</strong>: Each version has its own Kubernetes Service that selects pods with the corresponding <code>plt.dev/version</code> label. These Services are referenced by the HTTPRoute’s backendRefs.</p>
</li>
<li><p><strong>Pods (Watt + watt-extra)</strong>: Each pod runs the application container (Platformatic Watt runtime) plus watt-extra. watt-extra is the ICC agent that connects to ICC on startup and registers the pod. It sends the pod ID, and ICC discovers the version and deployment metadata through Kubernetes APIs. watt-extra also reports metrics to ICC for autoscaling and health monitoring.</p>
</li>
</ul>
<p><strong>Observability Layer</strong></p>
<p><strong>Prometheus</strong>: Collects metrics from all pods and services. ICC queries Prometheus to monitor traffic patterns for each version, track request rates for draining versions, and uses that data to determine when versions should be transitioned to Expired status (meaning services that received no traffic for the pre-configured grace period).</p>
<h2>How It All Works Together</h2>
<p>When a new application version is deployed:</p>
<ol>
<li><p>You deploy a new version of your app with the same <code>app.kubernetes.io/name</code> label and a new <code>plt.dev/version</code> label.</p>
</li>
<li><p>watt-extra registers the new pods with ICC, which detects the new version from the labels.</p>
</li>
<li><p>ICC makes the new version Active and moves the previous one to Draining. It updates the Gateway routing rules so that new sessions go to the active version, while existing sessions with a version cookie keep going to the draining version.</p>
</li>
<li><p>ICC monitors traffic on draining versions. Once there is no traffic, or the grace period elapses, ICC expires the old version — removing its routing rules and scaling it to zero, and optionally deleting the old Deployment and Service.</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/3241ed07-c284-4b94-8ce7-8fbb10041116.png" alt="" style="display:block;margin:0 auto" />

<h2>The Deployment Lifecycle in Detail</h2>
<p>When managing multiple versions, skew protection uses a well-defined state machine to guarantee flawless transitions:</p>
<ul>
<li><p><strong>Active</strong> → The current version serving new sessions. Exactly one version per application is Active at a time. The HTTPRoute’s default rule points to the Active version’s Service, and new visitors receive a cookie pinning them to this version.</p>
</li>
<li><p><strong>Draining</strong> → When a newer version is detected and becomes Active, the previous version transitions to Draining. No new sessions are assigned to it, but existing sessions with version-pinning cookies continue to be served. ICC monitors traffic activity for draining versions to determine when they can be safely retired.</p>
</li>
<li><p><strong>Expired</strong> → A version transitions to Expired when it has zero traffic over the traffic window (default: 30 minutes) or when the grace period elapses (default: 24 hours), whichever comes first. ICC then removes the version’s matching rules from the HTTPRoute, scales the Deployment to zero replicas via the autoscaler, and optionally deletes the Deployment and Service (if auto-cleanup is enabled).</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/88c464b9-183f-4548-8b0c-44a57eadcfc7.png" alt="" style="display:block;margin:0 auto" />

<p>The ICC uses Version Labels to determine state. Version labels are opaque strings andcan be numbers, semver, git SHAs, or any identifier that fits your workflow. ICC does not parse or compare them; it just treats the most recently detected version as Active.</p>
<p><strong>How users deploy a new version:</strong></p>
<ol>
<li><p>Build a new container image with the updated application code (e.g., <code>myapp:v43</code>)</p>
</li>
<li><p>Create a new K8s Deployment and Service with:</p>
<ul>
<li><p>Same <code>app.kubernetes.io/name</code> label (e.g., <code>myapp</code>) — this tells ICC it’s the same application</p>
</li>
<li><p>New <code>plt.dev/version</code> label (e.g., <code>43</code>) — this tells ICC it’s a new version</p>
</li>
<li><p>New Deployment name (e.g., <code>myapp-v43</code>) and matching Service name</p>
</li>
</ul>
</li>
<li><p>Apply the manifest: kubectl apply -f myapp-v43.yaml</p>
</li>
<li><p>ICC automatically detects the new version when pods start and watt-extra registers with ICC. The new version becomes Active, and the previous version begins draining.</p>
</li>
</ol>
<h3><strong>Getting Started with ICC</strong></h3>
<p>Platformatic’s skew protection is built into the Intelligent Command Center (ICC), a complete control plane for managing <a href="http://node.js">Node.js</a> applications or agents running in Kubernetes,with autoscaling, monitoring, and version-aware routing.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/1298be19-1dd1-47aa-98df-45a1163e7afb.png" alt="" style="display:block;margin:0 auto" />

<p><strong>To get started with ICC:</strong></p>
<ul>
<li><p><strong>Install ICC</strong> on your Kubernetes cluster. Follow our <a href="https://icc.platformatic.dev/installation/">Installation Guide</a> for step-by-step instructions, covering infrastructure requirements (Kubernetes, PostgreSQL, Valkey, Prometheus) and installation options.</p>
</li>
<li><p><strong>Deploy your first application</strong> using the standard ICC workflow:</p>
<ul>
<li><p>Add <code>@platformatic/watt-extra</code> to your app</p>
</li>
<li><p>Set <code>PLT_ICC_URL</code> so your app can register with ICC</p>
</li>
<li><p>Deploy with <code>kubectl apply</code> or your existing CI/CD pipeline</p>
</li>
</ul>
</li>
<li><p><strong>Enable Skew Protection</strong>:</p>
<ul>
<li><p>Enable <code>PLT_FEATURE_SKEW_PROTECTION</code></p>
</li>
<li><p>Ensure Gateway API CRDs are installed (Kubernetes 1.27+)</p>
</li>
<li><p>Deploy a Gateway API-compatible controller (Envoy Gateway, Contour, Cilium, Traefik, NGINX Gateway Fabric or Kong). See the <a href="https://icc.platformatic.dev/skew-protection/prerequisites/#compatible-controllers">Compatible Gateways in ICC documentation</a></p>
</li>
<li><p>Configure deployment labels:</p>
</li>
</ul>
</li>
</ul>
<pre><code class="language-plaintext">labels:
  app.kubernetes.io/name: myapp
   plt.dev/version: "43"
   # Optional: custom path prefix (default: /myapp)
   # plt.dev/path: "/api/leads"
   # Optional: hostname for HTTPRoute
   # plt.dev/hostname: "myapp.example.com"
</code></pre>
<h3>Bring Vercel-Grade Deployment Safety to Your Kubernetes Environment</h3>
<p>Platformatic’s skew protection is now available in ICC. It provides zero-downtime deployments and version-aware routing that keep each user session consistent.</p>
<p>If your team wants to try it in a real enterprise setup, send a message to <a href="https://www.linkedin.com/in/lucamaraschi/">Luca Maraschi</a> or <a href="https://www.linkedin.com/in/matteocollina/">Matteo Collina</a> via DMs on LinkedIn, or contact <a href="mailto:info@platformatic.dev">info@platformatic.dev</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Building an Auditable AI Gateway with Platformatic Watt]]></title>
      <description><![CDATA[Every engineering team that adopts AI quickly hits the same wall: a simple provider integration that worked for a demo turns into an operational bottleneck at scale. Tracking usage, containing costs, ]]></description>
      <link>https://blog.platformatic.dev/auditable-ai-gateway</link>
      <guid isPermaLink="true">https://blog.platformatic.dev/auditable-ai-gateway</guid>
      <category><![CDATA[Node.js]]></category>
      <category><![CDATA[AI]]></category>
      <dc:creator><![CDATA[Paolo Insogna]]></dc:creator>
      <pubDate>Wed, 04 Mar 2026 15:00:00 GMT</pubDate>
      <enclosure url="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/84034626-b07c-4b0e-b329-0af73a74b5b9.png" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p>Every engineering team that adopts AI quickly hits the same wall: a simple provider integration that worked for a demo turns into an operational bottleneck at scale. Tracking usage, containing costs, and keeping an audit trail across growing models and teams can slip out of reach fast. AI features are moving fast, but production teams still need the same thing they have always needed: not just control, but auditability.</p>
<p>That is exactly what ai-gateway-auditable delivers: an OpenAI-compatible gateway built with <a href="https://docs.platformatic.dev/">Platformatic Watt</a> that combines provider routing, fallback resiliency, and durable audit logging to S3.</p>
<p>For production teams, this translates directly into risk reduction and regulatory readiness: your audit trail is always preserved, and resilient routing keeps incidents contained. In real terms, this leads to fewer lost logs or broken provider integrations (and fewer 3 a.m. pages as a result), and reliable evidence when you need to answer compliance or security reviews.</p>
<p>This architecture is not only production-ready, but already operating a scale for one of our early adopters. One application (proxy) serves traffic, while another (audit worker) persists audits, and a durable queue between them keeps latency low while preserving records, using the filesystem to provide durability. This same early-adopter halved its application latency using this pattern with Watt. With clear audit trails and resilient traffic handling, they were able to trace errors quickly and keep their on-call load under control, while giving their LLM-enabled end-users performance that approached parity with direct API calls, which was critical for serving their real-time use cases.</p>
<p>Source code: <a href="https://github.com/platformatic/ai-gateway-auditable">github.com/platformatic/ai-gateway-auditable</a></p>
<h2><strong>Why this matters now</strong></h2>
<p>The direct integration pattern is usually the first-stop for teams, but often leads to audit-trace gaps. Finance needs clean attribution by key or team, security needs auditable traces of model interactions, and product needs stronger uptime when upstream providers degrade.</p>
<p>As a real-world example, our same early adopter saw this with their initial production rollout, which missed up to 15% of request logs during peak volume, and causing request latency to spike by more than 2x when provider response times flared. At the same time, you want a single, stable integration surface instead of scattering provider-specific logic across multiple services. An AI gateway is where all your needs converge into a single, manageable control point.</p>
<p>With ai-gateway-auditable, every request has a clear path, every response is traceable, and fallback behavior is visible instead of opaque.</p>
<h2><strong>Why Watt</strong></h2>
<p>Platformatic Watt is well-suited to this pattern because it lets us run the API-facing proxy and the audit worker as separate applications with a shared operational model, using them as worker threads. That separation is the foundation of reliability here: the proxy can stay focused on low-latency responses, while the worker can focus on durable queue consumption, batching, and S3 shipping.</p>
<p>Most importantly, this design is tolerant of worker crashes. Watt supervises applications (worker threads), so if an audit worker crashes, it is automatically restarted, and unhealthy workers are automatically replaced. During that window, the proxy can keep accepting requests and persisting audit jobs in FileStorage. When the replacement worker is up, it resumes consuming from the same queue path and drains pending jobs.</p>
<p>The result is graceful degradation rather than data loss: temporary worker failures increase audit lag but do not break the request path or discard audit events. This distinction is critical from a business perspective. Losing audit data can put regulatory compliance at risk and expose the company to possible fines or a loss of trust, while a short delay in audit processing only postpones analysis or reporting. In other words, our design trades brief insight delays for the certainty that no evidence is lost.</p>
<h2><strong>Why filesystem-based storage</strong></h2>
<p>We use filesystem-backed queue storage on purpose. Writing audit jobs to local disk is crash-tolerant because queued data survives process failures and restarts, unlike in-memory buffers.</p>
<p>It also keeps resource usage and request-path performance under control. We do not need to retain full audit payloads in memory awaiting for remote writes, and we do not put every request on the critical path of an external storage service. That removes network latency and remote availability as immediate blockers to request handling, while still providing durable buffering before batches are shipped to S3.</p>
<h2><strong>Architecture at a glance</strong></h2>
<p>The system runs as two applications (threads) inside of <a href="https://docs.platformatic.dev/">Platformatic Watt</a>, the Node.js application server.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63f78b3e207712e9dab049ad/f631ae19-c62d-4d5a-b2e5-fef16f32d9d6.png" alt="" style="display:block;margin:0 auto" />

<p>The proxy is optimized for low-latency request/response flow, while the audit-worker is optimized for durability, retries, and batch shipping. Keeping these concerns separate avoids a common failure mode: heavy audit I/O slowing down user-facing traffic.</p>
<p>How do the two applications communicate? Through the same FileStorage queue path on disk. proxy writes audit jobs to ./data/queue at the same rate as local queue operations, and audit-worker consumes those jobs independently in the background. This gives you explicit producer/consumer decoupling: the request path does not wait for S3 uploads, retries, or batch rotation. If the worker restarts, queued jobs remain on disk and are resumed when it comes back. If S3 is slow or temporarily unavailable, jobs continue to accumulate durably in the queue instead of being lost or pushing latency back to callers.</p>
<p>In other words, even when storage is under pressure or S3 is temporarily unavailable, the gateway can keep serving requests while the audit pipeline catches up safely in the background.</p>
<h2><strong>What the gateway gives you</strong></h2>
<p>At a product level, this gateway provides four strong guarantees:</p>
<ol>
<li><p><strong>OpenAI Completions compatible endpoint</strong> (/v1/chat/completions) for clients and SDKs.</p>
</li>
<li><p><strong>Model-based routing with fallback</strong> across providers.</p>
</li>
<li><p><strong>Complete request/response audit records</strong> for every successful exchange.</p>
</li>
<li><p><strong>Durable archival to S3</strong> with batched JSONL files partitioned by time (JSON Lines is a text file format where each line is a valid, independent JSON object, separated by newline characters).</p>
</li>
</ol>
<p>This means reduced provider lock-in, minimized operational risks, and heightened observability.</p>
<h2><strong>Service responsibilities</strong></h2>
<p>The key behavior is role decoupling: proxy only produces queue jobs, while audit-worker handles all downstream storage and shipping work.</p>
<h3><strong>proxy (external entrypoint)</strong></h3>
<p>proxy exposes:</p>
<ul>
<li><p><code>GET /health</code></p>
</li>
<li><p><code>POST /v1/chat/completions</code></p>
</li>
</ul>
<p>For each request, it:</p>
<ol>
<li><p>Selects a provider chain based on model routing rules.</p>
</li>
<li><p>Executes upstream calls with fallback on retryable failures.</p>
</li>
<li><p>Returns the upstream response to the client.</p>
</li>
<li><p>Enqueues an audit payload into the shared durable queue.</p>
</li>
</ol>
<h3><strong>audit-worker (internal service)</strong></h3>
<p>audit-worker is an internal Node application with no HTTP API (hasServer = false).</p>
<p>It owns the full audit persistence path:</p>
<ul>
<li><p>queue consumption with @platformatic/job-queue</p>
</li>
<li><p>durable local buffering with FileStorage</p>
</li>
<li><p>batched JSONL writing</p>
</li>
<li><p>S3 uploads signed with AWS SigV4.</p>
</li>
</ul>
<p>Queue settings used in the current implementation:</p>
<ul>
<li><p><code>concurrency: 1</code></p>
</li>
<li><p><code>maxRetries: 3</code></p>
</li>
<li><p><code>resultTTL: 60_000</code></p>
</li>
<li><p><code>visibilityTimeout: 30_000</code></p>
</li>
</ul>
<p>This is optimized for predictable sequential writes and safe retry semantics. Filesystem queue storage is chosen because it needs no external setup (no Redis/Valkey), making local development and single-node production rollouts much simpler. At the same time, it still provides crash resilience: queue state is persisted to disk, so in-flight and pending audit jobs survive process restarts.</p>
<p>That combination is the key trade-off here: you gain operational simplicity and zero external dependencies, without sacrificing durability for the audit trail. Note that adopting the file system exposes teams to the risk of data loss. Moving the auditability trail back to the main response cycle will introduce latency and cause a hard failure if the audit cannot be completed. The tradeoff, as always, is in the hands of engineers: availability or consistency?</p>
<h2><strong>Routing and fallback configuration</strong></h2>
<p>Routing lives in providers.json and uses two lists:</p>
<ul>
<li><p>providers: upstream connection and adapter definitions</p>
</li>
<li><p>routing: per-model routing rules with ordered provider chains</p>
</li>
</ul>
<pre><code class="language-javascript">{
 "providers": [
   {
     "id": "openai",
     "type": "openai",
     "baseUrl": "https://api.openai.com",
     "apiKey": "{OPENAI_API_KEY}"
   },
   {
     "id": "anthropic",
     "type": "anthropic",
     "baseUrl": "https://api.anthropic.com",
     "apiKey": "{ANTHROPIC_API_KEY}"
   }
 ],
 "routing": [
   {
     "id": "gpt-4o",
     "providers": ["openai"],
     "strategy": "fallback"
   },
   {
     "id": "claude-sonnet-4-6",
     "providers": ["anthropic"],
     "strategy": "fallback"
   },
   {
     "id": "*",
     "providers": ["openai"],
     "strategy": "fallback"
   }
 ]
}
</code></pre>
<p>Environment variables like <code>{OPENAI_API_KEY}</code> are resolved from process env at startup.</p>
<p>Fallback behavior is explicit and policy-driven: by exposing a clearly configurable list of retryable statuses, teams can align gateway failover with internal governance or incident playbooks. For example, you can tune which upstream failures (such as 429, 500, 502, 503, 504) trigger fallback based on your own risk, compliance, or incident response thresholds. This mapping between config and governance means compliance and security teams can review and pre-approve response handling in line with internal standards—a step that accelerates approval and audit-readiness.</p>
<ul>
<li><p>retryable statuses: 429, 500, 502, 503, 504</p>
</li>
<li><p>Connection failures are retryable</p>
</li>
<li><p>Non-retryable responses (400, 401, 403) are returned immediately.</p>
</li>
</ul>
<p>If you want delegated provider orchestration, you can configure OpenRouter as an openai-type provider and route * traffic to it.</p>
<h2><strong>Adapter model: one external contract, many upstreams</strong></h2>
<p>The gateway keeps a single OpenAI-compatible API surface, while adapters normalize provider differences behind the scenes.</p>
<ul>
<li><p>OpenAI adapter supports OpenAI-compatible endpoints, including Azure/OpenRouter-compatible APIs.</p>
</li>
<li><p>The anthropic adapter translates OpenAI chat requests and responses to Anthropic Messages API semantics.</p>
</li>
</ul>
<p>This removes provider-specific branching logic from your application layer.</p>
<h2><strong>Streaming support with full audit fidelity</strong></h2>
<p>Streaming UX matters, so the proxy preserves token-by-token delivery.</p>
<p>For stream: true requests, the proxy:</p>
<ol>
<li><p>Pipes SSE chunks to the client in real time.</p>
</li>
<li><p>Buffers chunks internally.</p>
</li>
<li><p>Reconstructs a complete Chat Completions response.</p>
</li>
<li><p>Emits a single audit record with streamed set to true.</p>
</li>
</ol>
<p>Users get low-latency streaming, and operators still get complete records for replay and analysis.</p>
<h2><strong>Audit record shape</strong></h2>
<p>Each JSONL line is a complete record with request, response, latency, caller hash, status, and routing metadata:</p>
<pre><code class="language-json">{
 "id": "a8f3b2c1-...",
 "timestamp": "2026-03-03T11:44:00.000Z",
 "duration_ms": 1243,
 "request": {
   "model": "gpt-4o",
   "messages": [{ "role": "user", "content": "Hello" }]
 },
 "response": {
   "id": "chatcmpl-...",
   "choices": [{ "message": { "role": "assistant", "content": "Hi!" } }]
 },
 "upstream_status": 200,
 "caller": "7a3f2b1c",
 "streamed": false,
 "routing": {
   "model": "gpt-4o",
   "planned_providers": [{ "id": "openai", "status": 200, "duration_ms": 1200 }],
   "used_provider": "openai"
 }
}
</code></pre>
<p>The caller is an 8-character SHA-256 prefix of the bearer token value, so attribution is possible without storing raw API keys.</p>
<h2><strong>Durable audit pipeline in detail</strong></h2>
<p>Inside the request path, proxy enqueues each payload using the request ID as the job ID, which naturally supports deduplication when IDs repeat.</p>
<p>audit-worker consumes those jobs and writes them into local JSONL batches before upload.</p>
<p>The writer then:</p>
<ol>
<li><p>Appends each record as one JSON line to a local batch file using flush semantics.</p>
</li>
<li><p>Rotates to a new batch when the size or time threshold is reached.</p>
</li>
<li><p>Uploads the batch file to S3 using undici and SigV4 headers.</p>
</li>
<li><p>Deletes local batch files only after successful upload.</p>
</li>
</ol>
<p>Current thresholds:</p>
<ul>
<li><p><code>BATCH_SIZE = 100</code></p>
</li>
<li><p><code>FLUSH_INTERVAL_MS = 5000</code></p>
</li>
</ul>
<p>S3 object keys are hour-partitioned for downstream querying:</p>
<p><code>audits/2026/03/03/11/batch-1741003090000-3bb7....jsonl</code></p>
<p>This structure works well with tools like Athena and other data lake pipelines.</p>
<h2><strong>Operating under failure</strong></h2>
<p>The gateway is intentionally designed to degrade gracefully.</p>
<p>Typical architectural components here include the file-backed queue directory (such as ./data/queue), which serves as the communication bridge between the proxy and the audit-worker; single-node deployment support via Platformatic Watt's supervised applications; and a default S3 bucket for audit archives. Core configuration files like providers.json define routing logic and provider chains, while runtime environment variables control credentials and logging. All of these components work together as the durable, fault-tolerant foundation that keeps this architecture reliable at scale. This keeps user-facing availability high while preserving eventual audit consistency.</p>
<h2><strong>Run it locally</strong></h2>
<pre><code class="language-plaintext">git clone https://github.com/platformatic/ai-gateway-auditable.git
cd ai-gateway-auditable
npx wattpm-utils install
docker compose up
</code></pre>
<p>Then call the gateway with any OpenAI-compatible client or a simple curl:</p>
<pre><code class="language-plaintext">curl http://localhost:3042/v1/chat/completions \
 -H 'Content-Type: application/json' \
 -H 'Authorization: Bearer sk-your-key' \
 -d '{
   "model": "gpt-4o",
   "messages": [{"role": "user", "content": "Hello"}]
 }'
</code></pre>
<h2><strong>Final take</strong></h2>
<p>ai-gateway-auditable is a practical pattern for teams that need to move fast with AI and still satisfy the operational norms of production software. It gives you:</p>
<ul>
<li><p>one consistent API surface with clear fallback behavior,</p>
</li>
<li><p>complete and queryable audit trails, and a clean separation between serving traffic and persisting evidence.</p>
</li>
</ul>
<p>If your roadmap includes multi-provider AI, compliance requirements, or strict SRE expectations, this architecture is ready to adopt and extend.</p>
<p>The easiest way to get started is to fork the repo, run the quick-start commands, and see the gateway in action with your own test requests. Try spinning up the service locally and sending a sample call: this practical step will show you right away how auditable AI operations can be within your own workflow.</p>
<p>Happy building!</p>
]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Introducing @platformatic/job-queue ]]></title>
      <description><![CDATA[Every backend developer knows the frustration: a key job disappears during a server restart, or duplicate tasks pile up when a client retries a request. Lost work, repeated emails, missing reports: th]]></description>
      <link>https://blog.platformatic.dev/job-queue-reliable-background-jobs</link>
      <guid isPermaLink="true">https://blog.platformatic.dev/job-queue-reliable-background-jobs</guid>
      <category><![CDATA[Node.js]]></category>
      <dc:creator><![CDATA[Matteo Collina]]></dc:creator>
      <pubDate>Tue, 03 Mar 2026 15:00:00 GMT</pubDate>
      <enclosure url="https://cdn.hashnode.com/uploads/covers/62bc139e9c913efac56c8de3/6c7c7af8-cdf9-4c65-8472-fcba452e2ca9.png" length="0" type="image/jpeg"/>
      <content:encoded><![CDATA[<p>Every backend developer knows the frustration: a key job disappears during a server restart, or duplicate tasks pile up when a client retries a request. Lost work, repeated emails, missing reports: these breakdowns always seem to happen when reliability matters most.</p>
<p><a href="https://github.com/platformatic/job-queue">@platformatic/job-queue</a> is a new queue library from Platformatic focused on reliability and operational simplicity. This library is built on a workflow that lets you enqueue jobs and wait for results when needed, making background processing feel just as smooth as calling a function. Alongside this, it provides Node.js teams with a modern API that includes built-in caching, deduplication, retries, and pluggable storage.</p>
<p>In practice, this means you can start with a tiny local setup and then move to a distributed, production-grade deployment without rewriting your application code.</p>
<h2><strong>What makes it different</strong></h2>
<p>Most queue setups force you to stitch together multiple patterns and handle edge cases yourself. @platformatic/job-queue includes those patterns out of the box:</p>
<ul>
<li><p><strong>Deduplication by job id</strong> so repeated enqueue attempts do not create duplicate work.</p>
</li>
<li><p><strong>Request/response support</strong> with enqueueAndWait() when you need async processing but still want a result.</p>
</li>
<li><p><strong>Reliable retries</strong> with configurable attempts and backoff behavior.</p>
</li>
<li><p><strong>Stalled job recovery</strong> via a Reaper that requeues jobs from crashed workers.</p>
</li>
<li><p><strong>Graceful shutdown</strong> ensures in-flight jobs complete before the service stops, reducing lost work during deploys and restarts.</p>
</li>
<li><p><strong>Move fast with safety:</strong> The API is TypeScript-native with typed payloads and results, so you catch errors at compile time and move confidently.</p>
</li>
</ul>
<p>This makes it appropriate for both classic fire-and-forget workloads and RPC-style workloads that require a response. You do not have to pick one model globally: many teams use both in the same system, depending on endpoint and latency requirements. For example, in use cases such as sending emails and notifications, fire-and-forget jobs make sense because results are often not needed immediately and occasional retries can be handled gracefully. On the other hand, workflows such as generating invoices or processing payments may require the caller to wait for a result, making the request/response pattern with enqueueAndWait() a better fit.</p>
<h2><strong>A quick look at the API</strong></h2>
<p>You can use the queue as a producer and consumer in the same process, or split them across services. The API is intentionally small, so the same primitives are easy to apply in monoliths, microservices, and worker pools.</p>
<pre><code class="language-javascript">import { Queue, MemoryStorage } from '@platformatic/job-queue'

const storage = new MemoryStorage()
const queue = new Queue&lt;{ email: string }, { sent: boolean }&gt;({
 storage,
 concurrency: 5
})

queue.execute(async job =&gt; {
 // your business logic
 return { sent: true }
})

await queue.start()

// fire-and-forget
await queue.enqueue('email-1', { email: 'user@example.com' })

// request/response
const result = await queue.enqueueAndWait('email-2', { email: 'another@example.com' }, { timeout: 30_000 })
console.log(result)

await queue.stop()
</code></pre>
<h3><strong>Architecture description</strong></h3>
<p>When you call enqueue(), the producer checks if the job already exists in the storage. If it’s a new job, it's added to the queue with the state “queued,” and the method returns immediately. If the job is a duplicate, the storage returns a duplicate status without creating a new entry.</p>
<img src="https://cdn.hashnode.com/uploads/covers/62bc139e9c913efac56c8de3/2a9a780e-e0bd-47e7-ad7a-7a6e72a11941.png" alt="" style="display:block;margin:0 auto" />

<p>When you call enqueueAndWait(), the producer first subscribes to a notification for that job, then enqueues it. If the job was already processed, it returns the cached result immediately. Otherwise, it waits for a notification from the worker when the job completes (or fails), then fetches the result and returns it.</p>
<img alt="" style="display:block;margin:0 auto" />

<p>The consumer continuously dequeues jobs from the storage using a blocking move operation. When it receives a job, it marks it as “processing” and executes the handler. On success, it stores the result with TTL and marks the job as completed. On failure, it either retries (if attempts remain) or marks the job as failed.</p>
<img alt="" style="display:block;margin:0 auto" />

<p>The producer API supports per-job options such as maxAttempts and resultTTL, which are useful when not all jobs have the same retention or retry requirements. For example, you might keep invoice-generation results longer than low-value notification results, even if they run on the same queue.</p>
<h2><strong>Storage backends for different environments</strong></h2>
<p>@platformatic/job-queue ships with three storage adapters:</p>
<h3><strong>MemoryStorage</strong></h3>
<p>MemoryStorage keeps all queue states in process memory. This makes it ideal for local development, testing, and simple single-instance services where data can be ephemeral.</p>
<pre><code class="language-javascript">import { Queue, MemoryStorage } from '@platformatic/job-queue'
const storage = new MemoryStorage()
const queue = new Queue({ storage })
</code></pre>
<p>Jobs are stored in JavaScript Maps and Sets within the same process. This gives you the lowest latency possible, but means jobs are lost if the process restarts. For development workflows where you restart frequently, this is usually not a concern.</p>
<h3><strong>FileStorage</strong></h3>
<p>FileStorage persists the queue state to the filesystem in JSON format. It works well for simple deployments on a single node where you need persistence but do not want external dependencies like Redis.</p>
<pre><code class="language-javascript">import { Queue, FileStorage } from '@platformatic/job-queue'

const storage = new FileStorage('./queue-data')
const queue = new Queue({ storage })
</code></pre>
<p>The storage writes atomically to prevent corruption, and it maintains separate files for jobs, metadata, and locks. Since it relies on file system locks, it is not suitable for multi-node deployments.</p>
<h3><strong>RedisStorage</strong></h3>
<p>RedisStorage uses Redis (7+) or Valkey (8+) for distributed queue operations. This is the recommended choice for production workloads that require horizontal scaling, leader election, or cross-instance coordination.</p>
<pre><code class="language-javascript">import { Queue, RedisStorage } from '@platformatic/job-queue'
const storage = new RedisStorage({ connectionString: 'redis://localhost:6379' })
const queue = new Queue({ storage })
</code></pre>
<p>RedisStorage leverages Redis data structures for atomic operations:</p>
<ul>
<li><p>Lists for job queues</p>
</li>
<li><p>Sorted sets for delayed job scheduling</p>
</li>
<li><p>Pub/sub for notifications across instances</p>
</li>
<li><p>Lua scripts for atomic state changes</p>
</li>
</ul>
<p>For high availability, RedisStorage also supports Sentinel and Cluster modes for failover and sharding.</p>
<h3>Choosing the right backend</h3>
<img src="https://cdn.hashnode.com/uploads/covers/62bc139e9c913efac56c8de3/303d84e8-9bd9-423f-a90a-19b7d032527d.png" alt="" style="display:block;margin:0 auto" />

<p>Start with MemoryStorage for development, use FileStorage for simple single-node deployments, and choose RedisStorage for production systems that need horizontal scaling.</p>
<h2><strong>Reliability features that matter in production</strong></h2>
<p>The library is designed around the real failure modes of job processing systems.</p>
<p>Visualize this: you deploy a routine patch, and one of your job workers crashes unnoticed. By the next day, 5,000 critical jobs piled up and could have vanished forever. But thanks to built-in recovery, every one of them was automatically rescued. Situations like this are exactly where background processing systems prove their worth, thanks to strong safeguards.</p>
<h3><strong>Recovering stalled jobs</strong></h3>
<p>If a worker crashes while processing a job, the Reaper can detect the stalled work and requeue it after visibilityTimeout.</p>
<pre><code class="language-javascript">import { Reaper } from '@platformatic/job-queue'
const reaper = new Reaper({
 storage,
 visibilityTimeout: 30_000
})
await reaper.start()
</code></pre>
<p>For high availability, the Reaper also supports leader election (with Redis storage), so multiple instances can run safely while only one acts as leader at a time. If the leader goes away, another instance takes over, which helps avoid manual control during incidents.</p>
<h3><strong>Controlled retries and terminal states</strong></h3>
<p>Failed jobs can retry automatically up to maxRetries. When retries are exhausted, errors are persisted as a terminal state so producers can inspect or react programmatically.</p>
<p>This gives you reliable behavior for flaky dependencies, such as third-party APIs: transient failures recover automatically, while permanent failures remain visible and actionable.</p>
<h3><strong>Graceful shutdown</strong></h3>
<p>When stopping a worker, queue.stop() waits for in-flight jobs to finish. This reduces dropped work during deploys and restarts and helps keep queue state consistent across gradual updates. In practice, this means you can safely perform blue/green or canary deployments without worrying about losing in-progress work. Teams can ship changes faster, with the confidence that jobs will complete and customer data will not go missing, even as new versions are rolled out.</p>
<h2><strong>Request/response without building custom plumbing</strong></h2>
<p>One particularly useful capability is enqueueAndWait(). Teams often build this pattern manually on top of queues, but it is already integrated here, including timeout handling and typed errors.</p>
<pre><code class="language-javascript">try {
 const result = await queue.enqueueAndWait('invoice-123', payload, { timeout: 10_000 })
 return result
} catch (error) {
 // handle TimeoutError / JobFailedError, etc.
}
</code></pre>
<p>This is a good fit when work should run in a worker context, but the caller still needs a bounded response path, such as document generation, webhook fan-out, or expensive validation that should not run on an HTTP thread.</p>
<p>You also get explicit queue errors (TimeoutError, JobFailedError, and others), so your application can distinguish among transport problems, worker failures, and business-level errors.</p>
<h2><strong>Getting started</strong></h2>
<p>Install the package:</p>
<pre><code class="language-plaintext">npm install @platformatic/job-queue
</code></pre>
<p>Then choose a backend based on your environment:</p>
<ol>
<li><p>Start with MemoryStorage for local development.</p>
</li>
<li><p>Move to RedisStorage (Redis 7+ or Valkey 8+) for production.</p>
</li>
<li><p>Add Reaper when running multiple workers or when stalled-job recovery is required.</p>
</li>
</ol>
<p>If you already have queue infrastructure in place, one good migration approach is to move one bounded workflow first (for example, email delivery or report generation), validate behavior and observability, and then expand usage across other jobs.</p>
<p>We recommend separating responsibilities into dedicated processes:</p>
<ul>
<li><p><strong>Producer services</strong> enqueue jobs from HTTP handlers or internal events.</p>
</li>
<li><p><strong>Worker services</strong> execute jobs with tuned concurrency.</p>
</li>
<li><p><strong>A Reaper instance</strong> handles stalled-job recovery (or multiple instances with leader election).</p>
</li>
</ul>
<p>This setup lets you scale producers and workers independently. If incoming traffic spikes, add producers; if processing backlog grows, add workers.</p>
<h2><strong>Final thoughts</strong></h2>
<p><code>@platformatic/job-queue</code> is a practical option for Node.js teams that want reliable background processing without having to assemble every reliability feature from scratch. The combination of deduplication, request/response semantics, retries, and pluggable storage makes it flexible enough for both simple jobs and more demanding production workloads. Most importantly, it lets you focus on what matters most: building features and generating value, knowing your background tasks are handled with care. Imagine deployments where you can sleep soundly, confident that every job is accounted for and that no critical work is lost, even during outages. With the right foundation, you are set up not just for peace of mind, but for lasting success as your systems and team continue to grow.</p>
<p>If you are evaluating queue systems for your next service, this is a good time to try it and share feedback with the team (us). Real-world feedback is especially valuable while the project is still young and evolving quickly. If you run into an unexpected edge case or a strange retry failure, please open an issue describing your scenario: we love to fix hard problems. Concrete examples help us improve reliability for everyone!</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
