<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Servet Arslan</title>
    <description>The latest articles on DEV Community by Servet Arslan (@serwetarslan).</description>
    <link>https://dev.to/serwetarslan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3944385%2Ff8f10c69-fe77-4cb2-8d76-4ff7347922f6.jpg</url>
      <title>DEV Community: Servet Arslan</title>
      <link>https://dev.to/serwetarslan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/serwetarslan"/>
    <language>en</language>
    <item>
      <title>How We Built a 40-Page Real-Time Dashboard with Next.js 16 and React Server Components</title>
      <dc:creator>Servet Arslan</dc:creator>
      <pubDate>Wed, 10 Jun 2026 10:32:33 +0000</pubDate>
      <link>https://dev.to/serwetarslan/how-we-built-a-40-page-real-time-dashboard-with-nextjs-16-and-react-server-components-2ie6</link>
      <guid>https://dev.to/serwetarslan/how-we-built-a-40-page-real-time-dashboard-with-nextjs-16-and-react-server-components-2ie6</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;We needed a dashboard for our webhook delivery platform. Not a simple admin panel — a full-blown monitoring dashboard with real-time analytics, endpoint management, team collaboration, and 40+ distinct pages.&lt;/p&gt;

&lt;p&gt;The requirements were brutal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real-time delivery stats updating every second&lt;/li&gt;
&lt;li&gt;SSE streaming for live event feeds&lt;/li&gt;
&lt;li&gt;Complex charts with 24h/7d/30d time ranges&lt;/li&gt;
&lt;li&gt;Role-based access (admin, editor, viewer)&lt;/li&gt;
&lt;li&gt;Dark mode (because obviously)&lt;/li&gt;
&lt;li&gt;Mobile responsive (because people check dashboards on their phones at 2am)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We chose Next.js 16. Here's why, and what we learned building it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Next.js 16?
&lt;/h2&gt;

&lt;p&gt;We evaluated a few options:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plain React + Vite&lt;/strong&gt; — Fast, simple, but no SSR. We needed SEO for our docs and landing pages, and server-side rendering for the dashboard's initial load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next.js 14/15&lt;/strong&gt; — Good, but we wanted the latest React Server Components patterns. Next.js 16 has better streaming, better caching, and the App Router is more mature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remix&lt;/strong&gt; — Interesting, but the ecosystem is smaller. We needed a UI component library with strong Next.js support, and shadcn/ui fits perfectly with Next.js.&lt;/p&gt;

&lt;p&gt;Next.js 16 won because of React Server Components. Here's why that matters for a dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  React Server Components: The Real Advantage
&lt;/h2&gt;

&lt;p&gt;The biggest misconception about RSC is that it's about performance. It's not — at least not primarily. The real advantage is &lt;strong&gt;data fetching architecture&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In a traditional React dashboard, you'd have something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Client component - fetches on every render&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DeliveryStats&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setStats&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/stats&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setStats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Skeleton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;StatsChart&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a waterfall: component renders → loading state → fetch → re-render. Every page has this pattern, and it gets messy fast.&lt;/p&gt;

&lt;p&gt;With RSC in Next.js 16:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Server component - fetches before render&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DeliveryStats&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getDeliveryStats&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;StatsChart&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No loading state. No useEffect. No waterfall. The data is ready before the component renders. For a dashboard with 15 different data panels on one page, this eliminates a massive amount of boilerplate.&lt;/p&gt;

&lt;p&gt;But here's the trick — you don't make everything a server component. The pattern we landed on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server components&lt;/strong&gt; for data fetching and layout&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client components&lt;/strong&gt; for interactive elements (charts, forms, real-time updates)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/dashboard/page.tsx (Server Component)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DashboardPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;recentDeliveries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nf"&gt;getEndpoints&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nf"&gt;getRecentDeliveries&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nf"&gt;getActiveAlerts&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DashboardLayout&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;StatsOverview&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;endpoints&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RecentDeliveriesStream&lt;/span&gt; &lt;span class="na"&gt;initialData&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;recentDeliveries&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AlertPanel&lt;/span&gt; &lt;span class="na"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;alerts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;DashboardLayout&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// components/RecentDeliveriesStream.tsx (Client Component)&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RecentDeliveriesStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;initialData&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;deliveries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setDeliveries&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initialData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eventSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/v1/stream/deliveries&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;eventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newDelivery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;setDeliveries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;newDelivery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;eventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;deliveries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DeliveryRow&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;delivery&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server component fetches the initial data. The client component takes it from there with SSE. Best of both worlds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-Time SSE Integration
&lt;/h2&gt;

&lt;p&gt;This was the hardest part. SSE (Server-Sent Events) with Next.js is tricky because Next.js wants to cache everything, and SSE is the opposite of cacheable.&lt;/p&gt;

&lt;p&gt;Our approach:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Separate SSE endpoint&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We don't serve SSE through Next.js. Our Rust API handles SSE directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The SSE connection goes directly to our API&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_API_URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useDeliveryStream&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eventSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v1/stream/deliveries`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;withCredentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;eventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delivery&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;delivery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="c1"&gt;// Update state&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;eventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Optimistic updates&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a user replays a webhook, we don't wait for the SSE event. We update the UI immediately and reconcile when the event arrives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleReplay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deliveryId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Optimistic update&lt;/span&gt;
  &lt;span class="nf"&gt;setDeliveries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;deliveryId&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;replaying&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Actual API call&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;replayDelivery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deliveryId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// SSE will update the status when it completes&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Connection status indicator&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SSE connections drop. We built a reconnecting EventSource wrapper with exponential backoff and a visual indicator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReconnectingEventSource&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;eventSource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EventSource&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;retryCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;maxRetryDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventSource&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retryCount&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxRetryDelay&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retryCount&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onopen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retryCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The 40-Page Architecture
&lt;/h2&gt;

&lt;p&gt;40 pages sounds like a lot, but most of them share patterns. Here's how we organized it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/
├── (auth)/
│   ├── login/
│   ├── register/
│   └── forgot-password/
├── (dashboard)/
│   ├── layout.tsx          # Shared dashboard layout
│   ├── page.tsx            # Overview (home)
│   ├── endpoints/
│   │   ├── page.tsx        # List
│   │   ├── [id]/
│   │   │   ├── page.tsx    # Detail
│   │   │   ├── edit/       # Edit form
│   │   │   └── deliveries/ # Delivery history
│   │   └── new/            # Create
│   ├── deliveries/
│   │   ├── page.tsx        # All deliveries
│   │   └── [id]/           # Delivery detail
│   ├── analytics/
│   │   ├── page.tsx        # Overview charts
│   │   ├── trends/         # Trend analysis
│   │   └── endpoints/      # Per-endpoint stats
│   ├── alerts/
│   │   ├── page.tsx        # Alert rules
│   │   └── [id]/           # Alert detail
│   ├── team/
│   │   ├── page.tsx        # Members
│   │   ├── invitations/    # Pending invites
│   │   └── settings/       # Team settings
│   └── settings/
│       ├── page.tsx        # General
│       ├── security/       # API keys, 2FA
│       ├── billing/        # Plans, invoices
│       └── notifications/  # Alert preferences
└── api/
    └── ...                 # API routes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: &lt;strong&gt;the layout does the heavy lifting&lt;/strong&gt;. The &lt;code&gt;(dashboard)/layout.tsx&lt;/code&gt; handles sidebar, navigation, breadcrumbs, and user context. Individual pages just focus on their content.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/(dashboard)/layout.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DashboardLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getCurrentUser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;team&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getTeam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;teamId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex h-screen"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Sidebar&lt;/span&gt; &lt;span class="na"&gt;team&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex-1 overflow-auto"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Breadcrumbs&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Authentication Flow
&lt;/h2&gt;

&lt;p&gt;We use NextAuth v5 with JWT tokens. The tricky part was handling token refresh in server components.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// middleware.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Check token expiry&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exp&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exp&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;refreshed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;refreshAccessToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;refreshed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For server components, we use &lt;code&gt;getToken()&lt;/code&gt; directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Any server component&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getToken&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next-auth/jwt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProtectedPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getProtectedData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DataView&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Don't fight the framework.&lt;/strong&gt; Next.js wants you to fetch data in server components. Let it. Trying to make everything a client component defeats the purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Streaming is your friend.&lt;/strong&gt; Use &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundaries aggressively. Load the shell first, stream in the heavy data panels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DashboardPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;StatsBar&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ChartSkeleton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DeliveryChart&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TableSkeleton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RecentDeliveries&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. shadcn/ui + Tailwind = fast iteration.&lt;/strong&gt; We built 40 pages in 3 weeks. The component library did the heavy lifting for consistent UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Server actions simplify mutations.&lt;/strong&gt; No need for API routes for simple form submissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;revalidatePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/endpoints&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Bundle size matters.&lt;/strong&gt; We use dynamic imports for heavy components (charts, code editors) to keep the initial load fast:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DeliveryChart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./DeliveryChart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ChartSkeleton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ssr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 16&lt;/strong&gt; (App Router, RSC)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; (strict mode)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS&lt;/strong&gt; + &lt;strong&gt;shadcn/ui&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recharts&lt;/strong&gt; for charts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NextAuth v5&lt;/strong&gt; for auth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSE&lt;/strong&gt; for real-time updates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel&lt;/strong&gt; for deployment&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;How do you handle real-time updates in your Next.js dashboards? I'm curious if anyone has a better SSE pattern.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>We Rewrote Our Webhook Platform from Go to Rust — Here's What Happened</title>
      <dc:creator>Servet Arslan</dc:creator>
      <pubDate>Wed, 10 Jun 2026 10:25:39 +0000</pubDate>
      <link>https://dev.to/serwetarslan/we-rewrote-our-webhook-platform-from-go-to-rust-heres-what-happened-3kif</link>
      <guid>https://dev.to/serwetarslan/we-rewrote-our-webhook-platform-from-go-to-rust-heres-what-happened-3kif</guid>
      <description>&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;Six months ago, our webhook delivery platform was running on Go. It worked fine for the first few months — we were processing around 50K webhooks per day, and Go's goroutines made concurrent delivery feel almost too easy.&lt;/p&gt;

&lt;p&gt;But then we started hitting walls.&lt;/p&gt;

&lt;p&gt;The first real problem showed up at around 200K daily deliveries. Our Go service was eating 2.8GB of memory just sitting idle, and during traffic spikes it would jump to 6GB+. We tried profiling, we tried optimizing, but every fix felt like a band-aid.&lt;/p&gt;

&lt;p&gt;The second problem was trickier: race conditions. Go makes concurrency easy to &lt;em&gt;start&lt;/em&gt; but hard to &lt;em&gt;get right&lt;/em&gt;. We had a bug where webhook deliveries were being marked as "success" before the response body was fully read. Took us two weeks to find it. The worst part? It only manifested under load, and Go's race detector didn't catch it because the shared state was hidden behind an interface.&lt;/p&gt;

&lt;p&gt;That's when we decided to try Rust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Rust Specifically?
&lt;/h2&gt;

&lt;p&gt;Three reasons:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Memory safety without garbage collection.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Go's GC is fine for most things, but when you're processing thousands of HTTP requests per second with large payloads, GC pauses become noticeable. Our p99 latency was 340ms on Go. We suspected a chunk of that was GC-related.&lt;/p&gt;

&lt;p&gt;Rust's ownership model means no GC, no pauses, and predictable memory usage. The compiler catches the bugs that the Go race detector misses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Performance headroom.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Webhook delivery is fundamentally I/O bound, but we do a lot of work around payload validation, HMAC signature computation, and JSON transformation. Rust's zero-cost abstractions mean we can write clean code that compiles down to tight machine code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The type system.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This one surprised me. Rust's type system isn't just about safety — it's a design tool. Sum types, pattern matching, and the &lt;code&gt;Result&lt;/code&gt;/&lt;code&gt;Option&lt;/code&gt; types force you to handle every error path. In Go, we had &lt;code&gt;if err != nil&lt;/code&gt; everywhere but still missed edge cases. In Rust, the compiler literally won't let you forget.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Rewrite: What We Changed
&lt;/h2&gt;

&lt;p&gt;We didn't rewrite everything at once. Here's what we did:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1: Core delivery engine (2 weeks)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The webhook delivery logic — HTTP client, retry logic, signature verification — went into Rust first. This is where performance matters most.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;deliver_webhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DeliveryResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DeliveryError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compute_hmac_signature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;
        &lt;span class="nf"&gt;.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="py"&gt;.url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-HookSniff-Signature"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.send&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="nf"&gt;.status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;OK&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CREATED&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ACCEPTED&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;DeliveryResult&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nn"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;TOO_MANY_REQUESTS&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;INTERNAL_SERVER_ERROR&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;BAD_GATEWAY&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SERVICE_UNAVAILABLE&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;GATEWAY_TIMEOUT&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;DeliveryResult&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Retryable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="nf"&gt;.status&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;DeliveryResult&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Failed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how the return type makes it impossible to forget handling the error case. The compiler enforces this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2: API layer (1 week)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We kept the REST API in Rust too, using Axum. The routing is clean, middleware is composable, and it plays well with tokio's async runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3: Migration (1 week)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We ran Go and Rust side by side for a week, comparing outputs. Any discrepancy was logged and fixed. Then we switched traffic gradually — 10%, 25%, 50%, 100%.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Results
&lt;/h2&gt;

&lt;p&gt;Here's what changed after the rewrite:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Go&lt;/th&gt;
&lt;th&gt;Rust&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Memory (idle)&lt;/td&gt;
&lt;td&gt;2.8 GB&lt;/td&gt;
&lt;td&gt;380 MB&lt;/td&gt;
&lt;td&gt;-86%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory (peak)&lt;/td&gt;
&lt;td&gt;6.2 GB&lt;/td&gt;
&lt;td&gt;1.1 GB&lt;/td&gt;
&lt;td&gt;-82%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p50 latency&lt;/td&gt;
&lt;td&gt;45ms&lt;/td&gt;
&lt;td&gt;12ms&lt;/td&gt;
&lt;td&gt;-73%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p99 latency&lt;/td&gt;
&lt;td&gt;340ms&lt;/td&gt;
&lt;td&gt;38ms&lt;/td&gt;
&lt;td&gt;-89%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU usage&lt;/td&gt;
&lt;td&gt;72%&lt;/td&gt;
&lt;td&gt;31%&lt;/td&gt;
&lt;td&gt;-57%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deliveries/sec&lt;/td&gt;
&lt;td&gt;2,400&lt;/td&gt;
&lt;td&gt;8,200&lt;/td&gt;
&lt;td&gt;+242%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The memory numbers are what hit me hardest. We went from needing a 8GB instance to running comfortably on 2GB. That's not just a performance win — it changes your deployment economics completely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hard Parts
&lt;/h2&gt;

&lt;p&gt;Rust isn't all sunshine. Here's what sucked:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The borrow checker is brutal at first.&lt;/strong&gt; I've been writing code for 12 years and the first two weeks with Rust felt like being a beginner again. You fight the compiler constantly. But around week three, something clicked. I started &lt;em&gt;thinking&lt;/em&gt; in ownership, and the fights became rare.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Async Rust has a learning curve.&lt;/strong&gt; Tokio is powerful but the ecosystem is fragmented. &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;.await&lt;/code&gt; is clean, but lifetimes in async contexts can be confusing. We had a few deadlock-like situations early on that took days to debug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compile times.&lt;/strong&gt; Our release build takes 4 minutes. Incremental debug builds are ~15 seconds, which is fine, but full rebuilds during CI are painful. We mitigated this with &lt;code&gt;cargo check&lt;/code&gt; in pre-commit hooks and &lt;code&gt;sccache&lt;/code&gt; in CI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ecosystem is younger.&lt;/strong&gt; Go has a library for everything. Rust is catching up fast, but we had to write a few things ourselves — like our webhook retry scheduler with exponential backoff and jitter. In Go, there are 10 crates for that. In Rust, we wrote our own in ~200 lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Would I Do It Again?
&lt;/h2&gt;

&lt;p&gt;Absolutely. The performance gains alone justified the rewrite, but the real win is confidence. When our Rust code compiles, I trust it in a way I never trusted our Go code. The type system catches entire categories of bugs that Go's runtime checks miss.&lt;/p&gt;

&lt;p&gt;If you're building something I/O heavy with strict reliability requirements — webhook delivery, message queues, API gateways — Rust is worth the learning curve. Just budget extra time for the first month. You'll be fighting the compiler a lot. But once you get past that hump, it's a different way of thinking about code.&lt;/p&gt;

&lt;p&gt;Our platform is open source if you want to poke around: &lt;a href="https://github.com/servetarslan02/HookSniff" rel="noopener noreferrer"&gt;HookSniff on GitHub&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's your experience with Rust vs Go for backend services? I'd love to hear how others handled the migration.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>rust</category>
      <category>go</category>
      <category>webdev</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Servet Arslan</dc:creator>
      <pubDate>Fri, 05 Jun 2026 22:55:19 +0000</pubDate>
      <link>https://dev.to/serwetarslan/-1h7e</link>
      <guid>https://dev.to/serwetarslan/-1h7e</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/serwetarslan/your-webhooks-are-failing-silently-heres-how-we-fixed-itpublished-false-2ajd" class="crayons-story__hidden-navigation-link"&gt;Your Webhooks Are Failing Silently — Here's How We Fixed It&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/serwetarslan" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3944385%2Ff8f10c69-fe77-4cb2-8d76-4ff7347922f6.jpg" alt="serwetarslan profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/serwetarslan" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Servet Arslan
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Servet Arslan
                
              
              &lt;div id="story-author-preview-content-3818985" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/serwetarslan" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3944385%2Ff8f10c69-fe77-4cb2-8d76-4ff7347922f6.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Servet Arslan&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/serwetarslan/your-webhooks-are-failing-silently-heres-how-we-fixed-itpublished-false-2ajd" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jun 4&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/serwetarslan/your-webhooks-are-failing-silently-heres-how-we-fixed-itpublished-false-2ajd" id="article-link-3818985"&gt;
          Your Webhooks Are Failing Silently — Here's How We Fixed It
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/rust"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;rust&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/saas"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;saas&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webhook"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webhook&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/serwetarslan/your-webhooks-are-failing-silently-heres-how-we-fixed-itpublished-false-2ajd" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/serwetarslan/your-webhooks-are-failing-silently-heres-how-we-fixed-itpublished-false-2ajd#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Your Webhooks Are Failing Silently — Here's How We Fixed It</title>
      <dc:creator>Servet Arslan</dc:creator>
      <pubDate>Thu, 04 Jun 2026 11:09:19 +0000</pubDate>
      <link>https://dev.to/serwetarslan/your-webhooks-are-failing-silently-heres-how-we-fixed-itpublished-false-2ajd</link>
      <guid>https://dev.to/serwetarslan/your-webhooks-are-failing-silently-heres-how-we-fixed-itpublished-false-2ajd</guid>
      <description>&lt;h1&gt;
  
  
  Your Webhooks Are Failing Silently — Here's How We Fixed It
&lt;/h1&gt;

&lt;p&gt;Last week I read through 50+ Dev.to posts about webhook problems. The same complaints came up over and over:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"My Stripe webhook was returning 500s for 3 days. Nobody told me."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"I have webhooks from 5 providers across 3 projects. I'm not checking 15 dashboards every morning."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"The endpoint processed the same payment twice because my retry logic was broken."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sound familiar? You're not alone. Webhooks are the backbone of modern integrations, but the tooling around them is broken. So we built &lt;a href="https://hooksniff.vercel.app" rel="noopener noreferrer"&gt;HookSniff&lt;/a&gt; — a webhook platform that addresses the exact problems developers complain about most.&lt;/p&gt;

&lt;p&gt;Here are the 5 biggest webhook frustrations developers face, and how we solved each one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 1: Webhooks Fail Silently (And Nobody Tells You)
&lt;/h2&gt;

&lt;p&gt;This is the #1 complaint across every forum, blog post, and Reddit thread about webhooks. Your endpoint goes down, the sender retries a few times, gives up, and moves on. You find out days later when a customer complains they were charged but never got access.&lt;/p&gt;

&lt;p&gt;One developer on Dev.to shared this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"A payment webhook broke in production. Three days of failed deliveries. Customers charged but never activated. One client gone for good."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The problem isn't that webhooks fail — they will. The problem is that &lt;strong&gt;nobody tells you&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How HookSniff fixes it:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every single delivery is tracked in real-time. You get a dashboard showing success rates, failure streaks, and latency percentiles — per endpoint. If your success rate drops below your configured threshold, you get an alert. Not tomorrow. Now.&lt;/p&gt;

&lt;p&gt;We track delivery status at every stage: &lt;code&gt;pending&lt;/code&gt; → &lt;code&gt;in_progress&lt;/code&gt; → &lt;code&gt;delivered&lt;/code&gt; or &lt;code&gt;failed&lt;/code&gt;. If a delivery fails, it moves to the dead letter queue automatically. Nothing gets silently dropped.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frbqotptvp1y1690r4pf4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frbqotptvp1y1690r4pf4.png" alt=" " width="800" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Every delivery is visible. Every failure is tracked. Nothing disappears.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9bhgr21d2f0xho6y3s1w.png" alt=" " width="800" height="449"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Problem 2: "Just Check the Dashboard" Doesn't Scale
&lt;/h2&gt;

&lt;p&gt;The second most common frustration: visibility. Developers are receiving webhooks from Stripe, GitHub, SendGrid, Shopify, and more — across multiple projects. The suggestion to "just check the dashboard" means checking 10+ dashboards every single day.&lt;/p&gt;

&lt;p&gt;Nobody does that.&lt;/p&gt;

&lt;p&gt;What developers actually want (we found this exact list repeated in multiple posts):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One place&lt;/strong&gt; to see all webhook endpoints across all projects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instant alerts&lt;/strong&gt; when something fails&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery logs&lt;/strong&gt; with response codes, latency, and payload details&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Success rate trends&lt;/strong&gt; over time (24h / 7d / 30d)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How HookSniff fixes it:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;HookSniff gives you a single dashboard for everything. Endpoint health cards show success rate, p95/p99 latency, and failure streaks at a glance. The analytics page shows delivery trends with configurable time ranges.&lt;/p&gt;

&lt;p&gt;No more jumping between Stripe's dashboard, GitHub's webhook logs, and your own error tracker. One place. One login.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvyz24s84mjjfxhtiurc1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvyz24s84mjjfxhtiurc1.png" alt=" " width="799" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All your endpoints, all their health metrics, one screen.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 3: Retry Logic Is Dangerously Half-Baked
&lt;/h2&gt;

&lt;p&gt;This one came up in a viral Dev.to post titled &lt;em&gt;"Most Webhook Implementations Are Dangerously Half-Baked"&lt;/em&gt; — and the comments were full of people agreeing.&lt;/p&gt;

&lt;p&gt;The core issue: most developers treat all failures the same. A 500 error and a 400 error get the same retry treatment. But they're completely different problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;500 (server error):&lt;/strong&gt; The endpoint had a temporary issue. Retry with backoff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;400 (bad request):&lt;/strong&gt; Your payload is wrong. Retrying 12 times won't fix it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;429 (rate limited):&lt;/strong&gt; Back off and retry after the specified window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeout:&lt;/strong&gt; The endpoint is slow. Retry, but maybe with a longer timeout.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you retry a 400 twelve times, you're just hammering someone's server with a broken payload. If you don't retry a 500, you're losing events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How HookSniff fixes it:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every failed delivery gets classified automatically:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Response&lt;/th&gt;
&lt;th&gt;Classification&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2xx&lt;/td&gt;
&lt;td&gt;&lt;code&gt;delivered&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Success — no retry needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;400/401/403&lt;/td&gt;
&lt;td&gt;&lt;code&gt;permanent_error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stop immediately — fix your payload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;408/429&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rate_limited&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retry after backoff window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5xx&lt;/td&gt;
&lt;td&gt;&lt;code&gt;server_error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Exponential backoff with jitter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeout&lt;/td&gt;
&lt;td&gt;&lt;code&gt;timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retry with increasing timeout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS/TLS&lt;/td&gt;
&lt;td&gt;&lt;code&gt;network_error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retry with backoff&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Retries use exponential backoff with ±25% jitter (to prevent thundering herd), configurable per endpoint. You can set max retry attempts, base delay, and max delay individually for each endpoint.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Every attempt is logged with status code, latency, response body, and classification.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgiihspre0v4y0k6awpfg.png" alt=" " width="800" height="282"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Problem 4: Duplicate Deliveries Break Everything
&lt;/h2&gt;

&lt;p&gt;Webhook providers guarantee &lt;strong&gt;at-least-once delivery&lt;/strong&gt;, not exactly-once. This means your handler will receive the same event multiple times — during retries, network blips, or provider-side issues.&lt;/p&gt;

&lt;p&gt;If your handler charges a card, creates a database record, or sends an email on every delivery, duplicates are a real problem. One developer shared:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"The endpoint processed the same payment twice because the response got lost in transit. The retry kicked in and the customer was charged twice."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;How HookSniff fixes it:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two layers of protection:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Idempotency keys on send:&lt;/strong&gt; When you send a webhook through HookSniff, you can include an &lt;code&gt;Idempotency-Key&lt;/code&gt; header. If the same key is seen within 24 hours, the second request returns the same response without creating a new delivery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Webhook-ID on delivery:&lt;/strong&gt; Every delivery gets a unique &lt;code&gt;webhook-id&lt;/code&gt; header. Your handler should track these and skip processing for IDs it's already seen. We provide the standard headers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;webhook-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;msg_abc123&lt;/span&gt;
&lt;span class="na"&gt;webhook-timestamp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1717500000&lt;/span&gt;
&lt;span class="na"&gt;webhook-signature&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1,sha256=abc123...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This follows the &lt;a href="https://www.standardwebhooks.com/" rel="noopener noreferrer"&gt;Standard Webhooks&lt;/a&gt; specification — the same approach used by Svix, Stripe, and other major providers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 5: Existing Solutions Are Too Expensive
&lt;/h2&gt;

&lt;p&gt;This is the elephant in the room. Let's look at what's available:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;What You Get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Svix&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$490/mo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Great product, but 10x the price&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hookdeck&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$39/mo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No FIFO ordering, no endpoint throttling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hook0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;€59/mo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;EU-focused, limited SDKs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For indie developers and small teams, $490/month for a webhook platform is a non-starter. And building it yourself means months of work on retry logic, signature verification, dead letter queues, monitoring, and alerting — time you could spend on your actual product.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6xgxfk8s00bm1609zvj2.jpg" alt=" " width="800" height="459"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  What Else Developers Asked For (And We Built)
&lt;/h2&gt;

&lt;p&gt;Beyond the top 5, here are other features that came up repeatedly in developer discussions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔐 Signature verification that actually works.&lt;/strong&gt; HMAC-SHA256 with &lt;code&gt;whsec_&lt;/code&gt; secrets, following the Standard Webhooks spec. Constant-time comparison to prevent timing attacks. Not optional — every delivery is signed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📊 FIFO ordering.&lt;/strong&gt; Critical for event-sourced systems. Sequence numbers guarantee your events arrive in order. Only Svix also offers this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔀 Smart routing.&lt;/strong&gt; Round-robin, failover, weighted, and random strategies. If your primary endpoint fails, traffic automatically shifts to a fallback URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🛡️ SSRF protection.&lt;/strong&gt; Blocks private IPs, metadata endpoints, and DNS rebinding attacks out of the box. No configuration needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔌 Inbound webhook proxy.&lt;/strong&gt; Receive webhooks from Stripe, GitHub, and Shopify through a single normalized endpoint. No other platform offers this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📦 11 SDKs (in development).&lt;/strong&gt; Node.js, Python, Go, Rust, Ruby, Java, Kotlin, PHP, C#, Elixir, and Swift.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It Today
&lt;/h2&gt;

&lt;p&gt;No vendor lock-in. No surprise pricing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🌐 &lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://hooksniff.vercel.app" rel="noopener noreferrer"&gt;hooksniff.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 &lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://hooksniff.vercel.app/docs" rel="noopener noreferrer"&gt;hooksniff.vercel.app/docs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've been burned by silent webhook failures, give it a try. If you're building webhook infrastructure from scratch, save yourself the months of work.&lt;/p&gt;

&lt;p&gt;And if you have webhook horror stories — drop them in the comments. We've all been there. 🪝&lt;/p&gt;




&lt;p&gt;*Built with ❤️ and Rust.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>devops</category>
      <category>saas</category>
      <category>webhook</category>
    </item>
    <item>
      <title>What is a Webhook? A Complete Guide for Beginners</title>
      <dc:creator>Servet Arslan</dc:creator>
      <pubDate>Thu, 21 May 2026 15:18:35 +0000</pubDate>
      <link>https://dev.to/serwetarslan/what-is-a-webhook-a-complete-guide-for-beginners-334l</link>
      <guid>https://dev.to/serwetarslan/what-is-a-webhook-a-complete-guide-for-beginners-334l</guid>
      <description>&lt;p&gt;If you have ever wondered what a webhook is and how it works, this guide is for you. We will explain webhooks in simple terms, show real-world examples, and help you understand why they are essential for modern applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Simple Explanation
&lt;/h2&gt;

&lt;p&gt;Think of a webhook like a pizza delivery notification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without webhooks (polling):&lt;/strong&gt; You keep calling the pizza place asking "Is my pizza ready?" every 5 minutes. Wastes your time and theirs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With webhooks:&lt;/strong&gt; The pizza place calls YOU when your pizza is ready. You get notified instantly, no wasted calls.&lt;/p&gt;

&lt;p&gt;That is exactly how webhooks work in software. Instead of your application constantly asking a server "Is there new data?", the server sends your application a notification when something happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Webhooks Work
&lt;/h2&gt;

&lt;p&gt;Here is the step-by-step flow:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: You Register a URL
&lt;/h3&gt;

&lt;p&gt;You tell a service: "When something happens, send data to this URL." This URL is your webhook endpoint — a piece of code on your server that listens for incoming requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Something Happens
&lt;/h3&gt;

&lt;p&gt;An event occurs in the service — a payment succeeds, a user signs up, an order ships, a code commit is pushed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: The Service Sends a POST Request
&lt;/h3&gt;

&lt;p&gt;The service sends an HTTP POST request to your URL with the event data (called a "payload"). This happens automatically, in real-time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Your Server Processes It
&lt;/h3&gt;

&lt;p&gt;Your server receives the data and takes action — updates a database, sends an email, triggers a workflow, or notifies a user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Examples
&lt;/h2&gt;

&lt;p&gt;Here are common webhook use cases you probably encounter every day:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Payment notifications&lt;/strong&gt; — Stripe sends a webhook when a payment succeeds or fails&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD pipelines&lt;/strong&gt; — GitHub sends a webhook when code is pushed, triggering a build&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chat bots&lt;/strong&gt; — Slack or Discord sends a webhook when a message is posted&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;E-commerce&lt;/strong&gt; — Order created, shipped, delivered — each triggers a webhook&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI agents&lt;/strong&gt; — An AI agent sends a webhook when a task completes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring&lt;/strong&gt; — An alert system sends a webhook when a server goes down&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Webhook vs API vs Polling
&lt;/h2&gt;

&lt;p&gt;Understanding the difference between these approaches is crucial:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Polling&lt;/th&gt;
&lt;th&gt;Webhook&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Direction&lt;/td&gt;
&lt;td&gt;You check periodically&lt;/td&gt;
&lt;td&gt;Server notifies you&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timing&lt;/td&gt;
&lt;td&gt;Seconds to minutes&lt;/td&gt;
&lt;td&gt;Milliseconds (real-time)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Efficiency&lt;/td&gt;
&lt;td&gt;Wastes bandwidth&lt;/td&gt;
&lt;td&gt;Only sends when needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complexity&lt;/td&gt;
&lt;td&gt;Simple to implement&lt;/td&gt;
&lt;td&gt;Needs endpoint setup&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  When to Use Polling
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The API does not support webhooks&lt;/li&gt;
&lt;li&gt;You need very infrequent updates (daily or weekly)&lt;/li&gt;
&lt;li&gt;You are building a quick prototype&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When to Use Webhooks
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You need real-time updates&lt;/li&gt;
&lt;li&gt;High volume of events&lt;/li&gt;
&lt;li&gt;You want to reduce API calls&lt;/li&gt;
&lt;li&gt;Building production integrations&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Webhook Security
&lt;/h2&gt;

&lt;p&gt;Webhooks are sent over HTTP, so anyone can send a request to your URL. You need to verify that the request actually came from the expected service.&lt;/p&gt;

&lt;h3&gt;
  
  
  HMAC Signatures
&lt;/h3&gt;

&lt;p&gt;The most common verification method. The service signs the payload with a secret key. You verify the signature on your end using the same secret.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTTPS Only
&lt;/h3&gt;

&lt;p&gt;Always use HTTPS for your webhook endpoints. Never accept webhooks over plain HTTP in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timestamp Validation
&lt;/h3&gt;

&lt;p&gt;Check the timestamp in the webhook header. Reject requests older than 5 minutes to prevent replay attacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started with Webhooks
&lt;/h2&gt;

&lt;p&gt;The easiest way to start with webhooks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create an endpoint&lt;/strong&gt; on your server (a URL that accepts POST requests)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Register that URL&lt;/strong&gt; with the service that will send webhooks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify the webhook signature&lt;/strong&gt; in your endpoint&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Process the event data&lt;/strong&gt; and take action&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return a 200 status code&lt;/strong&gt; to acknowledge receipt&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Pro Tip
&lt;/h3&gt;

&lt;p&gt;Use a webhook service like &lt;a href="https://hooksniff.vercel.app" rel="noopener noreferrer"&gt;HookSniff&lt;/a&gt; to handle retries, security, and monitoring — so you can focus on your product. HookSniff delivers webhooks reliably with automatic retries, HMAC-SHA256 signatures, and a real-time dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Webhook Challenges
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Failed Deliveries
&lt;/h3&gt;

&lt;p&gt;What happens when your server is down? Without a retry system, the webhook is lost. Services like HookSniff automatically retry failed deliveries with exponential backoff.&lt;/p&gt;

&lt;h3&gt;
  
  
  Duplicate Deliveries
&lt;/h3&gt;

&lt;p&gt;Webhooks can be delivered more than once. Always use idempotency keys to prevent duplicate processing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ordering
&lt;/h3&gt;

&lt;p&gt;Events might arrive out of order. Use sequence numbers or timestamps to handle this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Debugging
&lt;/h3&gt;

&lt;p&gt;When something goes wrong, you need visibility. A delivery dashboard shows every attempt, status code, and response body.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Webhooks are the foundation of modern event-driven architecture. They enable real-time integrations between services without the overhead of constant polling.&lt;/p&gt;

&lt;p&gt;Whether you are building a payment integration, a CI/CD pipeline, or an AI agent system, understanding webhooks is essential.&lt;/p&gt;

&lt;p&gt;Ready to start using webhooks? &lt;a href="https://hooksniff.vercel.app/register" rel="noopener noreferrer"&gt;Get started with HookSniff for free&lt;/a&gt; — 10,000 webhooks per month, no credit card required.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://hooksniff.vercel.app/blog/what-is-a-webhook" rel="noopener noreferrer"&gt;hooksniff.vercel.app/blog/what-is-a-webhook&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
    </item>
  </channel>
</rss>
