<?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>Sun, 20 Sep 2026 08:39:09 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/bf8ab33e-f186-408d-b099-2cff8747d25f.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>
  </channel>
</rss>
