<?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" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[bunny.net Blog]]></title><description><![CDATA[Performance tips, updates, tricks and insights into our Content Delivery Network.]]></description><link>https://bunny.net/blog/</link><image><url>https://bunny.net/blog/favicon.png</url><title>bunny.net Blog</title><link>https://bunny.net/blog/</link></image><generator>Ghost 5.82</generator><lastBuildDate>Sun, 25 Oct 2026 18:27:05 GMT</lastBuildDate><atom:link href="https://bunny.net/blog/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[CVE-2026-109618 and CVE-2026-109619: Bunny Shield's response to critical PHP vulnerabilities]]></title><description><![CDATA[Bunny Shield is providing an additional layer of protection while customers prepare to update]]></description><link>https://bunny.net/blog/cve-2026-109618-and-cve-2026-109619/</link><guid isPermaLink="false">6ad893938b5fed851dd541c4</guid><category><![CDATA[Security]]></category><dc:creator><![CDATA[Joe Connolly]]></dc:creator><pubDate>Wed, 21 Oct 2026 10:30:34 GMT</pubDate><media:content url="https://bunny.net/blog/content/images/2026/10/image--37-.png" medium="image"/><content:encoded><![CDATA[<blockquote><strong>Update - 21 October, 2026 14:15 UTC:</strong> PHP has published its security releases and technical advisories. We have updated this post with the fixed versions and added two managed WAF rules for multipart request patterns described in the advisory for CVE-2026-109618. Our earlier upload-progress rule targets one confirmed path for CVE-2026-109619. Matching requests are blocked only when your Shield Zone&apos;s WAF is in blocking mode; log-only mode does not block them.</blockquote><img src="https://bunny.net/blog/content/images/2026/10/image--37-.png" alt="CVE-2026-109618 and CVE-2026-109619: Bunny Shield&apos;s response to critical PHP vulnerabilities"><p>The PHP security team has announced an emergency release to address two critical vulnerabilities, each rated <strong>9.2 under CVSS v4.0</strong>. Researchers are tracking the reported issues as <strong>CVE-2026-109618</strong> and <strong>CVE-2026-109619</strong>. PHP plans to publish its security release on <strong>October 21, 2026 at 14:00 UTC</strong> and has urged users to prepare to update promptly. Read <a href="https://www.php.net/archive/2026.php" rel="noopener noreferrer">PHP&apos;s security announcement</a> and its advisories for <a href="https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w" rel="noopener noreferrer">CVE-2026-109618</a> and <a href="https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp" rel="noopener noreferrer">CVE-2026-109619</a>.</p><p>Bunny Shield has expanded its managed WAF coverage with two additional multipart rules, alongside the upload-progress rule we activated ahead of the release. These rules target specific request patterns described in PHP&apos;s advisories while customers update their PHP installations. <strong>They block matching requests only when your Shield Zone&apos;s WAF is in blocking mode.</strong></p><h2 id="understanding-the-vulnerabilities">Understanding the vulnerabilities</h2><h3 id="cve-2026-109618rce-via-oom-corruption">CVE-2026-109618 - RCE via OOM corruption</h3><p>PHP&apos;s out-of-memory handling can interrupt a request while internal data is only partly initialized. Cleanup can then use that invalid data. PHP has demonstrated unauthenticated code execution against <code>mod_php</code> before application code runs. Its advisory describes several ways multipart requests can drive memory exhaustion, including requests without a usable declared length and deeply nested field names.</p><h3 id="cve-2026-109619rce-via-stale-global-state">CVE-2026-109619 - RCE via stale global state</h3><p>Request-owned state can remain accessible after its memory is freed. A later request handled by the same worker may encounter that stale state before PHP resets it. PHP confirms a session upload-progress path, as well as other paths involving fatal errors and early initialization. Blocking the default upload-progress field addresses one request pattern; it does not cover every path to this vulnerability.</p><h2 id="fixed-php-versions">Fixed PHP versions</h2>
<!--kg-card-begin: html-->
<table style="border:1px solid rgba(24, 61, 109, 0.18); border-collapse:collapse; width:100%;">
  <thead>
    <tr style="font-weight:700; text-align:left; background:#223c6a; color:white;">
      <th style="padding:12px 14px; background:rgba(255,255,255,0.03); border-bottom:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); color:white;">PHP branch</th>
      <th style="padding:12px 14px; border-bottom:1px solid rgba(24, 61, 109, 0.18); color:white;">Fixed release</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="padding:12px 14px; border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">8.5</td>
      <td style="padding:12px 14px; vertical-align:middle; color:#183d6d;"><strong>8.5.12</strong></td>
    </tr>
    <tr>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">8.4</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;"><strong>8.4.27</strong></td>
    </tr>
    <tr>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">8.3</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;"><strong>8.3.36</strong></td>
    </tr>
    <tr>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">8.2</td>
      <td style="padding:12px 14px; vertical-align:middle; color:#183d6d;"><strong>8.2.35</strong></td>
    </tr>
  </tbody>
</table>
<!--kg-card-end: html-->
<h2 id="how-bunny-shield-protects-your-site">How Bunny Shield protects your site</h2><p>Three managed WAF rules now address request patterns associated with the confirmed paths:</p>
<!--kg-card-begin: html-->
<table style="border:1px solid rgba(24, 61, 109, 0.18); border-collapse:collapse; width:100%;">
  <thead>
    <tr style="font-weight:700; text-align:left; background:#223c6a; color:white;">
      <th style="padding:12px 14px; background:rgba(255,255,255,0.03); border-bottom:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); color:white;">Managed rule</th>
      <th style="padding:12px 14px; border-bottom:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); color:white;">What it matches</th>
      <th style="padding:12px 14px; border-bottom:1px solid rgba(24, 61, 109, 0.18); color:white;">Relevant path</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="padding:12px 14px; border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;"><code style="color:#c0392b; background:rgba(24, 61, 109, 0.05); padding:1px 5px; border-radius:3px; font-family:monospace;">WAF-PHP-UPLOAD-PROGRESS-POC</code></td>
      <td style="padding:12px 14px; border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Multipart requests containing the default <code style="color:#c0392b; background:rgba(24, 61, 109, 0.05); padding:1px 5px; border-radius:3px; font-family:monospace;">PHP_SESSION_UPLOAD_PROGRESS</code> field</td>
      <td style="padding:12px 14px; vertical-align:middle; color:#183d6d;">The confirmed session upload-progress path for CVE-2026-109619</td>
    </tr>
    <tr>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;"><code style="color:#c0392b; background:rgba(24, 61, 109, 0.05); padding:1px 5px; border-radius:3px; font-family:monospace;">WAF-PHP-MULTIPART-NESTING</code></td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Multipart field names nested <strong>20 or more levels</strong></td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">The nested-input memory pressure described for CVE-2026-109618</td>
    </tr>
  </tbody>
</table>
<!--kg-card-end: html-->
<h2 id="what-you-should-do-now">What you should do now</h2><ul><li><strong>Identify every PHP origin and version</strong> serving your websites, including sites behind content management systems and custom applications.</li><li><strong>Prepare to install PHP&apos;s fixed releases</strong> when the official advisory is published. Confirm the effective runtime version after updating; a WAF is an additional layer, not a substitute for the software fix.</li><li><strong>Check whether your application uses session upload progress.</strong> If it does not, consider disabling <code>session.upload_progress.enabled</code> in the effective PHP configuration. PHP <a href="https://www.php.net/manual/en/session.configuration.php">documents this setting</a>; turning it off prevents that feature from recording progress, but does not disable multipart upload parsing or establish a fix for either CVE.</li><li><strong>Keep Bunny Shield enabled and review security events</strong> for blocked requests. If your application relies on PHP session upload progress, test that workflow and contact support if it is affected.</li></ul><h2 id="protection-while-you-update">Protection while you update</h2><p>Security releases can take time to reach every server, especially across large fleets and managed hosting environments. Bunny Shield is already blocking the public request pattern before it reaches an origin while teams complete their PHP update rollout. We will keep monitoring the official advisory and new exploit research, then refine our protections as verified details become available.</p><p>To get started with Bunny Shield, <a href="https://dash.bunny.net/auth/login">log in</a> to your bunny.net account or <a href="https://dash.bunny.net/auth/register">sign up</a>. Keep PHP updated even when your site is protected at the edge.When your Shield Zone&apos;s WAF is in <strong>blocking mode</strong>, Bunny Shield blocks matching requests before they reach your origin. In <strong>log-only mode</strong>, these rules do <strong>not</strong> block requests. We respect the mode configured for your zone.</p><p>These rules reduce exposure to the request patterns they match; they are not complete mitigations for either CVE. Other routes to memory exhaustion or stale state are possible, and a customized upload-progress field name will not match the default-field rule. Applications using PHP session upload progress or unusually deep form fields may have legitimate requests matched by these rules. Review affected workflows and your Shield security events.</p><h2 id="what-you-should-do-now-1">What you should do now</h2><ul><li><strong>Patch PHP immediately</strong> to the fixed release for your branch, or install your distribution&apos;s verified backport. Check every PHP origin and restart workers as required by your deployment.</li><li><strong>Check your Bunny Shield WAF mode.</strong> Use blocking mode if you want these managed rules to stop matching requests. Log-only mode will not block them.</li><li><strong>Review upload workflows and security events</strong>, particularly if your application uses session upload progress or deeply nested fields.</li><li><strong>Follow PHP&apos;s temporary guidance if you cannot update at once.</strong> Its advisory covers administrative memory and request-size limits, and disabling <code>session.upload_progress.enabled</code> with a worker restart for the confirmed session path. These measures reduce exposure to particular paths; PHP says updating is the only complete solution.</li></ul><h2 id="protection-while-you-update-1">Protection while you update</h2><p>Bunny Shield can block the two identified request patterns at the edge for customers using WAF blocking mode. We&apos;ll continue reviewing PHP&apos;s advisories and emerging attack techniques and refine our managed rules as needed. <strong>Install the PHP security release even if Bunny Shield is blocking traffic</strong>: the vulnerabilities have paths outside these WAF matches.</p><p>To get started with Bunny Shield, <a href="https://dash.bunny.net/auth/login">log in</a> to your <a href="http://bunny.net">bunny.net</a> account or <a href="https://dash.bunny.net/auth/register">sign up</a>.</p>]]></content:encoded></item><item><title><![CDATA[Introducing the Bunny Stream SDK for React Native and Expo]]></title><description><![CDATA[Built on our iOS and Android SDKs, the new Bunny Stream SDK for React Native and Expo brings native playback, resumable uploads, camera capture, and live streaming together behind one TypeScript API]]></description><link>https://bunny.net/blog/introducing-the-bunny-stream-sdk-for-react-native-and-expo/</link><guid isPermaLink="false">6ad237ba8b5fed851dd54183</guid><category><![CDATA[News]]></category><dc:creator><![CDATA[Nathan Zerafa]]></dc:creator><pubDate>Fri, 16 Oct 2026 14:55:03 GMT</pubDate><media:content url="https://bunny.net/blog/content/images/2026/10/bunny-React-Native-mobile-SDK.png" medium="image"/><content:encoded><![CDATA[<img src="https://bunny.net/blog/content/images/2026/10/bunny-React-Native-mobile-SDK.png" alt="Introducing the Bunny Stream SDK for React Native and Expo"><p>Building a mobile app with React Native helps you move quickly. Adding video should feel just as straightforward. But between playback, uploads, device cameras, even a simple idea can turn into a long list of integrations.</p><p>When we introduced the <a href="https://bunny.net/blog/introducing-bunny-stream-mobile-sdk-bringing-bunny-stream-to-your-native-ios-and-android-applications/" rel="noreferrer">Bunny Stream Mobile iOS and Android SDKs</a>, we made it easier to bring video to native iOS and Android applications. Today, we&apos;re bringing that experience to more developers with the official <a href="https://github.com/BunnyWay/bunny-stream-react-native"><strong>Bunny Stream React Native SDK</strong></a>!</p><p>Built on our iOS and Android SDKs, it brings native playback, resumable uploads, camera capture, and live streaming together behind one TypeScript API. You build your app with familiar React components. Bunny Stream handles video encoding, storage, and delivery through our global CDN. We&#x2019;ve also bundled in an <a href="https://github.com/BunnyWay/bunny-stream-react-native/blob/main/docs/guides/expo.md">Expo</a> config plugin for faster integration with Expo React Native apps.</p><h2 id="one-integration-for-ios-and-android">One integration for iOS and Android</h2><p>The Bunny Stream React Native SDK connects your shared application code to the native Bunny players and media tools on each device, so you don&#x2019;t need to build separate integrations for every platform.</p><h3 id="resumable-uploads-for-unreliable-mobile-connections">Resumable uploads for unreliable mobile connections</h3><p>Mobile connections don&apos;t always cooperate. With TUS resumable uploads, an interrupted transfer can pick up where it left off instead of sending the whole video again. Progress events and pause, resume, and cancel controls make it easy to build an upload experience that keeps people informed.</p><p>Select TUS when starting an upload to enable resumable transfers. See the upload notes for recovery and platform-specific background behavior.</p><h3 id="familiar-components-native-playback">Familiar components, native playback</h3>
<!--kg-card-begin: html-->
<p>
  Add on-demand or live video with <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">BunnyStreamPlayer</code>. Playback runs through the native media engines on iOS and Android, while typed props, events, and React hooks help you connect the player to the rest of your app.
</p>
<!--kg-card-end: html-->
<p>Player settings from your Bunny Stream dashboard carry through to the native experience, helping you keep your videos consistent with your brand. Captions, audio track selection, and adaptive playback give your audience the controls they expect.</p><h3 id="from-camera-to-content">From camera to content</h3>
<!--kg-card-begin: html-->
<p>
  Let your users create as well as watch. Capture video directly from the device camera for your video library, or use <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">BunnyStreamBroadcaster</code> to go live from your app. Camera switching and microphone controls put the essentials within reach.
</p>
<!--kg-card-end: html-->
<h3 id="more-ways-to-watch">More ways to watch</h3><p>Give viewers more room to enjoy their content with fullscreen playback, picture-in-picture, AirPlay on iOS, and Chromecast on Android through the native player controls. For protected content, the SDK brings support for Apple FairPlay and Google Widevine. VAST/IMA ad support also connects your app to Bunny Stream&apos;s video monetization features.</p><p>For Android TV, an optional native TV player integration brings playback to the big screen with an additional SDK dependency.</p><h2 id="broadcast-live-replay-later">Broadcast live, replay later</h2>
<!--kg-card-begin: html-->
<p>
  Live streaming support is built right into the SDK. Use <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">BunnyStreamBroadcaster</code> to broadcast straight from your users&#x2019; phones via RTMP, with camera switching and microphone controls, then show the stream to viewers with the same <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">BunnyStreamPlayer</code> component you use for on-demand video.
</p>
<!--kg-card-end: html-->
<p>Features like DVR let viewers rewind a live event, while recordings keep it available for replay afterward.</p><p>Our Live streaming feature is currently in <strong>closed preview</strong> and being tested by fellow bunny.net users. If you&#x2019;d like to try it, you can request access through the banner in the dashboard (in any of your video libraries).</p><h2 id="using-expo-youre-invited-too">Using Expo? You&apos;re invited too.</h2><p>Using <a href="https://docs.expo.dev/develop/development-builds/introduction/" rel="noreferrer">Expo</a> requires <strong>Expo SDK 52+</strong>. The included Expo config plugin applies the native project configuration needed by the library during pre-build. Add it to your app configuration and create a development build locally or with EAS Build. Expo Go is not supported because the SDK uses native modules.</p><p>The SDK targets iOS and Android and uses React Native&apos;s New Architecture. Android requires <strong>API 26+</strong>; the setup guide covers the remaining native build requirements.</p><p>Install the <a href="https://www.npmjs.com/package/@bunny.net/stream-react-native" rel="noreferrer">package</a>:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#ECEC93;">npm</span> <span style="color:#ECEC93;">install</span> @bunny.net/stream-react-native
</pre>
<br>
<!--kg-card-end: html-->
<p>For Expo projects, add the plugin to your existing app configuration:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
{
  <span style="color:#D1949E;">&quot;expo&quot;</span>: {
    <span style="color:#D1949E;">&quot;plugins&quot;</span>: [<span style="color:#BDE052;">&quot;@bunny.net/stream-react-native&quot;</span>]
  }
}
</pre>
<br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  The repository&apos;s setup guide covers native setup, initialization, and camera and microphone permissions. The plugin supplies default iOS permission messages; customize them through the <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">cameraPermission</code> and <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">microphonePermission</code> plugin options. Example apps for React Native and Expo give you a starting point to explore.
</p>
<!--kg-card-end: html-->
<h2 id="let%E2%80%99s-get-to-the-code">Let&#x2019;s get to the code</h2><p>Once you&apos;ve completed setup and initialized the SDK with your library configuration, adding a video looks like this:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#569CD6;">import</span> { BunnyStreamPlayer } <span style="color:#569CD6;">from</span> <span style="color:#BDE052;">&apos;@bunny.net/stream-react-native&apos;</span>;

<span style="color:#569CD6;">export</span> <span style="color:#569CD6;">function</span> VideoScreen() {
  <span style="color:#569CD6;">return</span> (
    &lt;BunnyStreamPlayer source={{ type: <span style="color:#BDE052;">&apos;vod&apos;</span>, videoId: <span style="color:#BDE052;">&apos;YOUR_VIDEO_ID&apos;</span> }} style={{ flex: <span style="color:#D1949E;">1</span> }} /&gt;
  );
}
</pre>
<br>
<!--kg-card-end: html-->
<p>Showing a live stream uses the same component. Change the source:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
source={{
  type: <span style="color:#BDE052;">&apos;live&apos;</span>,
  streamId: <span style="color:#BDE052;">&apos;YOUR_STREAM_ID&apos;</span>,
}}
</pre>
<br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">libraryId</code> falls back to the library passed to <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">initialize()</code> for both VOD and live sources. Provide it explicitly on a source to override the initialized library.
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  The SDK also gives you typed APIs for managing videos, collections, live streams, captions, reencoding, and AI transcription, so your integration can grow beyond the player as your app grows. Alongside these APIs, <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">BunnyImage</code> helps display thumbnails and other Bunny-hosted images with support for CDN hotlink protection. See the platform notes for links to operation-specific limitations.
</p>
<!--kg-card-end: html-->
<h2 id="what-will-you-build">What will you build?</h2><ul><li><strong>Streaming and subscription apps:</strong> Bring your video catalog into one mobile experience, with protected playback and big-screen viewing options. Great for microdramas.</li><li><strong>Social and creator platforms:</strong> Let your community capture, upload, and broadcast from their phones, then keep recorded events available for anyone who missed them.</li><li><strong>Education and e-learning:</strong> Combine on-demand lessons with scheduled live classes. DVR helps learners revisit a tricky explanation, while recordings let them catch up later.</li></ul><h2 id="ready-to-hop-in">Ready to hop in?</h2><p>Adding video shouldn&#x2019;t take away from building the experience your audience comes for. With Bunny Stream for React Native and Expo, native video tools are now part of your React workflow.</p><p>Explore the <a href="https://github.com/BunnyWay/bunny-stream-react-native">official Bunny Stream React Native mobile SDK on GitHub</a>, try the example apps, and bring your next video idea to life.</p><h2 id="built-with-expertise-from-lighthouse-growth">Built with expertise from Lighthouse Growth</h2><p>The Bunny Stream React Native SDK was developed with support from <a href="https://www.lighthouse-growth.com/"><strong>Lighthouse Growth</strong></a>, a software development company with deep expertise in OTT, video streaming, and media technologies.</p><p>Their team brings hands-on experience building video platforms, mobile applications, and SDKs across iOS, Android, and React Native, and helped us make native-grade video capabilities more accessible to the React Native and Expo community. If you&apos;re building in this space, feel free to get in touch with them on <a href="https://www.linkedin.com/in/michal-bulakowski-63b468136/" rel="noreferrer">LinkedIn</a>.</p>]]></content:encoded></item><item><title><![CDATA[Deploy static sites anywhere with the bunny.net CLI]]></title><description><![CDATA[A new set of commands wires up Storage, CDN, and DNS, letting you deploy the folder your framework builds to our global network with a single command.]]></description><link>https://bunny.net/blog/deploy-static-sites-anywhere-with-the-bunny-net-cli/</link><guid isPermaLink="false">6ac7666b8b5fed851dd54109</guid><category><![CDATA[News]]></category><dc:creator><![CDATA[Jamie Barton]]></dc:creator><pubDate>Thu, 08 Oct 2026 10:11:35 GMT</pubDate><media:content url="https://bunny.net/blog/content/images/2026/10/image--41-.png" medium="image"/><content:encoded><![CDATA[<img src="https://bunny.net/blog/content/images/2026/10/image--41-.png" alt="Deploy static sites anywhere with the bunny.net CLI"><p>If you&apos;ve hosted a static site on bunny.net, you&apos;ve probably built your own deploy pipeline to do it. That means creating a storage zone, putting a pull zone in front of it, pointing your domain at it, and writing something to upload each build and purge the cache. There&apos;s even a community GitHub Action that handles the upload part.</p>
<!--kg-card-begin: html-->
<p>
  Today we&apos;re introducing <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny sites</code>, a set of commands in the bunny.net CLI that takes care of all of that for you. Run <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny sites deploy</code> in your project, and it puts your build live, creating the storage zone and pull zone on the first deploy. It also detects common frameworks and offers to run the build for you. <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny sites domains add</code> connects your own domain, setting up the DNS record for you if it&apos;s on Bunny DNS. And if a release goes wrong, rolling back is one command.
</p>
<!--kg-card-end: html-->
<h2 id="deploy-with-one-command-and-connect-to-github">Deploy with one command and connect to GitHub</h2>
<!--kg-card-begin: html-->
<p>
  <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny sites deploy</code> takes the folder your framework builds and ships it on the bunny.net global network:
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny sites <span style="color:#ECEC93;">deploy</span>
</pre>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  That one command uploads your build and puts it live. There is no separate publish step and no flag to remember: if the deploy succeeded, your site is serving it. A brand-new site gets a working HTTPS address on <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">b-cdn.net</code> immediately, so there are no DNS records or certificates to sort out before you can look at it.
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  If you&#x2019;d prefer not to run the command manually for each deploy, you can connect GitHub with a new GitHub Action. <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny sites ci init</code> scaffolds the workflow using the detected framework conventions, including the output folder and build command. Review the generated workflow and adjust it to fit your setup.
</p>
<!--kg-card-end: html-->
<p>With this workflow, pushes to the main branch go live, and you can trigger a redeploy manually whenever you need one.</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#ECEC93;">name:</span> Deploy site
<span style="color:#ECEC93;">on:</span>
  <span style="color:#ECEC93;">push:</span>
    <span style="color:#ECEC93;">branches:</span> [main]
  <span style="color:#ECEC93;">workflow_dispatch:</span>

<span style="color:#ECEC93;">concurrency:</span>
  <span style="color:#ECEC93;">group:</span> bunny-sites
  <span style="color:#ECEC93;">cancel-in-progress:</span> <span style="color:#D1949E;">false</span>

<span style="color:#ECEC93;">jobs:</span>
  <span style="color:#ECEC93;">deploy:</span>
    <span style="color:#ECEC93;">runs-on:</span> ubuntu-latest
    <span style="color:#ECEC93;">permissions:</span>
      <span style="color:#ECEC93;">contents:</span> <span style="color:#D1949E;">read</span>
      <span style="color:#ECEC93;">deployments:</span> <span style="color:#D1949E;">write</span> <span style="color:#8A8A8A;"># records the deploy in the repo&apos;s Environments</span>
    <span style="color:#ECEC93;">steps:</span>
      - <span style="color:#ECEC93;">uses:</span> actions/checkout@v4
      - <span style="color:#ECEC93;">uses:</span> oven-sh/setup-bun@v2
      - <span style="color:#ECEC93;">run:</span> bun install --frozen-lockfile
      - <span style="color:#ECEC93;">run:</span> bun run build
      - <span style="color:#ECEC93;">uses:</span> BunnyWay/actions/deploy-site@deploy-site_0.1.1
        <span style="color:#ECEC93;">with:</span>
          <span style="color:#ECEC93;">site:</span> my-site
          <span style="color:#ECEC93;">directory:</span> dist
          <span style="color:#ECEC93;">api_key:</span> ${{ secrets.BUNNYNET_API_KEY }}
</pre>
<!--kg-card-end: html-->
<h2 id="atomic-deploys-and-instant-rollback">Atomic deploys and instant rollback</h2><p>Deploys are immutable. Each one is identified by your Git commit or a hash of its contents, so deploying the same output twice does nothing, and publishing never overwrites files in place. Publishing is a pointer flip and a cache purge, which means the site switches over atomically, with no window where visitors see half of one deploy and half of another.</p><p>It also means rolling back is instant. Every deploy you&apos;ve shipped is still there, so undoing a bad release is one command:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny sites deployments <span style="color:#ECEC93;">list</span>
bunny sites deployments <span style="color:#ECEC93;">publish</span> <span style="color:#ECEC93;">--previous</span>
</pre>
<!--kg-card-end: html-->
<p>No rebuild, no re-upload. The previous deploy is live again in seconds.</p><h2 id="bring-your-own-framework">Bring your own framework</h2><p>Developer tooling changes. Frameworks come and go, and we don&apos;t believe you should have to rebuild your project around a platform to deploy it.</p>
<!--kg-card-begin: html-->
<p>
  If your framework generates static files, it works with <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny sites</code>. Astro, Hugo, Jekyll, Gatsby, Eleventy, Docusaurus, Gridsome, or any other static site generator: we deploy the output. The CLI also detects common frameworks and offers to run the build for you.
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  Single-page apps work too. When the CLI recognizes a client-routed framework such as Vite, Angular, or React Router, or spots a build that looks like one, it configures the site to serve <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">index.html</code> for unknown paths, so a deep link survives a refresh. A <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">404.html</code> in your build output becomes the not-found page for everything else.
</p>
<!--kg-card-end: html-->
<h2 id="how-it-works">How it works</h2>
<!--kg-card-begin: html-->
<p>
  <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny sites</code> is glue around Storage, CDN, and DNS. There&apos;s no new hosting platform underneath, just the primitives developers have been stitching together themselves, wired up so you don&apos;t have to.
</p>
<!--kg-card-end: html-->
<p>Creating a site provisions two things:</p><ul><li>a storage zone to hold your deploys,</li><li>and a pull zone in front of it, with a handful of edge rules that decide which deploy answers each request.</li></ul><figure class="kg-card kg-image-card"><img src="https://bunny.net/blog/content/images/2026/10/bunny-sites---how-it-works.png" class="kg-image" alt="Deploy static sites anywhere with the bunny.net CLI" loading="lazy" width="2000" height="1908" srcset="https://bunny.net/blog/content/images/size/w600/2026/10/bunny-sites---how-it-works.png 600w, https://bunny.net/blog/content/images/size/w1000/2026/10/bunny-sites---how-it-works.png 1000w, https://bunny.net/blog/content/images/size/w1600/2026/10/bunny-sites---how-it-works.png 1600w, https://bunny.net/blog/content/images/size/w2400/2026/10/bunny-sites---how-it-works.png 2400w" sizes="(min-width: 720px) 720px"></figure><p>Every deploy is uploaded to its own folder in the storage zone, named after your Git commit or a hash of its contents. Nothing in that folder is ever overwritten. One edge rule on the pull zone rewrites the incoming path into the live deploy&apos;s folder, so your site is always served from the root exactly as your framework built it. The CDN caches and serves the files directly from there, and a second rule blocks anyone from reaching a deploy folder by its internal path.</p><p>Publishing retargets that one rule and purges the cache, which is why going live and rolling back both take effect everywhere at once without a file moving.</p>
<!--kg-card-begin: html-->
<p>
  Attach a custom domain with <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny sites domains add yourdomain.com</code> to give production a proper address. If the domain is on Bunny DNS, the CLI offers to point the record at your site and issues the free SSL certificate; otherwise it prints the exact CNAME to set. Your site&apos;s <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">b-cdn.net</code> address keeps working either way.
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  Because deploys are never deleted automatically, deploy history accumulates over time. When you want to tidy up, <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny sites deployments prune</code> clears out old deploys, always keeps your most recent ones, and never touches the deploy that&apos;s live or the one before it, so rollback always works.
</p>
<!--kg-card-end: html-->
<p>Framework detection works much the same way you would do it by hand. When you run a deploy in a project the CLI hasn&apos;t seen before, it inspects the project to work out which framework you&apos;re using and where its build output lands, then offers to run the build and deploy the result.</p><h2 id="we-dont-build-your-site-we-deliver-it">We don&apos;t build your site, we deliver it</h2><p>We stick to our platform primitives and give developers the resources to build and deploy in whatever way suits them.</p>
<!--kg-card-begin: html-->
<p>
  You control the build in whatever CI provider and toolchain you already use, whether that&apos;s npm, pnpm, Bun, Cargo, or a Makefile, and <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny sites deploy</code> puts the files you give it in the right place across our global edge network.
</p>
<!--kg-card-end: html-->
<p>Because it&apos;s built on the same infrastructure developers already trust, nothing is hidden behind an abstraction. You can enable and tune any feature of Bunny CDN, DNS, and Storage, and add products like Stream and Edge Scripting alongside your site as it grows.</p><h2 id="pay-as-you-serve">Pay as you serve</h2>
<!--kg-card-begin: html-->
<p>
  Deploying with<code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny sites</code> doesn&apos;t introduce a new pricing model. You pay for the underlying bunny.net primitives:
</p>
<!--kg-card-end: html-->
<ul><li><strong>Storage:</strong> $0.01 per GB per month (standard tier, single region). Deploys are static files, so most sites cost fractions of a cent to store, even with previous deploys kept around for instant rollback.</li><li><strong>CDN:</strong> from $0.01 per GB of bandwidth (Europe &amp; North America; other regions range up to $0.06/GB), with no per-request fees. Traffic between Storage and the CDN is free.</li><li><strong>DNS:</strong> <a href="https://bunny.net/blog/were-making-bunny-dns-free/">free</a>. Custom domains run on Bunny DNS at no cost, with unlimited queries.</li></ul><p>Because everything is pay-as-you-go, your costs track actual traffic. A small site serving a few GB each month typically costs only a few cents. There&apos;s no idle charge for a site nobody visits beyond the $1 monthly account minimum. Delete a site and its zones, and it stops costing you anything.</p><h2 id="get-started">Get started</h2><p>Install the bunny.net CLI and log in:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#ECEC93;">npm install</span> <span style="color:#ECEC93;">-g</span> @bunny.net/cli
bunny <span style="color:#ECEC93;">login</span>
</pre>
<!--kg-card-end: html-->
<p>Here&apos;s a real blog going onto the edge, using Astro&apos;s blog template. If Jekyll, Hugo, or Eleventy is more your thing, the steps are identical: build, deploy, done.</p><p>Scaffold the blog:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#ECEC93;">npm create</span> astro@latest my-blog <span style="color:#ECEC93;">--</span> <span style="color:#ECEC93;">--template</span> blog
<span style="color:#ECEC93;">cd</span> my-blog
</pre>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  That gives you a complete blog with a few example posts, RSS, and a sitemap. Run <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">npm run dev</code> to have a look around, write a post if you like, then deploy it:
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny sites <span style="color:#ECEC93;">deploy</span> <span style="color:#ECEC93;">--build</span>
</pre>
<!--kg-card-end: html-->
<p>Your first deploy creates the site and provisions the storage zone and pull zone. The CLI detects Astro, runs the build, uploads the output, and puts the site live. With no custom domain at this point, it offers to attach one before it finishes.</p><p>Every later deploy is the same command. Deploy the same output again and nothing is re-uploaded at all, because the deploy is identified by its contents.</p><p>When you want production on your own domain:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny sites domains <span style="color:#ECEC93;">add</span> yourdomain.com
</pre>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  You can then run <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny sites ci init</code> to wire up GitHub. Every merge to <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">main</code> ships to production, and you can trigger a redeploy by hand whenever you need one.
</p>
<!--kg-card-end: html-->
<h2 id="faster-images-with-bunny-optimizer">Faster images with Bunny Optimizer</h2>
<!--kg-card-begin: html-->
<p>
  Static builds are usually stuck with whatever image sizes the build produced. Because every site sits on a regular pull zone, you can turn on Bunny Optimizer and resize images at the edge instead. Point your framework&apos;s image loader at it, and each <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">srcset</code> width becomes a URL like <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">/images/hero.png?width=640</code>.
</p>
<!--kg-card-end: html-->
<p>Next.js, Astro, Angular, and Nuxt all have a loader hook for this, and our <a href="https://github.com/BunnyWay/examples/tree/main/sites" rel="noopener noreferrer">static site examples</a> include a small helper for 21 frameworks, off by default:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny sites <span style="color:#ECEC93;">deploy</span> <span style="color:#ECEC93;">--build</span> <span style="color:#ECEC93;">--env</span> VITE_BUNNY_OPTIMIZER=<span style="color:#D1949E;">true</span>
</pre>
<!--kg-card-end: html-->
<p>Optimizer is an optional add-on at $9.50 a month per site.</p>
<!--kg-card-begin: html-->
<h2>What&apos;s next for <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny sites</code></h2>
<!--kg-card-end: html-->
<p>For now, deploying static sites is CLI only, but we&apos;re experimenting with bringing it to the dashboard.</p>
<!--kg-card-begin: html-->
<p>
  We&apos;re also actively working on deploying Edge Scripts automatically when you run <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny sites deploy</code>, so when your site needs a form handler or other server-side code, it&apos;s right there alongside it. And for full server-side rendering, we&apos;re experimenting with support for SSR frameworks on Edge Scripting.
</p>
<!--kg-card-end: html-->
<p>If any of this sounds interesting, let us know on <a href="https://discord.com/invite/bunnynet" rel="noopener noreferrer">Discord</a>.</p><h2 id="putting-the-bunnynet-stack-in-your-terminal">Putting the bunny.net stack in your terminal</h2>
<!--kg-card-begin: html-->
<p>
  <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny sites</code> joins a growing set of CLI capabilities that let you and your agent use our platform from the terminal:
</p>
<!--kg-card-end: html-->
<ul><li><a href="https://bunny.net/blog/introducing-the-bunny-net-cli/" rel="noreferrer">Manage</a> and <a href="https://bunny.net/blog/query-bunny-database-with-fetch-and-nothing-else/" rel="noreferrer">query</a> Bunny Database, plus <a href="https://bunny.net/blog/bunny-database-migrations-from-the-terminal-with-the-bunny-net-cli/" rel="noreferrer">handle database migrations</a></li><li><a href="https://bunny.net/blog/build-and-deploy-scripts-at-the-edge-with-the-bunny-net-cli/" rel="noreferrer">Build and deploy edge functions</a></li><li><a href="https://bunny.net/blog/manage-dns-from-the-terminal-with-bunny-net-cli/" rel="noreferrer">Manage Bunny DNS</a></li><li><a href="https://bunny.net/blog/you-can-now-manage-bunny-storage-with-the-bunny-net-cli/" rel="noreferrer">Manage Bunny Storage</a></li><li><a href="https://bunny.net/blog/introducing-bunny-sandbox-a-disposable-room-where-ai-can-build/" rel="noreferrer">Run code and agents in Bunny Sandbox</a></li></ul><p>Support for Bunny Stream and Magic Containers Apps is coming next.</p>
<!--kg-card-begin: html-->
<p>
  The CLI is <a href="https://github.com/BunnyWay/cli">open source</a> and built in public. If you&apos;ve deployed something with <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny sites</code>, come and tell us about it in <a href="https://discord.com/invite/bunnynet">Discord</a>.
</p>
<!--kg-card-end: html-->
]]></content:encoded></item><item><title><![CDATA[Introducing Bunny CDN Load Balancer: make your origins work together]]></title><description><![CDATA[Available now in Public Preview, the Load Balancer distributes HTTP(S) requests from your Pull Zones across the origins you configure, using your routing preferences alongside origin health and performance.]]></description><link>https://bunny.net/blog/introducing-bunny-cdn-load-balancer-make-your-origins-work-together/</link><guid isPermaLink="false">6abce3608b5fed851dd540e9</guid><category><![CDATA[News]]></category><dc:creator><![CDATA[Joe Connolly]]></dc:creator><pubDate>Wed, 30 Sep 2026 10:31:00 GMT</pubDate><media:content url="https://bunny.net/blog/content/images/2026/09/Load-balance.png" medium="image"/><content:encoded><![CDATA[<img src="https://bunny.net/blog/content/images/2026/09/Load-balance.png" alt="Introducing Bunny CDN Load Balancer: make your origins work together"><p>Say your application runs in two regions, with a third held in reserve. You want the first two to share everyday traffic, and the standby region to step in when they cannot serve it. When an origin fails, those priorities should still determine where the request goes next. </p><p>At bunny.net, we want you to be able to express that plan once and have the CDN carry it through from normal traffic distribution to failover.</p><p>Today, we&#x2019;re introducing <strong>Bunny</strong> <strong>CDN Load Balancer</strong>, available now in <strong>Public Preview</strong>. It distributes HTTP(S) requests from your Pull Zones across the origins you configure, using your routing preferences alongside origin health and performance.</p><h2 id="set-priorities-across-your-origin-pool">Set priorities across your origin pool</h2><p>A Load Balancer is a routing policy in your bunny.net account. Attach it to one Pull Zone or reuse it across several. Requests that need an origin follow that policy; cached responses continue to be served directly from the CDN edge.</p><p>Origin groups let you organize backends by region, provider, capacity, or purpose. In our example, the two primary regions would sit in the first priority tier, with the standby region in a later tier. Traffic can be shared across primary groups while backup capacity waits behind them.</p><p>You can spread traffic evenly, assign weights, or favor nearby or lower-latency groups. The pool can combine standard HTTP(S) servers, Bunny Storage, Edge Scripting, and Magic Containers, so your primary and standby capacity can use different types of backend.</p><figure class="kg-card kg-image-card"><img src="https://bunny.net/blog/content/images/2026/09/1-2.png" class="kg-image" alt="Introducing Bunny CDN Load Balancer: make your origins work together" loading="lazy" width="1934" height="1216" srcset="https://bunny.net/blog/content/images/size/w600/2026/09/1-2.png 600w, https://bunny.net/blog/content/images/size/w1000/2026/09/1-2.png 1000w, https://bunny.net/blog/content/images/size/w1600/2026/09/1-2.png 1600w, https://bunny.net/blog/content/images/2026/09/1-2.png 1934w" sizes="(min-width: 720px) 720px"></figure><h2 id="keep-your-priorities-intact-during-failover">Keep your priorities intact during failover</h2><p>For each request that needs an origin, the edge builds an ordered list of eligible destinations before contacting the first one. It orders the origin groups, then the origins within them, according to your priority tiers and balancing settings.</p><p>When another attempt is safe, the next candidate is already known. In our example, eligible primary capacity stays ahead of the standby region throughout failover.</p><figure class="kg-card kg-image-card"><img src="https://bunny.net/blog/content/images/2026/09/1-2--1-.png" class="kg-image" alt="Introducing Bunny CDN Load Balancer: make your origins work together" loading="lazy" width="1846" height="1192" srcset="https://bunny.net/blog/content/images/size/w600/2026/09/1-2--1-.png 600w, https://bunny.net/blog/content/images/size/w1000/2026/09/1-2--1-.png 1000w, https://bunny.net/blog/content/images/size/w1600/2026/09/1-2--1-.png 1600w, https://bunny.net/blog/content/images/2026/09/1-2--1-.png 1846w" sizes="(min-width: 720px) 720px"></figure><p>Retries also take account of what a request can do. After an origin returns a 5xx response, only requests that are safe to replay are tried again. Requests with a body, or a method that may create or change data, are not blindly repeated after that response. This reduces the risk of turning a failed request into a duplicate order, upload, or payment.</p><p>The failover chain has a time limit, so one request cannot keep working through slow origins indefinitely.</p><h2 id="respond-to-health-at-each-edge">Respond to health at each edge</h2><p>An origin can be reachable from the Frankfurt edge while a network problem prevents the S&#xE3;o Paulo edge from reaching it. Load Balancer combines a network-wide health view with what each edge sees in live traffic.</p><p>Optional health checks monitor origins in a group from across the bunny.net network. A degraded origin is excluded from normal rotation globally until it passes a health check again. Each edge also reacts to failures it encounters locally, even when active checks are disabled, so it can respond to a problem affecting its own requests.</p><p>If no healthy origin remains, Load Balancer can attempt an unhealthy origin as a last resort. Origins and groups you explicitly disable stay excluded.</p><p>For applications that need repeat requests to reach the same origin, optional sticky sessions use an encrypted edge cookie to maintain that choice. Health takes precedence: if the chosen origin becomes unhealthy or unavailable, the visitor is assigned another origin.</p><h2 id="balance-speed-and-capacity">Balance speed and capacity</h2><p>Repeatedly sending traffic to the origin with the lowest measured latency can make it the busiest, eroding the speed advantage that put it first. That&#x2019;s why latency-based origin balancing also considers requests already in flight. As an origin becomes busier, it grows less attractive for new requests, helping the rest of the pool share the load.</p><p>Stale and newly added origins are re-tested automatically. Probing backs off when they remain slow or unavailable, while recovered origins can be considered for traffic again.</p><h2 id="start-balancing-your-origin-traffic">Start balancing your origin traffic</h2><p>Public Preview is available now for <strong>$9.50 per month per Load Balancer</strong>, including <strong>50 million origin-bound requests each month</strong>. Additional requests are billed at <strong>$0.65 per million</strong>.</p><p>Requests served from the CDN cache do not count. Sharing a Load Balancer across multiple Pull Zones uses one monthly fee and one shared request allowance.</p><p>To get started:</p><ul><li>Create a Load Balancer and choose how traffic should be distributed between origin groups.</li><li>Add your origin groups and the origins each should contain, setting priorities to reflect your primary and standby capacity.</li><li>Set your Pull Zone&#x2019;s origin type to <strong>Load Balancer</strong> and select the configuration you created.</li></ul><p>Once traffic is flowing, use the dashboard or API statistics to see how requests are distributed, how each origin is performing, and where failover is happening. The <a href="https://bunny.net/docs/cdn/load-balancer/">Load Balancer documentation</a> covers the routing, health-check, sticky-session, and API options in detail.</p><p><a href="https://dash.bunny.net/auth/login"><strong>Log in</strong></a> or <a href="https://dash.bunny.net/auth/register"><strong>sign up</strong></a> today to join the Public Preview and start balancing your origin traffic with automatic failover.</p>]]></content:encoded></item><item><title><![CDATA[You shouldn't have to rebuild your website: introducing Agent Readability in Bunny Optimizer]]></title><description><![CDATA[Agent Readability in Bunny Optimizer automatically converts eligible HTML pages into Markdown and generates an llms.txt guide to help agents find them.]]></description><link>https://bunny.net/blog/you-shouldnt-have-to-rebuild-your-website-introducing-agent-readability-in-bunny-optimizer/</link><guid isPermaLink="false">6ab5045d8b5fed851dd540c2</guid><category><![CDATA[News]]></category><dc:creator><![CDATA[Joe Connolly]]></dc:creator><pubDate>Thu, 24 Sep 2026 11:22:51 GMT</pubDate><media:content url="https://bunny.net/blog/content/images/2026/09/bunny.net-AI-optimizer.png" medium="image"/><content:encoded><![CDATA[<img src="https://bunny.net/blog/content/images/2026/09/bunny.net-AI-optimizer.png" alt="You shouldn&apos;t have to rebuild your website: introducing Agent Readability in Bunny Optimizer"><p>In the traffic we analyzed for our <a href="https://bunny.net/edge-security-report-2026/">2026 Edge Security Report</a>, AI crawlers were already matching search-engine bots by request volume.</p><p>Alongside those crawlers are assistants visiting websites to answer people&#x2019;s questions. Sometimes, the agentic web is simply someone asking when their tea will ship. The shop has already published its shipping policy, but an assistant receives it wrapped in the HTML for navigation, layout, and everything else that makes the page work in a browser.</p><p>At bunny.net, we think making the internet hop faster includes helping these requests do less unnecessary work. An assistant can read HTML; a text-first version can give it the information with less presentation markup to process. We want that improvement to be something you can turn on for the website you already run, without a second publishing project to maintain.</p>
<!--kg-card-begin: html-->
<p>
  Today, we&#x2019;re launching <strong>Agent Readability in Bunny Optimizer</strong> in <strong>Public Preview</strong>. It automatically converts eligible HTML pages into Markdown and generates an <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">llms.txt</code> guide to help agents find them.
</p>

<!--kg-card-end: html-->
<h2 id="one-page-optimized-for-different-audiences">One page, optimized for different audiences</h2><p>Take the sample About page below, for a tea company called Alder &amp; Vine. Its shipping note survives the conversion intact: orders placed after noon Pacific ship the next business day. So do the headings, numbered list, and staff table. The site navigation and presentation markup are gone.</p><figure class="kg-card kg-image-card"><img src="https://bunny.net/blog/content/images/2026/09/33.png" class="kg-image" alt="You shouldn&apos;t have to rebuild your website: introducing Agent Readability in Bunny Optimizer" loading="lazy" width="2000" height="1200" srcset="https://bunny.net/blog/content/images/size/w600/2026/09/33.png 600w, https://bunny.net/blog/content/images/size/w1000/2026/09/33.png 1000w, https://bunny.net/blog/content/images/size/w1600/2026/09/33.png 1600w, https://bunny.net/blog/content/images/size/w2400/2026/09/33.png 2400w" sizes="(min-width: 720px) 720px"></figure><p>The company&#x2019;s story comes through, too. You can still read about the trip that inspired it and the farms it works with today. Markdown keeps that context alongside the practical details an assistant might need. A person browsing the site continues to see the original design, while Optimizer creates the text version from the published HTML.</p>
<!--kg-card-begin: html-->
<p>
  An agent can request the page with <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">Accept: text/markdown</code> or use its Markdown URL directly, for example <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">https://bunny.net/docs/index.md</code> on our documentation site.
</p>

<!--kg-card-end: html-->
<h2 id="help-agents-find-the-right-page">Help agents find the right page</h2><p>An assistant arriving at a documentation homepage still needs to work out where to go next. A setup guide, an account FAQ, and a changelog answer very different questions.</p>
<!--kg-card-begin: html-->
<p>
  Optimizer also builds <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">llms.txt</code> automatically from the converted pages. Compatible agents can use the descriptions in this file to find relevant pages, then follow the links to retrieve those pages as Markdown.
</p>

<!--kg-card-end: html-->
<p>The bunny.net documentation excerpt below shows how this helps with a specific task. An assistant helping someone get started can follow the quickstart entry, while one looking for recent changes can head to the changelog.</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
https://bunny.net/docs/llms.txt
</pre>
<br>
<!--kg-card-end: html-->
<figure class="kg-card kg-image-card"><img src="https://bunny.net/blog/content/images/2026/09/1--1-.png" class="kg-image" alt="You shouldn&apos;t have to rebuild your website: introducing Agent Readability in Bunny Optimizer" loading="lazy" width="2000" height="1310" srcset="https://bunny.net/blog/content/images/size/w600/2026/09/1--1-.png 600w, https://bunny.net/blog/content/images/size/w1000/2026/09/1--1-.png 1000w, https://bunny.net/blog/content/images/size/w1600/2026/09/1--1-.png 1600w, https://bunny.net/blog/content/images/size/w2400/2026/09/1--1-.png 2400w" sizes="(min-width: 720px) 720px"></figure><h2 id="give-it-a-try-on-your-own-site">Give it a try on your own site</h2><p>Agent Readability is included with Optimizer at no additional cost. Optimizer is <strong>$9.50/month</strong>, and <strong>Public Preview for Agent Readability is available now</strong>.</p><p>Open your Pull Zone, go to <strong>Optimizer</strong>, and enable <strong>Agent Readability</strong>. Then turn on <strong>Markdown</strong> and <strong>llms.txt</strong> for your site.</p><p>If you haven&#x2019;t enabled Optimizer yet, the <a href="https://bunny.net/docs/optimizer/quickstart">Optimizer quickstart</a> walks you through initial setup.</p>
<!--kg-card-begin: html-->
<p>
  Start with a page customers regularly ask you about. Open its Markdown version and compare it with the original, just as we&#x2019;ve done above. You can see the text an agent can retrieve, then use <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">llms.txt</code> to explore the other converted pages. Whether the question is about setting up a service or shipping a tin of tea, the answer can start with the content you already maintain.
</p>

<!--kg-card-end: html-->
<p><a href="https://dash.bunny.net/auth/login" rel="noopener noreferrer">Log in</a> or <a href="https://dash.bunny.net/auth/register" rel="noopener noreferrer">sign up</a> today and make the content you&#x2019;ve already published easier for AI agents to find and use.</p>]]></content:encoded></item><item><title><![CDATA[Burrow Smart Routing is Generally Available: a faster path for dynamic traffic]]></title><description><![CDATA[Burrow Smart Routing now finds faster routes to your origin sooner, adapts more quickly to changing conditions, and is more selective about rerouting.]]></description><link>https://bunny.net/blog/burrow-smart-routing-is-generally-available-a-faster-path-for-dynamic-traffic/</link><guid isPermaLink="false">6aaac0418b5fed851dd5408c</guid><category><![CDATA[News]]></category><dc:creator><![CDATA[Joe Connolly]]></dc:creator><pubDate>Thu, 17 Sep 2026 08:32:00 GMT</pubDate><media:content url="https://bunny.net/blog/content/images/2026/09/smart-routing-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://bunny.net/blog/content/images/2026/09/smart-routing-1.png" alt="Burrow Smart Routing is Generally Available: a faster path for dynamic traffic"><p>A shopper has loaded your site, filled their basket, and clicked checkout. The images are already on screen. Now they&#x2019;re waiting for a request that has to reach your origin.</p><p>The route to your origin can be the difference between a smooth checkout and a customer left staring at a loading spinner. Uncached API calls and personalized responses depend on it too. At bunny.net, we want our network to help accelerate the parts of your application that need a fresh response from the origin.</p><p>That was the goal when we <a href="https://bunny.net/blog/introducing-burrow-smart-routing-to-make-the-internet-hop-even-faster/">introduced Burrow Smart Routing</a> last October. When the direct origin path is slow, Burrow can carry requests across our backbone and reach the origin through another bunny.net region with a faster route.</p><p>Today, <strong>Burrow Smart Routing is Generally Available</strong> within <a href="https://bunny.net/docs/optimizer" rel="noreferrer">Bunny Optimizer</a>. During Public Preview, we rebuilt the routing engine to discover useful paths sooner, respond faster to changing conditions, and be more selective about when rerouting is worthwhile.</p><figure class="kg-card kg-image-card"><img src="https://bunny.net/blog/content/images/2026/09/1.png" class="kg-image" alt="Burrow Smart Routing is Generally Available: a faster path for dynamic traffic" loading="lazy" width="2000" height="1272" srcset="https://bunny.net/blog/content/images/size/w600/2026/09/1.png 600w, https://bunny.net/blog/content/images/size/w1000/2026/09/1.png 1000w, https://bunny.net/blog/content/images/size/w1600/2026/09/1.png 1600w, https://bunny.net/blog/content/images/size/w2400/2026/09/1.png 2400w" sizes="(min-width: 720px) 720px"></figure><h2 id="bringing-a-customer-back-online-through-los-angeles">Bringing a customer back online through Los Angeles</h2><p>For one customer, congestion from a transatlantic fiber cut pushed latency between our Taipei region and their origin to around <strong>three seconds</strong>. The network incident was disrupting access to their site.</p><p>Burrow automatically rerouted that origin traffic through <strong>Los Angeles</strong>. Latency dropped to <strong>below one second</strong>, bringing the site back online for affected visitors while the underlying incident was still ongoing.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://bunny.net/blog/content/images/2026/10/1--2-.png" class="kg-image" alt="Burrow Smart Routing is Generally Available: a faster path for dynamic traffic" loading="lazy" width="2000" height="1477" srcset="https://bunny.net/blog/content/images/size/w600/2026/10/1--2-.png 600w, https://bunny.net/blog/content/images/size/w1000/2026/10/1--2-.png 1000w, https://bunny.net/blog/content/images/size/w1600/2026/10/1--2-.png 1600w, https://bunny.net/blog/content/images/2026/10/1--2-.png 2072w" sizes="(min-width: 720px) 720px"><figcaption><i><em class="italic" style="white-space: pre-wrap;">Burrow routed the customer&#x2019;s origin traffic from Taipei through Los Angeles during the incident, reducing observed latency from around three seconds to below one second.</em></i></figcaption></figure><h2 id="discover-faster-origin-routes-with-active-origin-probing">Discover faster origin routes with Active Origin Probing</h2><p>The original Burrow learned from requests already traveling through the bunny.net network. A region could have a useful path to an origin and still be absent from the routing decision because it had never handled traffic for that site. Smaller sites, or those with visitors concentrated in one region, could wait longer for Burrow to gather enough evidence.</p><p><strong>Active Origin Probing</strong> helps fill that gap. When enabled, it lets Burrow ask suitable regions near the origin to measure alternative paths when a direct connection is slow enough to benefit from a change.</p><figure class="kg-card kg-image-card"><img src="https://bunny.net/blog/content/images/2026/09/2--1-.png" class="kg-image" alt="Burrow Smart Routing is Generally Available: a faster path for dynamic traffic" loading="lazy" width="2000" height="1272" srcset="https://bunny.net/blog/content/images/size/w600/2026/09/2--1-.png 600w, https://bunny.net/blog/content/images/size/w1000/2026/09/2--1-.png 1000w, https://bunny.net/blog/content/images/size/w1600/2026/09/2--1-.png 1600w, https://bunny.net/blog/content/images/size/w2400/2026/09/2--1-.png 2400w" sizes="(min-width: 720px) 720px"></figure><p>The checks use the same request pipeline as real origin traffic, and their results are shared across the bunny.net network. A site can therefore discover useful routes within minutes, even without a large, globally distributed audience.</p><p>These measurements run in the background. Probing stops when Burrow has enough information and backs off when checks fail.</p><h2 id="reroute-when-the-improvement-is-worthwhile">Reroute when the improvement is worthwhile</h2><p>Sending a request through another region adds backbone traffic. A small fluctuation in latency should not be enough to justify that extra routing.</p><p>Burrow now leaves fast direct connections alone and requires a meaningful measured improvement before choosing an alternative. This helps keep your Burrow bandwidth allowance focused on requests that benefit from the extra route.</p><p>After switching, Burrow measures the full journey through the additional edge. The actual result replaces the earlier estimate, giving subsequent routing decisions evidence from the path your traffic is using.</p><h2 id="adapt-as-network-conditions-change">Adapt as network conditions change</h2><p>Burrow continues measuring the direct path after traffic moves to an alternative. When conditions recover, it can move requests back. Recent measurements now carry more weight, helping it respond sooner when a route improves or deteriorates while retaining useful context for quieter workloads.</p><p>Routing plans are refreshed in the background. If there is not enough evidence to choose an alternative, requests continue directly to the origin while Burrow gathers more information.</p><h2 id="give-your-dynamic-traffic-a-faster-path">Give your dynamic traffic a faster path</h2><p>Burrow Smart Routing is now Generally Available with Bunny Optimizer. Each Optimizer subscription includes <strong>50 GB of Burrow-routed uncached traffic per month</strong>. Additional Burrow-routed traffic is billed at <strong>$0.06/GB</strong>. Cached responses served from the edge do not use Burrow or count toward that allowance.</p><p>Billing for additional Burrow-routed traffic begins on October 17, 2026. Until then, it&apos;s free for everyone, so you can try Burrow without any surprise charges.</p><p>To get started:</p>
<!--kg-card-begin: html-->
<ul>
  <li>Open your Pull Zone, go to <strong>Optimizer</strong>, and enable Optimizer if it is not already active.</li>
  <li>Open the <strong>Burrow</strong> tab and turn on <strong>Burrow Smart Routing</strong>.</li>
  <li>To use <strong>Active Origin Probing</strong>, first configure your origin to return a successful response at <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">/.well-known/bunny-burrow-probe.txt</code>, then enable the <strong>Active Origin Probing</strong> toggle in the same Burrow area.</li>
</ul>
<!--kg-card-end: html-->
<p>Optimizer statistics show how many requests Burrow has optimized and an <strong>estimate of the time saved</strong> for your Pull Zone, so you can see what the routing is contributing to your application&#x2019;s delivery.</p><p>Thank you to everyone who ran Burrow during Public Preview and helped us improve it.</p><p><a href="https://dash.bunny.net/auth/login"><strong>Log in</strong></a> or <a href="https://dash.bunny.net/auth/register"><strong>sign up</strong></a> today and let Burrow find a faster route for your dynamic traffic.</p>]]></content:encoded></item><item><title><![CDATA[Bunny Database migrations from the terminal with the bunny.net CLI]]></title><description><![CDATA[Bunny Database now has migrations built into the CLI, so you can version your schema and ship it like code. Create migrations, preview them with a dry run before anything touches your database, and catch drift when a file changes after it's applied.]]></description><link>https://bunny.net/blog/bunny-database-migrations-from-the-terminal-with-the-bunny-net-cli/</link><guid isPermaLink="false">6a9ec6148b5fed851dd54032</guid><category><![CDATA[News]]></category><dc:creator><![CDATA[Jamie Barton]]></dc:creator><pubDate>Mon, 07 Sep 2026 14:42:28 GMT</pubDate><media:content url="https://bunny.net/blog/content/images/2026/09/Database-migrations-from-the-terminal-with-bunny.net-CLI-2.png" medium="image"/><content:encoded><![CDATA[<img src="https://bunny.net/blog/content/images/2026/09/Database-migrations-from-the-terminal-with-bunny.net-CLI-2.png" alt="Bunny Database migrations from the terminal with the bunny.net CLI"><p>Database migrations start simple. You need a users table, so you write some SQL. A week later you add an index, someone else adds a column, production needs another change, and staging is one migration ahead. Before long the SQL lives in a mix of folders, Slack messages, deployment scripts, and someone&apos;s Downloads directory.</p><p>Writing the SQL is the easy part. The part that gets messy is keeping track of what ran, where, and in what order.</p>
<!--kg-card-begin: html-->
<p>
  <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny db</code> now has a migration workflow built in. It&apos;s there for when you&apos;re using <a href="https://bunny.net/docs/database/">Bunny Database</a> for the first time, starting a new project, or  still haven&apos;tsettled on a way to manage schema changes. Keep the migration workflow you already have if you like it.
</p>
<!--kg-card-end: html-->
<h2 id="your-first-migration">Your first migration</h2><p>Say we&apos;re building a small application backed by Bunny Database. We need somewhere to store users, so we create the first migration:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny db migrations <span style="color:#ECEC93;">create</span> create_users
</pre>
<br>
<!--kg-card-end: html-->
<p>The CLI creates an empty, numbered SQL file:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
migrations/
&#x2514;&#x2500;&#x2500; 0001_create_users.sql
</pre>
<br>
<!--kg-card-end: html-->
<p>Leave the name out and the CLI will ask you for one:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny db migrations <span style="color:#ECEC93;">create</span>
</pre>
<br>
<!--kg-card-end: html-->
<p>Then open the file and write the schema change:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#569CD6;">CREATE</span> <span style="color:#569CD6;">TABLE</span> users (
  id <span style="color:#569CD6;">INTEGER</span> <span style="color:#569CD6;">PRIMARY</span> <span style="color:#569CD6;">KEY</span>,
  email <span style="color:#569CD6;">TEXT</span> <span style="color:#ECEC93;">NOT</span> <span style="color:#D1949E;">NULL</span> <span style="color:#569CD6;">UNIQUE</span>,
  created_at <span style="color:#569CD6;">TEXT</span> <span style="color:#ECEC93;">NOT</span> <span style="color:#D1949E;">NULL</span> <span style="color:#569CD6;">DEFAULT</span> <span style="color:#569CD6;">CURRENT_TIMESTAMP</span>
);
</pre>
<br>
<!--kg-card-end: html-->
<p>The filename matters as much as the SQL inside it. It identifies the migration, and the numeric prefix decides the order things run in. As the application grows, the directory becomes a record of how the schema got to where it is:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
migrations/
&#x251C;&#x2500;&#x2500; 0001_create_users.sql
&#x251C;&#x2500;&#x2500; 0002_add_display_name.sql
&#x2514;&#x2500;&#x2500; 0003_add_email_index.sql
</pre>
<br>
<!--kg-card-end: html-->
<p>Those files live alongside the code that depends on them, so they get committed to your repository and shared with everyone else on the team. The order is written into the filenames, and nothing important ends up sitting in a Downloads folder.</p><h2 id="see-whats-waiting">See what&apos;s waiting</h2><p>Before you change anything, you can check where things stand:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny db migrations <span style="color:#ECEC93;">list</span>
</pre>
<br>
<!--kg-card-end: html-->
<p>The CLI compares the migration files in your project against the migrations already applied to the database, then shows which are applied and which are still pending.</p><p>That&apos;s especially useful when others are involved. A teammate adds a migration for avatars, commits it with the application code that needs it, and pushes. You pull their branch and now have:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
migrations/
&#x251C;&#x2500;&#x2500; 0001_create_users.sql
&#x251C;&#x2500;&#x2500; 0002_add_display_name.sql
&#x251C;&#x2500;&#x2500; 0003_add_email_index.sql
&#x2514;&#x2500;&#x2500; 0004_add_avatar_url.sql
</pre>
<br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  Run <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">list</code> and you&apos;ll see that the first three applied and the fourth still waiting, so nobody has to ask whether they already ran this one.
</p>

<!--kg-card-end: html-->
<p>You can also preview the run without touching anything:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny db migrations <span style="color:#ECEC93;">apply</span> <span style="color:#ECEC93;">--dry-run</span>
</pre>
<br>
<!--kg-card-end: html-->
<p>A dry run really is dry: it won&apos;t create the migration tracking table or change your database.</p><p>If it all looks right, apply the pending migrations:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny db migrations <span style="color:#ECEC93;">apply</span>
</pre>
<br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  The CLI runs them in order and records each successful migration in the database, so running <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">apply</code> again won&apos;t replay anything that has already gone through.
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  Each migration is applied together with the record that tracks it, as a single atomic operation. Either the migration succeeds and gets recorded, or neither happens. If one fails, the CLI stops there and leaves the remaining files alone. Fix the problem and run <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">apply</code> again; the failed migration is still pending.
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  The upshot is that a migration travels with the code that needs it. Nobody has to maintain a shared document reminding the team to run <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">add-avatar-final-v2.sql</code>, and nobody has to guess whether the SQL file in that folder ever made it to production.
</p>
<!--kg-card-end: html-->
<h2 id="catching-migration-drift">Catching migration drift</h2>
<!--kg-card-begin: html-->
<p>
  There&apos;s a version of the scattered-migration problem that&apos;s harder to spot: someone changes an old migration after it has already run. Maybe <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">0002_add_display_name.sql</code> gets edited during a refactor, or an old migration disappears from the repository entirely.
</p>

<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  The CLI stores a checksum when it applies a migration, so <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny db migrations list</code> can tell when the files on disk no longer match what the database remembers. Along with applied and pending migrations, it flags any that have been modified or are missing.
</p>
<!--kg-card-end: html-->
<p>It won&apos;t guess what you meant, and it won&apos;t rewrite your database history to make the mismatch go away. The fix is usually the same one: leave the applied migration alone and write a new migration on top of it.</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny db migrations <span style="color:#ECEC93;">create</span> fix_display_name
</pre>
<!--kg-card-end: html-->
<h2 id="already-using-a-migration-tool">Already using a migration tool</h2><p>The best migration tool is often the one your project already has, and the built-in workflow isn&apos;t a requirement for using Bunny Database. If your application manages its schema with Drizzle, a Go migration tool, or your framework&apos;s own migration system, point it at your Database and carry on as usual. Your migrations can stay exactly where they are.</p>
<!--kg-card-begin: html-->
<p>
  <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny db migrations</code> is there for projects starting from an empty schema.
</p>
<!--kg-card-end: html-->
<h2 id="generate-with-drizzle-apply-with-the-cli">Generate with Drizzle, apply with the CLI</h2><p>There&apos;s a middle ground for ORMs that generate plain SQL. Drizzle owns the schema and writes each migration file, and the bunny.net CLI applies that SQL to your Database and records what has run.</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
drizzle-kit <span style="color:#ECEC93;">generate</span>
bunny db migrations <span style="color:#ECEC93;">apply</span>
</pre>
<br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  When there&apos;s no <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">migrations/</code> folder, the CLI picks up <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">drizzle/</code> on its own, splits each file at Drizzle&apos;s statement breakpoints, and leaves the <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">meta/</code> journal alone. If you keep both folders, name the one you mean:
</p>

<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny db migrations <span style="color:#ECEC93;">apply</span> <span style="color:#ECEC93;">--dir</span> drizzle
</pre>
<br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  The appeal is what you don&apos;t have to set up. The CLI finds the database from your <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">.env</code> and signs in with your bunny.net API key, so your app carries no database URL or long-lived token for migrations and you write no migrate script. You also get <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">--dry-run</code>, <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny db migrations list</code>, and drift detection over the generated files.
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  Two rules keep this tidy. Generate every migration with Drizzle so it lands in Drizzle&apos;s journal, and skip <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny db migrations create</code> in this setup. Pick one applier.
</p>

<!--kg-card-end: html-->
<h2 id="a-default-for-your-first-database">A default for your first database</h2><p>If this is your first Bunny Database, you probably don&apos;t want to pick a migration framework before you&apos;ve created your first table. The built-in workflow gives you something to use in the meantime, with no extra tool to install and no framework-specific schema format to learn. You write plain SQL, keep it with your application, and let the CLI handle ordering and tracking.</p><p>Four commands cover it:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#8A8A8A;"># Create a migration</span>
bunny db migrations <span style="color:#ECEC93;">create</span> add_users

<span style="color:#8A8A8A;"># Check migration state</span>
bunny db migrations <span style="color:#ECEC93;">list</span>

<span style="color:#8A8A8A;"># Preview what will run</span>
bunny db migrations <span style="color:#ECEC93;">apply</span> <span style="color:#ECEC93;">--dry-run</span>

<span style="color:#8A8A8A;"># Apply pending migrations</span>
bunny db migrations <span style="color:#ECEC93;">apply</span>
</pre>

<br>
<!--kg-card-end: html-->
<p>If your application eventually grows into a framework with its own migration system, use that one. When you create your first Bunny Database and wonder how you&apos;re meant to manage schema changes, there&apos;s an answer already sitting in the CLI.</p><h2 id="local-development-and-ci">Local development and CI</h2>
<!--kg-card-begin: html-->
<p>
  The same workflow works when nobody is sitting at the terminal. Interactively, <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">apply</code> asks for confirmation before it changes the database. In a non-interactive environment such as CI it skips the prompt, so your deployment isn&apos;t stuck waiting for someone to answer it. You can skip confirmation explicitly too:
</p>

<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny db migrations <span style="color:#ECEC93;">apply</span> <span style="color:#ECEC93;">--force</span>
</pre>
<br>
<!--kg-card-end: html-->
<p>Once the migration files are committed alongside your application, your deployment pipeline can apply whatever migrations came with a release:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny db migrations <span style="color:#ECEC93;">apply</span>
</pre>
<br>
<!--kg-card-end: html-->
<p>The same files you used in development travel through the pipeline with the code that depends on them, so there&apos;s no separate pile of production SQL scripts to maintain.</p><h2 id="get-started">Get started</h2><p>If you already have a migration tool you love, keep using it. If you don&apos;t, getting started takes a few commands:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#ECEC93;">npm install</span> <span style="color:#ECEC93;">-g</span> @bunny.net/cli

bunny <span style="color:#ECEC93;">login</span>

bunny db migrations <span style="color:#ECEC93;">create</span> my_first_migration
</pre>
<br>
<!--kg-card-end: html-->
<p>Write some SQL, check what&apos;s waiting, and apply it:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny db migrations <span style="color:#ECEC93;">list</span>
bunny db migrations <span style="color:#ECEC93;">apply</span>
</pre>
<br>
<!--kg-card-end: html-->
<p>Your schema changes stay next to the code that depends on them, your database remembers what it has already applied, and that folder of mystery SQL files turns into something you can read as a history.</p><p>The bunny.net CLI is open source and built in public. If something&apos;s broken, or you&apos;ve got an idea for making migrations better, come talk to us on <a href="https://github.com/BunnyWay/cli/issues" rel="noreferrer">GitHub</a> or <a href="https://discord.com/invite/bunnynet" rel="noreferrer">Discord</a>.</p>]]></content:encoded></item><item><title><![CDATA[Query Bunny Database with fetch and nothing else]]></title><description><![CDATA[The new Bunny Database client pairs naturally with Edge Scripting: query your database right inside an edge function and return the result, with no separate backend in between.]]></description><link>https://bunny.net/blog/query-bunny-database-with-fetch-and-nothing-else/</link><guid isPermaLink="false">6a981484160dc403fbfcf6f4</guid><category><![CDATA[News]]></category><dc:creator><![CDATA[Jamie Barton]]></dc:creator><pubDate>Wed, 02 Sep 2026 12:59:31 GMT</pubDate><media:content url="https://bunny.net/blog/content/images/2026/09/Bunny-Database-Client.png" medium="image"/><content:encoded><![CDATA[
<!--kg-card-begin: html-->
<img src="https://bunny.net/blog/content/images/2026/09/Bunny-Database-Client.png" alt="Query Bunny Database with fetch and nothing else"><p>
  We just shipped <a href="https://npmjs.org/@bunny.net/database-client"><code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">@bunny.net/database-client</code></a>, a small SQL client for <a href="https://bunny.net/docs/database/">Bunny Database</a>. It uses <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">fetch</code> with no external dependencies, and is built for <a href="https://bunny.net/docs/scripting">Bunny Edge Scripting</a>.
</p>

<!--kg-card-end: html-->
<p>The same code runs in Deno, Bun, Node, or any TypeScript function or API where you want to use Bunny Database without extra runtime dependencies.</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#569CD6;">import</span> * <span style="color:#569CD6;">as</span> BunnySDK <span style="color:#569CD6;">from</span> <span style="color:#BDE052;">&quot;npm:@bunny.net/edgescript-sdk@0.12.1&quot;</span>;
<span style="color:#569CD6;">import</span> { connect } <span style="color:#569CD6;">from</span> <span style="color:#BDE052;">&quot;npm:@bunny.net/database-client&quot;</span>;

<span style="color:#569CD6;">const</span> db = <span style="color:#D1949E;">connect</span>();
</pre><br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  With no arguments, <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">connect()</code> reads <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">BUNNY_DATABASE_URL</code> and <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">BUNNY_DATABASE_AUTH_TOKEN</code> from the environment, which is what <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny db link</code> and Edge Scripting already set.
</p>

<!--kg-card-end: html-->
<p>There are working examples for <a href="https://github.com/BunnyWay/cli/tree/main/packages/database-client/examples/bun">Bun</a>, <a href="https://github.com/BunnyWay/cli/tree/main/packages/database-client/examples/node">Node</a>, <a href="https://github.com/BunnyWay/cli/tree/main/packages/database-client/examples/hono">Hono</a>, <a href="https://github.com/BunnyWay/cli/tree/main/packages/database-client/examples/edge-script">Edge Scripting</a>, <a href="https://github.com/BunnyWay/cli/tree/main/packages/database-client/examples/next">Next.js</a>, <a href="https://github.com/BunnyWay/cli/tree/main/packages/database-client/examples/astro">Astro</a>, and <a href="https://github.com/BunnyWay/cli/tree/main/packages/database-client/examples/sveltekit">SvelteKit</a> if you want to read code before you read the rest of this post.</p><p><strong>The library is experimental, a work-in-progress, and open to contribution.</strong></p><h2 id="queries">Queries</h2><p>Statements are prepared, bound, and then executed with whichever method fits the shape you want back:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#569CD6;">const</span> rows = <span style="color:#569CD6;">await</span> db.<span style="color:#D1949E;">prepare</span>(<span style="color:#BDE052;">&quot;SELECT id, name FROM users&quot;</span>).<span style="color:#D1949E;">all</span>();
<span style="color:#8A8A8A;">// [{ id: 1, name: &quot;Alice&quot; }, { id: 2, name: &quot;Bob&quot; }]</span>

<span style="color:#569CD6;">const</span> user = <span style="color:#569CD6;">await</span> db.<span style="color:#D1949E;">prepare</span>(<span style="color:#BDE052;">&quot;SELECT * FROM users WHERE id = ?&quot;</span>).<span style="color:#D1949E;">bind</span>(<span style="color:#D1949E;">1</span>).<span style="color:#D1949E;">first</span>();
<span style="color:#8A8A8A;">// { id: 1, name: &quot;Alice&quot; }  or  null</span>

<span style="color:#569CD6;">const</span> name = <span style="color:#569CD6;">await</span> db.<span style="color:#D1949E;">prepare</span>(<span style="color:#BDE052;">&quot;SELECT name FROM users WHERE id = ?&quot;</span>).<span style="color:#D1949E;">bind</span>(<span style="color:#D1949E;">1</span>).<span style="color:#D1949E;">first</span>(<span style="color:#BDE052;">&quot;name&quot;</span>);
<span style="color:#8A8A8A;">// &quot;Alice&quot;  or  null</span>
</pre><br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">run()</code> adds write metadata:
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#569CD6;">const</span> result = <span style="color:#569CD6;">await</span> db
  .<span style="color:#D1949E;">prepare</span>(<span style="color:#BDE052;">&quot;INSERT INTO users (name) VALUES (?) RETURNING id&quot;</span>)
  .<span style="color:#D1949E;">bind</span>(<span style="color:#BDE052;">&quot;Carol&quot;</span>)
  .<span style="color:#D1949E;">run</span>();
<span style="color:#8A8A8A;">// { rows: [{ id: 3 }], columns: [&quot;id&quot;], rowsAffected: 1, lastInsertRowid: 3 }</span>
</pre><br>
<!--kg-card-end: html-->
<p>Statements are immutable, so you can prepare once and bind as often as you like:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#569CD6;">const</span> byId = db.<span style="color:#D1949E;">prepare</span>(<span style="color:#BDE052;">&quot;SELECT id, name FROM users WHERE id = ?&quot;</span>);

<span style="color:#569CD6;">const</span> alice = <span style="color:#569CD6;">await</span> byId.<span style="color:#D1949E;">bind</span>(<span style="color:#D1949E;">1</span>).<span style="color:#D1949E;">first</span>();
<span style="color:#569CD6;">const</span> bob = <span style="color:#569CD6;">await</span> byId.<span style="color:#D1949E;">bind</span>(<span style="color:#D1949E;">2</span>).<span style="color:#D1949E;">first</span>();
</pre>
<br>
<!--kg-card-end: html-->
<p>SQLite&apos;s named parameters work too. Pass a single object instead of positional values, with or without the sigil:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#569CD6;">const</span> team = <span style="color:#569CD6;">await</span> db
  .<span style="color:#D1949E;">prepare</span>(<span style="color:#BDE052;">&quot;SELECT id, name FROM users WHERE team = :team AND active = :active&quot;</span>)
  .<span style="color:#D1949E;">bind</span>({ team: <span style="color:#BDE052;">&quot;platform&quot;</span>, active: <span style="color:#569CD6;">true</span> })
  .<span style="color:#D1949E;">all</span>();
</pre>

<br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  Rows are typed as <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">Record&lt;string, SqlValue&gt;</code> by default, so you can pass your own shape to
  skip the cast:
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#569CD6;">interface</span> <span style="color:#D1949E;">User</span> {
  id: <span style="color:#BDE052;">number</span>;
  name: <span style="color:#BDE052;">string</span>;
}

<span style="color:#569CD6;">const</span> users = <span style="color:#569CD6;">await</span> db.<span style="color:#D1949E;">prepare</span>(<span style="color:#BDE052;">&quot;SELECT id, name FROM users&quot;</span>).all&lt;<span style="color:#D1949E;">User</span>&gt;();
</pre>
<br>
<!--kg-card-end: html-->
<p>That type is an assertion <em>for now</em>. Nothing checks the rows against it at runtime, so it&#x2019;s only ever as accurate as your SQL.</p><h2 id="transactions-in-one-round-trip">Transactions in one round trip</h2>
<!--kg-card-begin: html-->
<p>
  <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">batch()</code> runs every statement in a single transaction and a single HTTP request. All of them commit or none do:
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#569CD6;">const</span> [inserted, count] = <span style="color:#569CD6;">await</span> db.<span style="color:#D1949E;">batch</span>([
  db.<span style="color:#D1949E;">prepare</span>(<span style="color:#BDE052;">&quot;INSERT INTO users (name) VALUES (?)&quot;</span>).<span style="color:#D1949E;">bind</span>(<span style="color:#BDE052;">&quot;Dan&quot;</span>),
  db.<span style="color:#D1949E;">prepare</span>(<span style="color:#BDE052;">&quot;SELECT COUNT(*) AS c FROM users&quot;</span>),
]);

inserted.rowsAffected; <span style="color:#8A8A8A;">// 1</span>
count.rows[<span style="color:#D1949E;">0</span>].c; <span style="color:#8A8A8A;">// 4</span>
</pre>
<br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  You get one result per statement you passed, in order. If any statement fails, the transaction rolls back and <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">batch()</code> throws that statement&apos;s error, so there is no half-applied state to reason about.
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  For schema setup, there is <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">db.exec()</code>, which runs a multi-statement script:
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#569CD6;">await</span> db.<span style="color:#D1949E;">exec</span>(<span style="color:#BDE052;">`</span><span style="color:#BDE052;">
  CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
  CREATE INDEX IF NOT EXISTS users_name ON users (name);
</span><span style="color:#BDE052;">`</span>);
</pre><br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  It takes no parameters and returns no rows. For a schema you expect to keep changing, reach for <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny db migrations</code>.
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny db migrations <span style="color:#ECEC93;">create</span> add_users_table  <span style="color:#8A8A8A;"># migrations/0001_add_users_table.sql</span>
bunny db migrations <span style="color:#ECEC93;">apply</span>
bunny db migrations <span style="color:#ECEC93;">list</span>
</pre><br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny db migrations apply --dry-run</code> lists what would run without touching the database. The migration commands are still marked experimental and could change based on your feedback.
</p>
<!--kg-card-end: html-->
<h2 id="errors-carry-the-sqlite-code">Errors carry the SQLite code</h2>
<!--kg-card-begin: html-->
<p>
  Errors are <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">DatabaseError</code> with the SQLite code attached, so you can catch a <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">SQLITE_CONSTRAINT</code> without string-matching a message.
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#569CD6;">import</span> { <span style="color:#D1949E;">DatabaseError</span> } <span style="color:#569CD6;">from</span> <span style="color:#BDE052;">&quot;@bunny.net/database-client&quot;</span>;

<span style="color:#569CD6;">try</span> {
  <span style="color:#569CD6;">await</span> db.<span style="color:#D1949E;">prepare</span>(<span style="color:#BDE052;">&quot;INSERT INTO users (email) VALUES (?)&quot;</span>).<span style="color:#D1949E;">bind</span>(email).<span style="color:#D1949E;">run</span>();
} <span style="color:#569CD6;">catch</span> (error) {
  <span style="color:#8A8A8A;">// Prefix match, so this catches SQLITE_CONSTRAINT and its extended forms alike.</span>
  <span style="color:#569CD6;">if</span> (error instanceof <span style="color:#D1949E;">DatabaseError</span> &amp;&amp; error.code?.<span style="color:#D1949E;">startsWith</span>(<span style="color:#BDE052;">&quot;SQLITE_CONSTRAINT&quot;</span>)) {
    <span style="color:#569CD6;">return</span> <span style="color:#BDE052;">Response</span>.<span style="color:#D1949E;">json</span>({ error: <span style="color:#BDE052;">&quot;That email is taken.&quot;</span> }, { status: <span style="color:#D1949E;">409</span> });
  }

  <span style="color:#569CD6;">throw</span> error;
}
</pre><br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  Failures that happen before any SQL runs are wrapped the same way, with codes <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">NETWORK</code>, <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">TIMEOUT</code>, and <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">ABORTED</code>, so one <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">instanceof DatabaseError</code> covers the whole surface and no <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">TypeError</code> slips past it.
</p>
<!--kg-card-end: html-->
<h2 id="keep-it-on-the-server">Keep it on the server</h2><p>A database token authorizes the connection, and anything holding it can run whatever SQL the token allows. This client belongs in server-side code only. Never ship it, or a token, to a browser.</p><p>The pattern we typically see from users is an Edge Script sitting in front of Bunny Database. The token stays on the server, and the browser only sees the responses you choose to return:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#569CD6;">import</span> * <span style="color:#569CD6;">as</span> BunnySDK <span style="color:#569CD6;">from</span> <span style="color:#BDE052;">&quot;npm:@bunny.net/edgescript-sdk@0.12.1&quot;</span>;
<span style="color:#569CD6;">import</span> { connect, <span style="color:#D1949E;">DatabaseError</span> } <span style="color:#569CD6;">from</span> <span style="color:#BDE052;">&quot;npm:@bunny.net/database-client&quot;</span>;

<span style="color:#569CD6;">const</span> db = <span style="color:#D1949E;">connect</span>({ timeout: <span style="color:#D1949E;">2000</span> });

BunnySDK.net.http.<span style="color:#D1949E;">serve</span>(<span style="color:#569CD6;">async</span> (request: <span style="color:#D1949E;">Request</span>): Promise&lt;<span style="color:#D1949E;">Response</span>&gt; =&gt; {
  <span style="color:#569CD6;">const</span> { pathname } = new <span style="color:#D1949E;">URL</span>(request.url);
  <span style="color:#569CD6;">if</span> (pathname !== <span style="color:#BDE052;">&quot;/notes&quot;</span>) <span style="color:#569CD6;">return</span> new <span style="color:#D1949E;">Response</span>(<span style="color:#BDE052;">&quot;Not found&quot;</span>, { status: <span style="color:#D1949E;">404</span> });

  <span style="color:#569CD6;">try</span> {
    <span style="color:#569CD6;">if</span> (request.method === <span style="color:#BDE052;">&quot;POST&quot;</span>) {
      <span style="color:#569CD6;">const</span> payload = <span style="color:#569CD6;">await</span> request.<span style="color:#D1949E;">json</span>().<span style="color:#569CD6;">catch</span>(() =&gt; <span style="color:#569CD6;">null</span>);
      <span style="color:#569CD6;">const</span> title = typeof payload?.title === <span style="color:#BDE052;">&quot;string&quot;</span> ? payload.title.<span style="color:#D1949E;">trim</span>() : <span style="color:#BDE052;">&quot;&quot;</span>;
      <span style="color:#569CD6;">if</span> (!title) {
        <span style="color:#569CD6;">return</span> <span style="color:#BDE052;">Response</span>.<span style="color:#D1949E;">json</span>({ error: <span style="color:#BDE052;">&quot;A title is required.&quot;</span> }, { status: <span style="color:#D1949E;">400</span> });
      }

      <span style="color:#569CD6;">const</span> note = <span style="color:#569CD6;">await</span> db
        .<span style="color:#D1949E;">prepare</span>(<span style="color:#BDE052;">&quot;INSERT INTO notes (title) VALUES (?) RETURNING id, title&quot;</span>)
        .<span style="color:#D1949E;">bind</span>(title)
        .<span style="color:#D1949E;">first</span>();

      <span style="color:#569CD6;">return</span> <span style="color:#BDE052;">Response</span>.<span style="color:#D1949E;">json</span>(note, { status: <span style="color:#D1949E;">201</span> });
    }

    <span style="color:#8A8A8A;">// Parameterized, and scoped to the columns and rows the caller may see.</span>
    <span style="color:#569CD6;">const</span> notes = <span style="color:#569CD6;">await</span> db
      .<span style="color:#D1949E;">prepare</span>(<span style="color:#BDE052;">&quot;SELECT id, title FROM notes WHERE published = 1 ORDER BY id LIMIT 50&quot;</span>)
      .<span style="color:#D1949E;">all</span>();

    <span style="color:#569CD6;">return</span> <span style="color:#BDE052;">Response</span>.<span style="color:#D1949E;">json</span>(notes);
  } <span style="color:#569CD6;">catch</span> (error) {
    <span style="color:#8A8A8A;">// The server&apos;s message and SQLite code stay in your logs, not in the response.</span>
    console.<span style="color:#D1949E;">error</span>(error instanceof <span style="color:#D1949E;">DatabaseError</span> ? <span style="color:#BDE052;">`</span><span style="color:#BDE052;">${</span>error.code<span style="color:#BDE052;">}</span><span style="color:#BDE052;">: </span><span style="color:#BDE052;">${</span>error.message<span style="color:#BDE052;">}</span><span style="color:#BDE052;">`</span> : error);

    <span style="color:#569CD6;">return</span> <span style="color:#BDE052;">Response</span>.<span style="color:#D1949E;">json</span>({ error: <span style="color:#BDE052;">&quot;Something went wrong.&quot;</span> }, { status: <span style="color:#D1949E;">500</span> });
  }
});
</pre>
<br>
<!--kg-card-end: html-->
<p>The browser calls your endpoint, and your endpoint decides what SQL runs. Narrow the token while you are at it using the bunny.net CLI:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny db tokens <span style="color:#ECEC93;">create</span> <span style="color:#ECEC93;">--read-only</span> <span style="color:#ECEC93;">--expiry</span> 30d
</pre>
<!--kg-card-end: html-->
<p>A narrowed token still exposes every row of every table to whatever holds it, because it authorizes the connection and not the individual query. It limits the damage without removing it, which is why the token stays on your side of the wire.</p><h2 id="it-works-with-your-framework-too">It works with your framework too</h2>
<!--kg-card-begin: html-->
<p>
  <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">@bunny.net/database-client</code> works with the frameworks you already use. Each one keeps its environment variables somewhere slightly different, so that&apos;s the only part of the setup that changes:
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#8A8A8A;">// Next.js, where .env is already on process.env</span>
<span style="color:#569CD6;">const</span> db = () =&gt; <span style="color:#D1949E;">connect</span>();

<span style="color:#8A8A8A;">// Astro, where it is on import.meta.env</span>
<span style="color:#569CD6;">const</span> db = () =&gt;
  <span style="color:#D1949E;">connect</span>({
    url: <span style="color:#D1949E;">import.meta.env.BUNNY_DATABASE_URL</span>,
    authToken: <span style="color:#D1949E;">import.meta.env.BUNNY_DATABASE_AUTH_TOKEN</span>,
  });

<span style="color:#8A8A8A;">// SvelteKit, where $env/dynamic/private keeps it out of the client bundle</span>
<span style="color:#569CD6;">const</span> db = () =&gt;
  <span style="color:#D1949E;">connect</span>({
    url: <span style="color:#D1949E;">env.BUNNY_DATABASE_URL</span>,
    authToken: <span style="color:#D1949E;">env.BUNNY_DATABASE_AUTH_TOKEN</span>,
  });
</pre>
<br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  Once <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">db()</code> is wired up, the query code looks the same wherever you call it from:
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#569CD6;">export</span> <span style="color:#569CD6;">const</span> GET: <span style="color:#D1949E;">RequestHandler</span> = <span style="color:#569CD6;">async</span> () =&gt;
  <span style="color:#BDE052;">Response</span>.<span style="color:#D1949E;">json</span>(
    <span style="color:#569CD6;">await</span> <span style="color:#D1949E;">db</span>().<span style="color:#D1949E;">prepare</span>(<span style="color:#BDE052;">&quot;SELECT id, title FROM notes ORDER BY id&quot;</span>).<span style="color:#D1949E;">all</span>(),
  );

<span style="color:#569CD6;">export</span> <span style="color:#569CD6;">const</span> POST: <span style="color:#D1949E;">RequestHandler</span> = <span style="color:#569CD6;">async</span> ({ request }) =&gt; {
  <span style="color:#569CD6;">const</span> { title } = (<span style="color:#569CD6;">await</span> request.<span style="color:#D1949E;">json</span>()) <span style="color:#569CD6;">as</span> { title?: <span style="color:#BDE052;">string</span> };
  <span style="color:#569CD6;">if</span> (!title?.<span style="color:#D1949E;">trim</span>()) {
    <span style="color:#569CD6;">return</span> <span style="color:#BDE052;">Response</span>.<span style="color:#D1949E;">json</span>({ error: <span style="color:#BDE052;">&quot;A title is required.&quot;</span> }, { status: <span style="color:#D1949E;">400</span> });
  }

  <span style="color:#569CD6;">return</span> <span style="color:#BDE052;">Response</span>.<span style="color:#D1949E;">json</span>(
    <span style="color:#569CD6;">await</span> <span style="color:#D1949E;">db</span>()
      .<span style="color:#D1949E;">prepare</span>(<span style="color:#BDE052;">&quot;INSERT INTO notes (title) VALUES (?) RETURNING id, title&quot;</span>)
      .<span style="color:#D1949E;">bind</span>(title.<span style="color:#D1949E;">trim</span>())
      .<span style="color:#D1949E;">first</span>(),
    { status: <span style="color:#D1949E;">201</span> },
  );
};
</pre>
<br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  React Server Components can call <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">db()</code> directly too:
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#569CD6;">export</span> <span style="color:#569CD6;">default</span> <span style="color:#569CD6;">async</span> <span style="color:#569CD6;">function</span> <span style="color:#D1949E;">Page</span>() {
  <span style="color:#569CD6;">const</span> notes = <span style="color:#569CD6;">await</span> <span style="color:#D1949E;">db</span>()
    .<span style="color:#D1949E;">prepare</span>(<span style="color:#BDE052;">&quot;SELECT id, title FROM notes ORDER BY id&quot;</span>)
    .all&lt;{ id: <span style="color:#BDE052;">number</span>; title: <span style="color:#BDE052;">string</span> }&gt;();

  <span style="color:#569CD6;">async</span> <span style="color:#569CD6;">function</span> <span style="color:#D1949E;">create</span>(formData: <span style="color:#D1949E;">FormData</span>) {
    <span style="color:#BDE052;">&quot;use server&quot;</span>;
    <span style="color:#569CD6;">const</span> title = String(formData.<span style="color:#D1949E;">get</span>(<span style="color:#BDE052;">&quot;title&quot;</span>) ?? <span style="color:#BDE052;">&quot;&quot;</span>).<span style="color:#D1949E;">trim</span>();
    <span style="color:#569CD6;">if</span> (!title) <span style="color:#569CD6;">return</span>;

    <span style="color:#569CD6;">await</span> <span style="color:#D1949E;">db</span>().<span style="color:#D1949E;">prepare</span>(<span style="color:#BDE052;">&quot;INSERT INTO notes (title) VALUES (?)&quot;</span>).<span style="color:#D1949E;">bind</span>(title).<span style="color:#D1949E;">run</span>();
    <span style="color:#D1949E;">revalidatePath</span>(<span style="color:#BDE052;">&quot;/&quot;</span>);
  }

  <span style="color:#569CD6;">return</span> (
    &lt;<span style="color:#D1949E;">main</span>&gt;
      &lt;<span style="color:#D1949E;">form</span> action={create}&gt;
        &lt;<span style="color:#D1949E;">input</span> name=<span style="color:#BDE052;">&quot;title&quot;</span> placeholder=<span style="color:#BDE052;">&quot;Note title&quot;</span> /&gt;
        &lt;<span style="color:#D1949E;">button</span> type=<span style="color:#BDE052;">&quot;submit&quot;</span>&gt;Add&lt;/<span style="color:#D1949E;">button</span>&gt;
      &lt;/<span style="color:#D1949E;">form</span>&gt;
      &lt;<span style="color:#D1949E;">ul</span>&gt;
        {notes.<span style="color:#D1949E;">map</span>((note) =&gt; (
          &lt;<span style="color:#D1949E;">li</span> key={note.id}&gt;{note.title}&lt;/<span style="color:#D1949E;">li</span>&gt;
        ))}
      &lt;/<span style="color:#D1949E;">ul</span>&gt;
    &lt;/<span style="color:#D1949E;">main</span>&gt;
  );
}
</pre>
<br>
<!--kg-card-end: html-->
<h2 id="try-it">Try it</h2><p>This is an early release, and designed to be small. Create a Bunny Database using the Dashboard or the bunny.net CLI:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#ECEC93;">npm install</span> -g @bunny.net/cli
bunny login
bunny db <span style="color:#ECEC93;">create</span>
</pre>

<br>
<!--kg-card-end: html-->
<p>Then pick an example to get you started:</p>
<!--kg-card-begin: html-->
<table style="border:1px solid rgba(24, 61, 109, 0.18); border-collapse:collapse; width:100%;">
  <thead>
    <tr style="font-weight:700; text-align:left; background:#223c6a; color:white;">
      <th style="padding:12px 14px; background:rgba(255,255,255,0.03); border-bottom:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); color:white;">Example</th>
      <th style="padding:12px 14px; border-bottom:1px solid rgba(24, 61, 109, 0.18); color:white;">Runs on</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="padding:12px 14px; border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;"><a href="https://github.com/BunnyWay/cli/tree/main/packages/database-client/examples/bun"><code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bun/</code></a></td>
      <td style="padding:12px 14px; vertical-align:middle; color:#183d6d;">Bun</td>
    </tr>
    <tr>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;"><a href="https://github.com/BunnyWay/cli/tree/main/packages/database-client/examples/node"><code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">node/</code></a></td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Node 24+</td>
    </tr>
    <tr>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;"><a href="https://github.com/BunnyWay/cli/tree/main/packages/database-client/examples/hono"><code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">hono/</code></a></td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Bun or Deno</td>
    </tr>
    <tr>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;"><a href="https://github.com/BunnyWay/cli/tree/main/packages/database-client/examples/edge-script"><code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">edge-script/</code></a></td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Bunny Edge Scripting</td>
    </tr>
    <tr>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;"><a href="https://github.com/BunnyWay/cli/tree/main/packages/database-client/examples/next"><code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">next/</code></a></td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Next.js</td>
    </tr>
    <tr>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;"><a href="https://github.com/BunnyWay/cli/tree/main/packages/database-client/examples/astro"><code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">astro/</code></a></td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Astro</td>
    </tr>
    <tr>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;"><a href="https://github.com/BunnyWay/cli/tree/main/packages/database-client/examples/sveltekit"><code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">sveltekit/</code></a></td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">SvelteKit</td>
    </tr>
  </tbody>
</table>
<br>
<!--kg-card-end: html-->
<p>When you&apos;re ready to deploy, push an Edge Script with the CLI, or deploy a framework app to <a href="https://bunny.net/docs/magic-containers" rel="noreferrer">Magic Containers</a>.</p><p>If you&apos;ve already got a project you want to add Bunny Database to, install the client with your package manager of choice:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#ECEC93;">npm install</span> @bunny.net/database-client
<span style="color:#8A8A8A;"># pnpm add @bunny.net/database-client</span>
<span style="color:#8A8A8A;"># bun add @bunny.net/database-client</span>
</pre>
<br>
<!--kg-card-end: html-->
<p>If something you need is missing, tell us on <a href="https://discord.com/invite/bunnynet" rel="noreferrer">Discord</a>, or open a pull request on <a href="https://github.com/BunnyWay/cli/issues" rel="noreferrer">GitHub</a>. The CLI is open to contributions from the community.</p>]]></content:encoded></item><item><title><![CDATA[You can now manage Bunny Storage with the bunny.net CLI]]></title><description><![CDATA[Create Storage Zones, connect them to Pull Zones, upload and download files, configure custom domains, and retrieve S3-compatible credentials, all without leaving your terminal.]]></description><link>https://bunny.net/blog/you-can-now-manage-bunny-storage-with-the-bunny-net-cli/</link><guid isPermaLink="false">6a96b380160dc403fbfcf6a1</guid><category><![CDATA[News]]></category><dc:creator><![CDATA[Jamie Barton]]></dc:creator><pubDate>Tue, 01 Sep 2026 11:37:38 GMT</pubDate><media:content url="https://bunny.net/blog/content/images/2026/09/bunny-storage-added-to-CLI.png" medium="image"/><content:encoded><![CDATA[<img src="https://bunny.net/blog/content/images/2026/09/bunny-storage-added-to-CLI.png" alt="You can now manage Bunny Storage with the bunny.net CLI"><p>When we introduced the bunny.net CLI, it shipped with full <a href="https://bunny.net/blog/introducing-the-bunny-net-cli/" rel="noreferrer">Database</a> support, and we said more of the stack would follow. Since then, we&#x2019;ve added support for <a href="https://bunny.net/blog/build-and-deploy-scripts-at-the-edge-with-the-bunny-net-cli/" rel="noreferrer">Edge Scripting</a>, <a href="https://bunny.net/blog/manage-dns-from-the-terminal-with-bunny-net-cli/" rel="noreferrer">DNS</a>, and <a href="https://bunny.net/blog/introducing-bunny-sandbox-a-disposable-room-where-ai-can-build/" rel="noreferrer">Sandboxes</a>.</p><p><strong>Today, we&#x2019;re bringing Bunny Storage to the CLI in Public Preview.</strong></p><p>You can now create Storage Zones, connect them to Pull Zones, upload and download files, configure custom domains, and retrieve S3-compatible credentials, all without leaving your terminal.</p><figure class="kg-card kg-image-card"><img src="https://bunny.net/blog/content/images/2026/09/Bunny-Storage-manual-vs.-CLI-1.png" class="kg-image" alt="You can now manage Bunny Storage with the bunny.net CLI" loading="lazy" width="1846" height="919" srcset="https://bunny.net/blog/content/images/size/w600/2026/09/Bunny-Storage-manual-vs.-CLI-1.png 600w, https://bunny.net/blog/content/images/size/w1000/2026/09/Bunny-Storage-manual-vs.-CLI-1.png 1000w, https://bunny.net/blog/content/images/size/w1600/2026/09/Bunny-Storage-manual-vs.-CLI-1.png 1600w, https://bunny.net/blog/content/images/2026/09/Bunny-Storage-manual-vs.-CLI-1.png 1846w" sizes="(min-width: 720px) 720px"></figure><p>None of these steps is particularly difficult on its own; they&apos;re just spread across different parts of the platform. The CLI brings everything together into a single workflow.</p><h2 id="two-zones-one-job">Two zones, one job</h2><p>Bunny Storage is built around two services that work together: a Storage Zone holds your files, and a Pull Zone serves them over HTTP through the bunny.net CDN.</p><p>Files in a Storage Zone aren&apos;t publicly reachable on their own. The Pull Zone sits in front, treats storage as its origin, caches the result across the network, and serves it to your users.</p><p>The CLI knows this, so it can do both at once:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny storage zones <span style="color:#ECEC93;">add</span>
</pre><br>

<!--kg-card-end: html-->
<p>That creates the Storage Zone, offers to create a Pull Zone alongside it, connects the two, and offers to attach a custom domain. One command, and you end up with a URL that resolves.</p><h2 id="create-your-storage">Create your storage</h2><p>Everything starts with a Storage Zone. Pick a name, choose a tier, choose a main region, optionally add replication, and you can start uploading.</p><p>Run it with no arguments and it asks:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny storage zones <span style="color:#ECEC93;">add</span>
</pre><br>
<!--kg-card-end: html-->
<p>Every one of those prompts has a matching flag, so the same command works in CI:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny storage zones <span style="color:#ECEC93;">add</span> my-storage <span style="color:#ECEC93;">--region</span> <span style="color:#D1949E;">DE</span>
</pre><br>
<!--kg-card-end: html-->
<p>Two of those choices are permanent, so it&apos;s worth knowing about them before you answer.</p>
<!--kg-card-begin: html-->
<p>
  The tier is either Standard (HDD) or Edge (SSD). <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">--tier ssd</code> is faster and always lives in Frankfurt, which is why it won&apos;t let you pass a different main region.
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  S3 compatibility is switched on at creation with <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">--s3</code> and cannot be added later while S3 is in Public Preview. If you think you might want the S3 API, turn it on now.
</p>
<!--kg-card-end: html-->
<p>If you&apos;re not sure which region code you need, the CLI will tell you:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny storage <span style="color:#ECEC93;">regions</span> <span style="color:#ECEC93;">--tier</span> ssd
</pre><br>
<!--kg-card-end: html-->
<h2 id="replicate-where-it-matters">Replicate where it matters</h2><p>Storage doesn&apos;t have to live in one place. When you create a Storage Zone, you can pick extra regions to replicate into:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny storage zones <span style="color:#ECEC93;">add</span> my-storage <span style="color:#ECEC93;">--region</span> <span style="color:#D1949E;">DE</span> <span style="color:#ECEC93;">--replicate</span> <span style="color:#D1949E;">NY,SG</span>
</pre><br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  Replicas are permanent. There&apos;s no API to remove one once it exists, so the CLI confirms before adding any, and <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">zones update --replication</code> only ever adds to the set.
</p>
<!--kg-card-end: html-->
<h2 id="work-with-your-files">Work with your files</h2><p>Once the zone exists, you can list, upload, download, and delete files:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny storage files <span style="color:#ECEC93;">list</span>
bunny storage files <span style="color:#ECEC93;">upload</span> ./photo.png <span style="color:#ECEC93;">--to</span> images/
bunny storage files <span style="color:#ECEC93;">download</span> images/photo.png
bunny storage files <span style="color:#ECEC93;">remove</span> images/photo.png
</pre><br>
<!--kg-card-end: html-->
<p>Notice what&apos;s missing from those commands: the zone. Link your project&#x2019;s directory once, and every file command defaults to it.</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny storage <span style="color:#ECEC93;">link</span> my-storage
</pre><br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  You&#x2019;ll end up with a <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">.bunny</code> metadata folder the CLI can reference when you run commands without flags, so other developers can clone your code and run their own storage zones in development too. The CLI already knows which regional endpoint and which password belong to that zone, so there&#x2019;s no hostname to look up and no SDK to configure first.
</p>

<!--kg-card-end: html-->
<h2 id="s3-compatible-credentials">S3-compatible credentials</h2><p>Not every tool speaks the bunny.net Storage API, which is why developers have been asking for S3-compatible Bunny Storage for years. It&#x2019;s currently in Public Preview, and since most tools already speak S3, it&#x2019;s a perfect fit for the CLI.</p>
<!--kg-card-begin: html-->
<p>
  If your zone was created with <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">--s3</code>, the CLI can hand its credentials to those tools in the format each one expects: the AWS CLI, <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">rclone</code>, <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">s3cmd</code>, shell environment variables, or JSON.
</p>

<!--kg-card-end: html-->
<p>Export them into your current shell:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#ECEC93;">eval</span> <span style="color:#BDE052;">&quot;$(bunny storage zones credentials my-storage --format env)&quot;</span>
</pre><br>
<!--kg-card-end: html-->
<p>Now anything that speaks S3 can use them:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#ECEC93;">aws</span> s3 <span style="color:#ECEC93;">sync</span> ./dist s3://my-storage/assets
</pre><br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  Or write an <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">rclone</code> remote straight into your config:
</p>

<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny storage zones <span style="color:#ECEC93;">credentials</span> my-storage \
  <span style="color:#ECEC93;">--format</span> rclone \
  &gt;&gt; ~/.config/rclone/rclone.conf
</pre><br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  There are no extra keys to create or rotate. These are your existing Storage Zone credentials, reshaped. Pass <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">--read-only</code> if the tool on the other end only needs to read.
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  Secrets are masked by default, including under <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">--output json</code>. You get the real value when you ask for it with <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">--show-secret</code>, or when you ask for a tool config, because a config with a masked password in it is no use to anyone.
</p>
<!--kg-card-end: html-->
<p>If you already have S3 tooling, nothing in your workflow has to change. S3 support is in Public Preview, so check the <a href="https://bunny.net/docs/storage/s3#s3-api-compatibility-matrix">full compatibility</a> details before you point production at it.</p><h2 id="built-for-automation">Built for automation</h2><p>Like everything else in the bunny.net CLI, Storage works the same in a script as it does in a terminal.</p>
<!--kg-card-begin: html-->
<p>
  Every prompt has a flag. Every command has <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">--output json</code>. Nothing waits on an answer that isn&apos;t coming: run without a TTY, or pass <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">--force</code>, and anything that would have prompted either takes the flag value or tells you which flag it needed.
</p>

<!--kg-card-end: html-->
<p>The commands you run on your laptop are the commands you run in CI.</p><h2 id="whats-next">What&apos;s next?</h2><p>Bunny Storage joins <a href="https://bunny.net/docs/cli/commands/db" rel="noreferrer">Bunny Database</a>, <a href="https://bunny.net/docs/cli/commands/scripts" rel="noreferrer">Edge Scripting</a>, <a href="https://bunny.net/docs/dns/scriptable/introduction#cli" rel="noreferrer">DNS</a>, and <a href="https://bunny.net/docs/cli/commands/sandbox" rel="noreferrer">Sandboxes</a> in the CLI today.</p><p>Magic Containers are next. The goal hasn&apos;t changed:</p><ul><li>Start with an empty terminal.</li><li>Provision your infrastructure.</li><li>Deploy your application.</li><li>Upload your assets.</li><li>Manage your services.</li></ul><p>All without leaving the command line.</p>]]></content:encoded></item><item><title><![CDATA[Built on Bunny - July 2026]]></title><description><![CDATA[Every month developers build remarkable things on bunny.net. Here's the latest batch with weather services, course platforms, migrations, and open-source tools.]]></description><link>https://bunny.net/blog/built-on-bunny-july-2026/</link><guid isPermaLink="false">6a7b10f2160dc403fbfcf680</guid><category><![CDATA[News]]></category><dc:creator><![CDATA[Dino Kukic]]></dc:creator><pubDate>Tue, 11 Aug 2026 12:20:08 GMT</pubDate><media:content url="https://bunny.net/blog/content/images/2026/08/Built-on-bunny-July.png" medium="image"/><content:encoded><![CDATA[<img src="https://bunny.net/blog/content/images/2026/08/Built-on-bunny-July.png" alt="Built on Bunny - July 2026"><p>It&apos;s been a busy couple of months. We <a href="https://bunny.net/blog/were-making-bunny-dns-free/">made Bunny DNS free</a>, <a href="https://www.linkedin.com/posts/bunnynet_internet-developer-application-activity-7473695258804158464-bZEX">crossed 100,000 paying customers</a>, and throughout all of that, you kept building genuinely cool things on top of bunny.net. Here&apos;s a fresh batch of projects, migrations, and community tools we spotted.</p><p>As always, if you&apos;ve built something on bunny.net and want it featured, let us know.</p><pre><code class="language-python">The community packages below are built by people working 
with bunny.net, but they aren&apos;t officially maintained by us.</code></pre><h2 id="shipped-on-bunnynet">Shipped on bunny.net</h2><h3 id="airpultprivacy-first-weather-running-on-most-of-the-platform">Airpult - privacy-first weather, running on most of the platform</h3><p><a href="https://airpult.com/">Airpult</a> is an independent, EU-based weather service with its own forecasting pipeline, API, and apps, plus a genuine privacy stance to go with it. Alerts use blind-signed tokens so notifications can&apos;t be tied back to you, there are no ad SDKs or data brokers, and everything runs on EU infrastructure.</p><p>Airpult runs all its traffic through our <a href="https://bunny.net/cdn/">CDN</a>, uses <a href="https://bunny.net/optimizer/">Bunny Optimizer</a> to keep things fast, recently added <a href="https://bunny.net/shield/">Bunny Shield</a> in front of its API traffic, and just started using <a href="https://bunny.net/storage/">Bunny Storage</a> with our new <a href="https://bunny.net/docs/storage/s3">S3-compatible</a> API to power part of its map product.</p><h3 id="jens-schmidt-hansena-board-game-side-project-that-runs-on-3-banana-splits-a-month">Jens Schmidt Hansen - a board-game side project that runs on &quot;3 banana splits a month&quot;</h3><p>I really like seeing side-project cost breakdowns across socials. Jens shared <a href="https://www.linkedin.com/posts/schmidthansen_%F0%9D%97%9C-%F0%9D%97%B7%F0%9D%98%82%F0%9D%98%80%F0%9D%98%81-%F0%9D%97%BF%F0%9D%97%B2%F0%9D%97%B0%F0%9D%97%B2%F0%9D%97%B6%F0%9D%98%83%F0%9D%97%B2%F0%9D%97%B1-%F0%9D%98%81%F0%9D%97%B5%F0%9D%97%B2-%F0%9D%97%AF%F0%9D%97%B6%F0%9D%97%B9%F0%9D%97%B9%F0%9D%98%80-share-7470074517730570241-17cb/">one for his side project</a>, a niche tool for organizing board-game nights that handles 1.5 million board games for around 15,000 collectors and is used in board-game caf&#xE9;s across a dozen countries. Total monthly bill: netcup &#x20AC;20.19, bunny.net ~&#x20AC;1.77, Scaleway &#x20AC;0.03. Roughly three banana splits.</p><p>The architecture is also worth taking a look. Clients upload directly to S3-compatible storage via a presigned URL, a third-party vendor compresses each file to WebP and scans it for malware, then deletes the original. Jens then lets Bunny Storage <strong>replicate the compressed file across 10 regions</strong> at $0.20/GB. At ~50&#x2013;70 KB per file, that&apos;s 14,000&#x2013;20,000 uploads per GB, or as he puts it, &quot;$1 = 70,000-100,000 user uploads.&quot;</p><h3 id="tina-mraka-prestashop-crawl-trap-and-the-fix">Tina Mrak - a PrestaShop crawl-trap, and the fix</h3><p>Tina Mrak wrote up <a href="https://tinamrak.com/blog/prestashop-faceted-search-creating-thousands-of-urls-plesk-blocking-bunny-cdn-ips-and-google-deindexing-pages">a great debugging story</a> that hit close to home, as I&#x2019;ve run into the same faceted-search problem in the past. PrestaShop&apos;s filters generated 500,000+ crawlable URLs, and bots crawled all of them.The origin&apos;s Plesk/Fail2Ban then ended up banning Bunny&apos;s CDN IPs, leading to Googlebot 500s and deindexed pages.</p><h2 id="shipped-for-bunnynet">Shipped for bunny.net</h2><p>The community tooling around bunny.net has had a great couple of months across a bunch of ecosystems.</p><p><strong>PHP &amp; Symfony</strong></p><ul><li><a href="https://github.com/four-bytes/flysystem-bunny-storage">flysystem-bunny-storage</a> - an actively maintained Flysystem adapter for Bunny Storage, now with v3 support and an async upload interface.</li><li><a href="https://packagist.org/packages/survos/bunny-bundle">survos/bunny-bundle</a> - a Symfony bundle for working with Bunny CDN.</li></ul><p><strong>Ruby</strong></p><ul><li><a href="https://github.com/thomaspietschmann/bunnycdn-rails">bunnycdn-rails</a> - unofficial Rails helpers for Bunny CDN and Bunny Storage. Good to see more Ruby love.</li></ul><p><strong>DNS &amp; infrastructure</strong></p><ul><li><a href="https://github.com/qdm12/ddns-updater">qdm12/ddns-updater</a> added bunny.net as a DNS provider - it&apos;s a hugely popular self-hosted tool, so this one reaches a lot of people.</li><li>The <a href="https://github.com/opnsense/plugins">OPNsense plugins</a> project added a native bunny.net dynamic DNS provider.</li><li><a href="https://github.com/caddy-dns/bunny">caddy-dns/bunny</a> and the underlying libdns provider kept improving, including adding DNSSEC support.</li></ul><p><strong>Analytics</strong></p><ul><li><a href="https://github.com/matomo-org/matomo-log-analytics">matomo-log-analytics</a> added a bunny.net log format</li><li><a href="https://github.com/PostHog/posthog">PostHog</a> added a bunny.net import source to its data warehouse and now allows you to pull data from <a href="https://bunny.net/docs/api-reference/core/pull-zone/list-pull-zones">pull zones</a>, <a href="https://bunny.net/docs/storage">storage zones</a>, <a href="https://bunny.net/docs/dns">DNS zones</a> and <a href="https://bunny.net/docs/stream">Stream video libraries</a>.</li></ul><p><strong>Security tooling</strong></p><ul><li><a href="https://pypi.org/project/octorules-bunny/">octorules-bunny</a>, the manage-your-Shield-WAF-rules-as-code tool we featured back in May, is still shipping, withseveral releases since then.</li></ul><h2 id="working-with-ai-agents-the-edge">Working with AI agents &amp; the edge</h2><ul><li><a href="https://github.com/Shroud-email">Shroud-email</a>, the privacy-focused email service, is using a <a href="https://bunny.net/edge-scripting/" rel="noreferrer">Bunny Edge Script</a> for geolocalized pricing, serving different prices by region right at the edge.</li><li><a href="https://github.com/stefanhoelzl/SnapSync">SnapSync</a> moved its device-facing runtime onto Bunny Edge Scripting (built by the same developer behind a full Scaleway-to-Bunny migration).</li><li><a href="https://github.com/hookdeck/webhook-skills">hookdeck/webhook-skills</a> shipped a <a href="https://bunny.net/stream/" rel="noreferrer">Bunny Stream</a> webhooks skill for AI agents.</li></ul><p>That&#x2019;s it for July. We&#x2019;ll be back next month with more projects, tools, and experiments from across the bunny.net community. If you&#x2019;d like to be featured, tag us on social media or share your project in our Discord community. <a href="https://discord.com/invite/bunnynet?ref=bunny.net">Join our Discord here</a>.</p>]]></content:encoded></item><item><title><![CDATA[Why attacks sometimes look like normal traffic]]></title><description><![CDATA[Why attacks that look like normal traffic are the hardest to stop. How L3/4 and L7 defenses differ, and what four real DDoS incidents reveal about each.]]></description><link>https://bunny.net/blog/why-attacks-sometimes-look-like-normal-traffic/</link><guid isPermaLink="false">6a6b2c76160dc403fbfcf606</guid><category><![CDATA[Security]]></category><dc:creator><![CDATA[Dino Kukic]]></dc:creator><pubDate>Wed, 05 Aug 2026 12:08:09 GMT</pubDate><media:content url="https://bunny.net/blog/content/images/2026/07/Why-attacks-look-like-normal-traffic.png" medium="image"/><content:encoded><![CDATA[<img src="https://bunny.net/blog/content/images/2026/07/Why-attacks-look-like-normal-traffic.png" alt="Why attacks sometimes look like normal traffic"><p><em>(This post is co-authored with </em><a href="https://rishi.app/" rel="noreferrer"><em>Rishi Raj Jain</em></a><em>)</em></p><p><strong>The more closely an attack resembles your normal traffic, the harder it is to stop.</strong> A spoofed packet flood is easy to drop precisely because nothing legitimate resembles it. When attack traffic uses valid requests, real IP addresses, and standard protocol behavior, your filters face a harder problem: you cannot block what you cannot distinguish.</p><p>This post explores where that split happens in the network stack and how it should shape the defenses you build. We&apos;ll compare two broad categories of attacks: those that expose clear signals at L3/4 and those that hide behind valid application traffic at L7. We&apos;ll also look at four real incidents that show the difference: Slowloris in 2009, M&#x113;ris in 2021, HTTP/2 Rapid Reset in 2023, and a lesser-known attack that can crash a server while leaving <strong>nothing in your access logs</strong>, the kind of attack you cannot even see, let alone block.</p><h2 id="layer-numbers-are-shorthand-for-filter-cost">Layer numbers are shorthand for filter cost</h2><figure class="kg-card kg-image-card"><img src="https://bunny.net/blog/content/images/2026/08/image4.png" class="kg-image" alt="Why attacks sometimes look like normal traffic" loading="lazy" width="1999" height="887" srcset="https://bunny.net/blog/content/images/size/w600/2026/08/image4.png 600w, https://bunny.net/blog/content/images/size/w1000/2026/08/image4.png 1000w, https://bunny.net/blog/content/images/size/w1600/2026/08/image4.png 1600w, https://bunny.net/blog/content/images/2026/08/image4.png 1999w" sizes="(min-width: 720px) 720px"></figure><p>At L3/4, spoofed SYN floods, memcached reflection bursts, and UDP amplification patterns all leave <strong>measurable signals</strong>: bandwidth spikes, impossible source behavior, and protocol states with no benign equivalent. Those signals appear before application code runs, which means you can make a drop decision at the edge cheaply before spending anything on TLS termination or HTTP parsing.</p><p>At L7, a botnet request and a customer request during a flash sale can be <strong>byte-for-byte identical on the wire</strong>. The signal you need is intent, and reading intent requires decrypting traffic, parsing requests, and comparing behavior against a baseline, all of which costs CPU before you can confidently block anything. That cost asymmetry is what makes L7 harder, and it grows as bots get better at mimicking real browsers.</p><h2 id="attack-size-is-a-poor-measure-of-difficulty">Attack size is a poor measure of difficulty</h2><p>When someone quotes an attack in terabits per second, the instinct is to reach for bandwidth planning and scrubbing capacity. That works for volumetric floods, but <strong>size is a weak proxy for mitigation difficulty</strong> once traffic looks application-shaped.</p>
<!--kg-card-begin: html-->
<table style="border:1px solid rgba(24, 61, 109, 0.18); border-collapse:collapse; width:100%;">
  <thead>
    <tr style="font-weight:700; text-align:left; background:#223c6a; color:white;">
      <th style="padding:12px 14px; background:rgba(255,255,255,0.03); border-bottom:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); color:white;">Common assumption</th>
      <th style="padding:12px 14px; border-bottom:1px solid rgba(24, 61, 109, 0.18); color:white;">Why it breaks</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="padding:12px 14px; border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">A larger attack is harder to stop</td>
      <td style="padding:12px 14px; vertical-align:middle; color:#183d6d;"><a href="https://en.wikipedia.org/wiki/Slowloris_(cyber_attack)" style="color:#183d6d; text-decoration:underline;">Slowloris</a> took down servers with a <a href="https://www.theregister.com/security/2009/06/20/tiny-traffic-dos-attack-spotlights-apache-flaw/1356639" style="color:#183d6d; text-decoration:underline;">single laptop and near-zero bandwidth</a>. The server ran out of connection slots, not capacity.</td>
    </tr>
    <tr>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">More edge capacity solves modern DDoS</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;"><a href="https://blog.qrator.net/en/meris-botnet-climbing-to-the-record_142/" style="color:#183d6d; text-decoration:underline;">M&#x113;ris</a> sent <a href="https://www.bleepingcomputer.com/news/security/new-m-ris-botnet-breaks-ddos-record-with-218-million-rps-attack/" style="color:#183d6d; text-decoration:underline;">21.8 million valid HTTP requests per second</a> from real devices. No amount of capacity tells you which ones to drop.</td>
    </tr>
    <tr>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Broken packets are the main filter signal</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;"><a href="https://www.cisa.gov/news-events/alerts/2023/10/10/http2-rapid-reset-vulnerability-cve-2023-44487" style="color:#183d6d; text-decoration:underline;">HTTP/2 Rapid Reset</a> used only valid <a href="https://blog.qualys.com/vulnerabilities-threat-research/2023/10/10/cve-2023-44487-http-2-rapid-reset-attack" style="color:#183d6d; text-decoration:underline;">RST_STREAM frames</a>. Every packet was protocol-compliant, with nothing malformed to catch.</td>
    </tr>
  </tbody>
</table>
<!--kg-card-end: html-->
<h2 id="what-signals-look-like-at-each-layer">What signals look like at each layer</h2><figure class="kg-card kg-image-card"><img src="https://bunny.net/blog/content/images/2026/08/image3.png" class="kg-image" alt="Why attacks sometimes look like normal traffic" loading="lazy" width="1999" height="548" srcset="https://bunny.net/blog/content/images/size/w600/2026/08/image3.png 600w, https://bunny.net/blog/content/images/size/w1000/2026/08/image3.png 1000w, https://bunny.net/blog/content/images/size/w1600/2026/08/image3.png 1600w, https://bunny.net/blog/content/images/2026/08/image3.png 1999w" sizes="(min-width: 720px) 720px"></figure><p>Knowing which layer an attack targets tells you where in the stack you need to act. The more important question is what signal you have available at that point, because its quality determines whether you can make a fast, cheap drop decision or whether you are stuck doing expensive inspection work before you can act at all.</p>
<!--kg-card-begin: html-->
<table style="border:1px solid rgba(24, 61, 109, 0.18); border-collapse:collapse; width:100%;">
  <thead>
    <tr style="font-weight:700; text-align:left; background:#223c6a; color:white;">
      <th style="padding:12px 14px; background:rgba(255,255,255,0.03); border-bottom:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); color:white;">Layer focus</th>
      <th style="padding:12px 14px; border-bottom:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); color:white;">Primary signal type</th>
      <th style="padding:12px 14px; border-bottom:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); color:white;">Typical measurement</th>
      <th style="padding:12px 14px; border-bottom:1px solid rgba(24, 61, 109, 0.18); color:white;">Filter cost</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="padding:12px 14px; border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">L3 volumetric</td>
      <td style="padding:12px 14px; border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Bandwidth and reflection patterns</td>
      <td style="padding:12px 14px; border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Bits per second</td>
      <td style="padding:12px 14px; vertical-align:middle; color:#183d6d;">Low at provider edge</td>
    </tr>
    <tr>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">L4 protocol</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Connection state exhaustion</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Packets per second</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Low to moderate with stateless techniques</td>
    </tr>
    <tr>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">L7 application</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Request validity and behavior</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Work per request at origin</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">High due to TLS and parsing</td>
    </tr>
  </tbody>
</table>
<!--kg-card-end: html-->
<h3 id="l3-and-l4-measurable-signals">L3 and L4: measurable signals</h3><p>Volumetric and protocol attacks expose signals you can measure before application code runs, before you incur the cost of TLS termination and HTTP parsing. Common examples include sudden bandwidth spikes, spoofed or implausible source addresses, SYN packets that never complete a handshake, and fragmented or repetitive protocol patterns with no real client equivalent. These signals are visible early, which is why provider-scale scrubbing, anycast distribution, and stateless mitigations like SYN cookies remain effective against floods that would overwhelm an origin if they arrived unfiltered.</p><h3 id="l7-interpreted-signals">L7: interpreted signals</h3><p>Application-layer attacks require you to decrypt traffic, parse requests, and compare behavior against a baseline for your specific service. That inspection costs you resources before you can confidently drop anything, while the attacker may have spent very little to generate each request.</p><p>Volumetric floods are measured in bits per second, protocol floods in packets per second, and <strong>application floods in the ratio between request cost at the client and request cost at your origin</strong>. A single search, login, or checkout can cost the server orders of magnitude more than it cost to send, which makes L7 abuse as much an economics problem as a networking problem.</p><h2 id="l34-mitigation-is-largely-a-solved-problem-at-the-edge">L3/4 mitigation is largely a solved problem at the edge</h2><p>&#x201C;Solved&#x201D; does not mean attacks have gotten smaller, and it does not mean the supply of attack infrastructure is shrinking: every newly misconfigured server exposed to the internet becomes another reflector that an attacker can bounce amplified traffic through. What is solved, if you sit behind serious network protection, is the <strong>classification problem</strong>. This traffic looks nothing like a legitimate user session, so you can drop it on cheap signals alone without ever reasoning about intent.</p><p>GitHub&#x2019;s February 2018 memcached incident shows that clarity at extreme scale. The attack peaked at <a href="https://github.blog/news-insights/company-news/ddos-incident-report/" rel="noreferrer"><strong>1.35 Tbps and 126.9 million packets per second,</strong> generated by tens of thousands of reflectors</a>. The service recovered within minutes after GitHub routed traffic through Akamai scrubbing. The signal was obvious: a 15-byte UDP request to an exposed memcached instance could produce responses <a href="https://www.securityweek.com/memcached-abused-ddos-amplification-attacks/" rel="noreferrer">up to 51,000 times the request size</a>. Nothing in normal application traffic resembles that pattern.</p><p>L3/4 attacks fall into two families. Volumetric attacks fill bandwidth through reflection across UDP-based services (DNS, NTP, memcached, ICMP) and are measured in bits per second. Protocol attacks exhaust state in firewalls, load balancers, and servers through SYN floods, ACK floods, and fragment abuse, and are measured in packets per second. Neither family tries to impersonate a legitimate browser traffic.</p>
<!--kg-card-begin: html-->
<table style="border:1px solid rgba(24, 61, 109, 0.18); border-collapse:collapse; width:100%;">
  <thead>
    <tr style="font-weight:700; text-align:left; background:#223c6a; color:white;">
      <th style="padding:12px 14px; background:rgba(255,255,255,0.03); border-bottom:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); color:white;">Attack family</th>
      <th style="padding:12px 14px; border-bottom:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); color:white;">Representative vectors</th>
      <th style="padding:12px 14px; border-bottom:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); color:white;">Primary measurement</th>
      <th style="padding:12px 14px; border-bottom:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); color:white;">What makes it easy to filter</th>
      <th style="padding:12px 14px; border-bottom:1px solid rgba(24, 61, 109, 0.18); color:white;">Common mitigations</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="padding:12px 14px; border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Volumetric</td>
      <td style="padding:12px 14px; border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">UDP reflection, DNS/NTP/memcached amplification, ICMP floods</td>
      <td style="padding:12px 14px; border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Bits per second</td>
      <td style="padding:12px 14px; border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">No legitimate user session produces this traffic shape</td>
      <td style="padding:12px 14px; vertical-align:middle; color:#183d6d;">Anycast, provider scrubbing, raw edge capacity</td>
    </tr>
    <tr>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Protocol</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">SYN floods, ACK floods, fragment abuse</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Packets per second</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Half-open or impossible handshake behavior</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">SYN cookies, stateless drop rules, connection limits</td>
    </tr>
  </tbody>
</table>
<!--kg-card-end: html-->
<p>Spoofed sources, half-open handshakes, and reflection floods have no legitimate twin in production traffic, so dropping them rarely harms real users when rules are scoped correctly.</p><figure class="kg-card kg-image-card"><img src="https://bunny.net/blog/content/images/2026/08/image7.png" class="kg-image" alt="Why attacks sometimes look like normal traffic" loading="lazy" width="1999" height="914" srcset="https://bunny.net/blog/content/images/size/w600/2026/08/image7.png 600w, https://bunny.net/blog/content/images/size/w1000/2026/08/image7.png 1000w, https://bunny.net/blog/content/images/size/w1600/2026/08/image7.png 1600w, https://bunny.net/blog/content/images/2026/08/image7.png 1999w" sizes="(min-width: 720px) 720px"></figure><p>If you run bare metal directly on the public internet without scrubbing in front of it, even well-understood attack classes can overwhelm you because the classification and absorption work has nowhere else to happen.</p><p>L7 is a different problem entirely.</p><h2 id="l7-mitigation-is-harder-in-production">L7 mitigation is harder in production</h2><p>A single HTTP request can appear completely normal while still being part of an attack campaign. The same GET /products/12841 from a distributed botnet and one from a customer during a promotion can be identical on the wire. Packet signatures are useless here. <strong>Behavioral context and baselines</strong> are what separate the two.</p><p>Several structural factors make that harder in practice:</p><ul><li><strong>Inspection cost</strong>: TLS termination and HTTP parsing consume defender CPU before a confident block decision is possible, while attackers generate requests cheaply at scale.</li><li><strong>Moving baseline</strong>: Real audiences are bursty. Launches and viral links can reproduce the shape of an attack without any malicious intent.</li><li><strong>Uneven endpoint cost</strong>: Applying the same rate limit to a static page and to a login or checkout flow can block humans on cheap routes while leaving expensive routes exposed.</li><li><strong>Realistic bots</strong>: Headless browsers, residential proxies, and realistic TLS client behavior mean simple header checks are no longer a durable strategy.</li></ul><p>Let&#x2019;s look at four attacks that worked in different ways but bypassed the cheap filters for the same reason, which is that none of them looked like an attack at the layer those filters watch.</p><h3 id="1-http2-continuation-flood-and-the-invisible-request">1. HTTP/2 CONTINUATION Flood and the invisible request</h3><p>HTTP/2 lets a client split request headers across several frames: an opening HEADERS frame followed by one or more CONTINUATION frames, with an END_HEADERS flag marking the end of the block. The CONTINUATION Flood, disclosed in April 2024 by researcher Bartek Nowotarski, abuses this by sending an unbounded stream of CONTINUATION frames and never setting END_HEADERS. The server keeps reading and buffering the header block, waiting for an end that never comes, and <a href="https://snyk.io/blog/exploiting-http-2-continuation-frames-dos-attacks/" rel="noreferrer">a single connection can drive memory to an out-of-memory crash</a>.</p><figure class="kg-card kg-image-card"><img src="https://bunny.net/blog/content/images/2026/08/image2.png" class="kg-image" alt="Why attacks sometimes look like normal traffic" loading="lazy" width="1999" height="780" srcset="https://bunny.net/blog/content/images/size/w600/2026/08/image2.png 600w, https://bunny.net/blog/content/images/size/w1000/2026/08/image2.png 1000w, https://bunny.net/blog/content/images/size/w1600/2026/08/image2.png 1600w, https://bunny.net/blog/content/images/2026/08/image2.png 1999w" sizes="(min-width: 720px) 720px"></figure><p>Because the request never completes, most servers never write it to the access log, so the server can go down while your dashboards and logs see nothing. There is no slow client to flag, no malformed frame to match, and no finished request to rate-limit. The fix is to cap the number of CONTINUATION frames per stream and the total header size, then close connections that exceed those limits.</p><h3 id="2-slowloris-and-low-bandwidth-exhaustion">2. Slowloris and low-bandwidth exhaustion</h3><p>Robert Hansen (RSnake) released Slowloris in June 2009 to show that application availability could be destroyed without a volumetric flood. The technique uses one machine to open many connections, send partial HTTP headers, and drip additional header lines slowly enough to prevent timeouts while never completing a request. The server holds each connection open while waiting for completion. <strong>Once the pool is full, legitimate clients cannot establish new connections.</strong></p><p>The attack uses almost no bandwidth, so volume-based defenses see nothing alarming. Each connection looks like a slow mobile client on a poor network rather than a flood. Effective mitigation is behavioral: connection and header timeouts, per-IP connection limits, and reverse proxies that buffer and terminate incomplete requests before they tie up application threads.</p><figure class="kg-card kg-image-card"><img src="https://bunny.net/blog/content/images/2026/08/image6.png" class="kg-image" alt="Why attacks sometimes look like normal traffic" loading="lazy" width="1999" height="885" srcset="https://bunny.net/blog/content/images/size/w600/2026/08/image6.png 600w, https://bunny.net/blog/content/images/size/w1000/2026/08/image6.png 1000w, https://bunny.net/blog/content/images/size/w1600/2026/08/image6.png 1600w, https://bunny.net/blog/content/images/2026/08/image6.png 1999w" sizes="(min-width: 720px) 720px"></figure><p>Slowloris separates attack impact from attack size. When your dashboards only watch bandwidth, this class of abuse can look healthy right up until the connection pool is gone.</p><h3 id="3-m%C4%93ris-and-valid-requests-at-record-scale">3. M&#x113;ris and valid requests at record scale</h3><p>In September 2021, the M&#x113;ris botnet (named after the Latvian word for plague) struck Yandex at <strong>21.8 million requests per second</strong>, a record at the time. The ramp was gradual rather than a sudden cliff, and each increase could be mistaken for organic growth until source and behavior context made the pattern clear.</p><p>M&#x113;ris was built from compromised MikroTik routers exploited through CVE-2018-14847, a vulnerability patched in 2018 that remained present on many devices in the wild. Research from Yandex and Qrator Labs <a href="https://therecord.media/meet-meris-the-new-250000-strong-ddos-botnet-terrorizing-the-internet" rel="noreferrer">estimated the full compromised population at around 250,000 hosts</a>, while individual attacks used only a fraction of those hosts at a time. The attack traffic consisted of <strong>complete valid HTTP requests</strong> sent at enormous volume. Bots used HTTP pipelining to send multiple requests down a single connection without waiting for responses, and SOCKS proxies obscured origin behavior. Because the sources were real network devices on residential and business networks, each host looked like an ordinary client, and each request looked like legitimate HTTP.</p><figure class="kg-card kg-image-card"><img src="https://bunny.net/blog/content/images/2026/08/image8.png" class="kg-image" alt="Why attacks sometimes look like normal traffic" loading="lazy" width="1999" height="669" srcset="https://bunny.net/blog/content/images/size/w600/2026/08/image8.png 600w, https://bunny.net/blog/content/images/size/w1000/2026/08/image8.png 1000w, https://bunny.net/blog/content/images/size/w1600/2026/08/image8.png 1600w, https://bunny.net/blog/content/images/2026/08/image8.png 1999w" sizes="(min-width: 720px) 720px"></figure><p>When every request is syntactically valid, you rely on rate control, fingerprinting, reputation, and behavioral baselines. You also need a clear picture of what normal growth looks like for your service because a botnet spike and a successful launch can look similar in raw request rate.</p><h3 id="4-http2-rapid-reset-and-protocol-valid-abuse">4. HTTP/2 Rapid Reset and protocol-valid abuse</h3><p>HTTP/2 Rapid Reset (CVE-2023-44487) was disclosed on October 10, 2023, through coordinated publication by Google, Cloudflare, and AWS after exploitation in the wild between August and October of that year. Google reported a peak of <strong>398 million requests per second</strong>: roughly 7.5 times the previous record. Cloudflare observed about <a href="https://www.bleepingcomputer.com/news/security/new-http-2-rapid-reset-zero-day-attack-breaks-ddos-records/" rel="noreferrer">201 million requests per second, generated by a botnet of approximately 20,000 machines</a>.</p><figure class="kg-card kg-image-card"><img src="https://bunny.net/blog/content/images/2026/08/image5.png" class="kg-image" alt="Why attacks sometimes look like normal traffic" loading="lazy" width="1999" height="718" srcset="https://bunny.net/blog/content/images/size/w600/2026/08/image5.png 600w, https://bunny.net/blog/content/images/size/w1000/2026/08/image5.png 1000w, https://bunny.net/blog/content/images/size/w1600/2026/08/image5.png 1600w, https://bunny.net/blog/content/images/2026/08/image5.png 1999w" sizes="(min-width: 720px) 720px"></figure><p>HTTP/2 multiplexes many streams over one TCP connection and lets clients cancel individual streams with RST_STREAM frames: a legitimate feature real browsers use, for example, when aborting image loads after scrolling. Attackers repeatedly opened a stream, canceled it immediately, and repeated the cycle. <strong>The attacker incurred very little cost while the server paid the setup and teardown cost for each cycle.</strong> Every frame was valid HTTP/2, so there was no malformed packet to match and no simple signature beyond resource accounting behavior.</p><p>Concurrent stream limits alone do not solve the problem because abusive clients can churn cancellations faster than servers reclaim state in some architectures. Mitigation focused on protocol accounting changes: reset counting per connection, tighter stream limits, and earlier connection closure for abusive patterns. The fix was <strong>resource accounting</strong>: counting resets per connection, tightening stream limits, and closing connections earlier when abuse patterns appear.</p><p>Taken together, all four attacks follow the same playbook.</p><h2 id="the-shared-pattern-across-all-four-attacks">The shared pattern across all four attacks</h2><p>These attacks differ in speed, bandwidth profile, and protocol details, but they all become hard to stop at the same point: <strong>when the cheapest filters cannot tell them apart from normal traffic, or cannot see them at all</strong>.</p>
<!--kg-card-begin: html-->
<table style="border:1px solid rgba(24, 61, 109, 0.18); border-collapse:collapse; width:100%;">
  <thead>
    <tr style="font-weight:700; text-align:left; background:#223c6a; color:white;">
      <th style="padding:12px 14px; background:rgba(255,255,255,0.03); border-bottom:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); color:white;">Dimension</th>
      <th style="padding:12px 14px; border-bottom:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); color:white;">Slowloris (2009)</th>
      <th style="padding:12px 14px; border-bottom:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); color:white;">M&#x113;ris (2021)</th>
      <th style="padding:12px 14px; border-bottom:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); color:white;">Rapid Reset (2023)</th>
      <th style="padding:12px 14px; border-bottom:1px solid rgba(24, 61, 109, 0.18); color:white;">CONTINUATION Flood (2024)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="padding:12px 14px; border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Speed profile</td>
      <td style="padding:12px 14px; border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Very slow</td>
      <td style="padding:12px 14px; border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Fast</td>
      <td style="padding:12px 14px; border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Very fast</td>
      <td style="padding:12px 14px; vertical-align:middle; color:#183d6d;">Fast</td>
    </tr>
    <tr>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Bandwidth footprint</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Almost none</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">High</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">High but cheap to generate</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Almost none</td>
    </tr>
    <tr>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Unit of abuse</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Valid slow request</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Valid full request</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Valid canceled stream</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Endless header frames</td>
    </tr>
    <tr>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Why it evades cheap filters</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">No volume anomaly</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Resembles organic traffic</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">No malformed frame to match</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Never completes, never logged</td>
    </tr>
    <tr>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Durable mitigation theme</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Connection behavior controls</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Reputation, rate limits, fingerprints</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Protocol accounting and limits</td>
      <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">Header size and frame-count limits</td>
    </tr>
  </tbody>
</table>
<!--kg-card-end: html-->
<figure class="kg-card kg-image-card"><img src="https://bunny.net/blog/content/images/2026/08/image1.png" class="kg-image" alt="Why attacks sometimes look like normal traffic" loading="lazy" width="1999" height="836" srcset="https://bunny.net/blog/content/images/size/w600/2026/08/image1.png 600w, https://bunny.net/blog/content/images/size/w1000/2026/08/image1.png 1000w, https://bunny.net/blog/content/images/size/w1600/2026/08/image1.png 1600w, https://bunny.net/blog/content/images/2026/08/image1.png 1999w" sizes="(min-width: 720px) 720px"></figure><p>This pattern explains why raw capacity stops helping once attacks resemble normal traffic. Peak volumes may keep climbing, but defense now depends on behavior profiling, rate control, and protocol limits rather than more bandwidth.</p><h2 id="practical-strategies-for-l7-defense">Practical strategies for L7 defense</h2><p>No single WAF rule or rate limit covers every L7 attack pattern. Most teams stack several controls and accept that each one can misclassify some traffic. The goal is to block abuse efficiently without breaking legitimate spikes.</p><figure class="kg-card kg-image-card"><img src="https://bunny.net/blog/content/images/2026/08/image9.png" class="kg-image" alt="Why attacks sometimes look like normal traffic" loading="lazy" width="1999" height="1121" srcset="https://bunny.net/blog/content/images/size/w600/2026/08/image9.png 600w, https://bunny.net/blog/content/images/size/w1000/2026/08/image9.png 1000w, https://bunny.net/blog/content/images/size/w1600/2026/08/image9.png 1600w, https://bunny.net/blog/content/images/2026/08/image9.png 1999w" sizes="(min-width: 720px) 720px"></figure><p>Some common mitigations that handle most of the work:</p><ul><li><strong>Tier your rate limits:</strong> Set a blanket per-IP limit on a rolling window, then tighten it on compute-heavy routes such as login, search, checkout, and signup.</li><li><strong>Order rules cheapest first:</strong> Run IP reputation, geo constraints, and coarse heuristics before bot inspection and application logic, so most unwanted traffic is dropped before it reaches a costly control. <a href="https://docs.bunny.net/shield/bot-detection">Bunny Shield&#x2019;s bot detection</a> layers checks in this order: cheap request-integrity, IP, and ASN reputation checks run first, and you can escalate to header-anomaly and full fingerprint validation by moving from the Low to the High sensitivity profile or by toggling individual checks in Custom mode.</li><li><strong>Add an adaptive baseline:</strong> Track what your normal traffic looks like over time and filter what does not fit. For the challenge layer, <a href="https://docs.bunny.net/shield/ddos">Bunny Shield&#x2019;s DDoS protection</a> provides a JavaScript proof-of-work challenge you can scope to specific endpoints or run in Always-On Mode. Challenges raise attacker cost but add user friction and become less effective as automation improves. The open-source Anubis proxy gates bots behind a SHA-256 puzzle and is used by groups like UNESCO and GNOME, yet researchers note <a href="https://www.theregister.com/software/2025/07/09/anubis-fighting-off-the-hordes-of-llm-bot-crawlers/546817" rel="noreferrer">the compute cost is negligible for a well-funded scraper</a>, so it works best as one added layer.</li><li><strong>Harden the application and protocol:</strong> Set request, header, and absolute connection timeouts, enforce minimum ingress data rates to defeat slow-send attacks, and cap HTTP/2 streams with reset counting. See <a href="https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html" rel="noreferrer">OWASP&#x2019;s Denial of Service Cheat Sheet for the full list</a>. Many of these gaps are invisible to firewall signatures.</li></ul><h2 id="where-this-is-heading">Where this is heading</h2><p>How difficult an attack is to stop depends on how different it looks from your legitimate users. L3/4 remains tractable at the edge because benign production traffic rarely shares the shape of floods and reflection abuse. L7 remains difficult because benign and malicious traffic can share the same protocol, the same endpoints, and the same request structure.</p><p>As bots continue to improve at impersonating real clients through residential IP diversity, realistic TLS stacks, and more complete page interaction patterns, the right question shifts from how many requests arrived to who is sending them and whether that pattern fits your service.</p><p>No single layer solves the whole problem, so a complete defense needs both. Network-level DDoS protection absorbs and drops obvious floods at the edge, while application-aware controls inspect traffic that looks like real users. As you design your own setup, the rule that holds up over time is to filter on clear signals as early and as cheaply as you can, then fall back on behavior and baselines only for the requests that look fine on their own.</p>]]></content:encoded></item><item><title><![CDATA[Build your own caching rules with pre-cache execution and Cache API in Edge Scripting]]></title><description><![CDATA[Edge Scripting can now run before or after the cache, giving your code control over every single request. And with the new Cache API, your scripts can read, write, and delete entries in a separate cache at the edge.]]></description><link>https://bunny.net/blog/build-your-own-caching-rules-with-pre-cache-execution-and-cache-api-in-edge-scripting/</link><guid isPermaLink="false">6a63639b160dc403fbfcf5d3</guid><category><![CDATA[News]]></category><dc:creator><![CDATA[Marek Nalikowski]]></dc:creator><pubDate>Tue, 28 Jul 2026 08:43:00 GMT</pubDate><media:content url="https://bunny.net/blog/content/images/2026/07/Edge-scripting-pre-cache.png" medium="image"/><content:encoded><![CDATA[<img src="https://bunny.net/blog/content/images/2026/07/Edge-scripting-pre-cache.png" alt="Build your own caching rules with pre-cache execution and Cache API in Edge Scripting"><p>Until now, <a href="https://docs.bunny.net/scripting">Edge Scripting</a> has had an opinionated default: your code runs after the CDN cache. That&apos;s great for efficiency because fully cached traffic never executes your script. But it also means that the moment a response gets cached, your code is out of the loop.</p><p>Today, that&apos;s becoming a choice instead of a rule. Edge Scripting can now also run before the cache, giving your code control over every single request. And with the new Cache API, your scripts can read, write, and delete entries in a separate cache at the edge.</p><p>Let&apos;s hop into the details.</p><h2 id="why-run-before-the-cache">Why run before the cache</h2><p>Running after the cache is a feature, not a bug. When a response is cached, the CDN serves it directly and your script never runs. Cached traffic adds zero script execution, which keeps costs low and latency predictable. For delivery-heavy workloads, that&apos;s exactly the behavior you want.</p><p>The tradeoff shows up when your workload is dynamic. As soon as a response is cached, you lose per-request control. No auth checks, no personalization, no routing logic; nothing runs until the cache entry expires. The usual workaround is to disable caching for dynamic routes, which solves the problem by throwing away the benefit.</p><p>That means you&apos;ve had to choose between caching and control. Now you can have both.</p><h2 id="how-pre-cache-execution-works">How pre-cache execution works</h2><p>Edge Scripts get two new hooks that run on the client side of the cache:</p>
<!--kg-card-begin: html-->
<ul>
  <li><strong>onClientRequest</strong> fires on every incoming request, before the cache lookup. You can modify the request or return a response directly and short-circuit everything behind it.</li>
  <li><strong>onClientResponse</strong> fires just before a cache or origin response is sent back to the client. If <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">onClientRequest</code> returns a response directly, the request is short-circuited there.</li>
</ul>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  Together with the existing <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">onOriginRequest</code> and <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">onOriginResponse</code> hooks, the request lifecycle now looks like this:
</p>
<!--kg-card-end: html-->
<figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://bunny.net/blog/content/images/2026/07/cache-lifecycle-diagram.png" class="kg-image" alt="Build your own caching rules with pre-cache execution and Cache API in Edge Scripting" loading="lazy" width="2000" height="1097" srcset="https://bunny.net/blog/content/images/size/w600/2026/07/cache-lifecycle-diagram.png 600w, https://bunny.net/blog/content/images/size/w1000/2026/07/cache-lifecycle-diagram.png 1000w, https://bunny.net/blog/content/images/size/w1600/2026/07/cache-lifecycle-diagram.png 1600w, https://bunny.net/blog/content/images/2026/07/cache-lifecycle-diagram.png 2000w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Your code runs on the client side of the cache (onClientRequest, onClientResponse) and on the origin side (onOriginRequest, onOriginResponse). The CDN cache is served automatically, while your script can also read and write a separate, script-controlled cache.</span></figcaption></figure><p>On a hit, the CDN cache answers and your client-side hooks run around it. On a miss, the request continues to the origin hooks and back, writing to the cache on the way if it&apos;s cacheable. Either way, your code sits before the cache lookup and just before the response goes out, so you get control around the cache without bypassing it.</p><h2 id="meet-the-cache-api">Meet the Cache API</h2><p>Running before the cache is one half of the story. The other half is a cache your script controls directly, separate from the CDN cache that serves requests automatically. That&apos;s where the Cache API comes in.</p>
<!--kg-card-begin: html-->
<p>
  Modeled after the browser&#x2019;s <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">Cache</code> interface, it lets scripts match, put, and delete request/response pairs. Entry lifetimes follow the <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">Cache-Control</code> headers you set on the responses you write, and expired entries are purged automatically.
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  Here&#x2019;s a pattern for taking ownership of a cache entry&#x2019;s lifecycle: serve a cached value on <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">GET</code>, refresh it on <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">POST</code>, and purge the exact entry on <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">DELETE</code>. The key detail is that all three handlers build the same URL-based key, so reads, writes, and deletes all target the same entry.
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#569CD6;">import</span> * <span style="color:#569CD6;">as</span> BunnySDK <span style="color:#569CD6;">from</span> <span style="color:#BDE052;">&quot;@bunny.net/edgescript-sdk&quot;</span>;

<span style="color:#8A8A8A;">// GET serves the cached entry, POST refreshes it, DELETE purges it.</span>
<span style="color:#8A8A8A;">// (Error handling omitted: match, put, and delete can all throw.)</span>
BunnySDK.net.http.serve(<span style="color:#569CD6;">async</span> (request: <span style="color:#BDE052;">Request</span>): <span style="color:#BDE052;">Promise&lt;Response&gt;</span> =&gt; {
  <span style="color:#569CD6;">const</span> cache = <span style="color:#569CD6;">await</span> caches.open(<span style="color:#BDE052;">&quot;demo:v1&quot;</span>);
  <span style="color:#569CD6;">const</span> key = <span style="color:#569CD6;">new</span> URL(request.url).toString(); <span style="color:#8A8A8A;">// one key for all three</span>

  <span style="color:#569CD6;">if</span> (request.method === <span style="color:#BDE052;">&quot;POST&quot;</span>) {
    <span style="color:#569CD6;">const</span> fresh = Response.json(
      { refreshedAt: <span style="color:#569CD6;">new</span> Date().toISOString() },
      { headers: { <span style="color:#D1949E;">&quot;Cache-Control&quot;</span>: <span style="color:#BDE052;">&quot;max-age=60&quot;</span> } },
    );
    <span style="color:#569CD6;">await</span> cache.put(key, fresh.clone());
    <span style="color:#569CD6;">return</span> <span style="color:#569CD6;">new</span> Response(<span style="color:#BDE052;">&quot;Refreshed.&quot;</span>, { headers: { <span style="color:#D1949E;">&quot;Cache-Control&quot;</span>: <span style="color:#BDE052;">&quot;no-cache&quot;</span> } });
  }

  <span style="color:#569CD6;">if</span> (request.method === <span style="color:#BDE052;">&quot;DELETE&quot;</span>) {
    <span style="color:#569CD6;">const</span> deleted = <span style="color:#569CD6;">await</span> cache.delete(key);
    <span style="color:#569CD6;">return</span> <span style="color:#569CD6;">new</span> Response(deleted ? <span style="color:#BDE052;">&quot;Purged.&quot;</span> : <span style="color:#BDE052;">&quot;Nothing to purge.&quot;</span>, {
      headers: { <span style="color:#D1949E;">&quot;Cache-Control&quot;</span>: <span style="color:#BDE052;">&quot;no-cache&quot;</span> },
    });
  }

  <span style="color:#569CD6;">const</span> cached = <span style="color:#569CD6;">await</span> cache.match(key);
  <span style="color:#569CD6;">return</span> cached ?? <span style="color:#569CD6;">new</span> Response(<span style="color:#BDE052;">&quot;No cached value yet.&quot;</span>, {
    status: <span style="color:#D1949E;">404</span>,
    headers: { <span style="color:#D1949E;">&quot;Cache-Control&quot;</span>: <span style="color:#BDE052;">&quot;no-cache&quot;</span> },
  });
});
</pre>
<br>
<!--kg-card-end: html-->
<p>That same pattern scales up to tenant-aware keys, microcaching, manual refresh hooks, and exact-entry purges. It&apos;s a script-controlled cache at the edge, separate from the automatic CDN cache: you decide how entries are created, found, and removed.</p><p>A few things to know before you design around it:</p>
<!--kg-card-begin: html-->
<ul>
  <li><strong>Cache contents are regional.</strong> Entries live in the region where they were created and don&apos;t replicate globally. A page cached in Frankfurt gets cached again in Singapore on the first request there.</li>
  <li><strong>Cache keys are yours to design.</strong> An entry&apos;s key is exactly the URL or Request you pass, nothing more. There&apos;s no automatic normalization, so <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">/page</code> and <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">/page?utm_source=x</code> are two different entries unless your code makes them one. We recommend building keys from the current request URL. That discipline pays off when it&#x2019;s time to delete an entry: you can only purge it if you can rebuild its exact key.</li>
  <li><strong>Pull Zone domains share cache instances.</strong> Cache instances are shared across all domains associated with your Pull Zone, but they need to be accessed through the incoming request&apos;s hostname. For multi-domain Pull Zones, build keys from the current request host or request URL.</li>
  <li><strong>Limits:</strong> <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">match</code>, <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">put</code>, and <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">delete</code> are the supported operations on Cache instances, with a 100 MB limit per cache file.</li>
</ul>

<!--kg-card-end: html-->
<p>Pre-cache execution and the Cache API are two separate features, but they&apos;re better together. Running before the cache gives you control on every request, and the Cache API is how you put caching back in on your terms.</p><p>This unlocks two major use cases:</p><h2 id="multi-tenant-delivery-on-your-caching-rules">Multi-tenant delivery on your caching rules</h2><p>When every hostname, path, or account maps to a different tenant, one-size-fits-all cache rules stop being enough. With scripts in front of the cache, your delivery logic can:</p>
<!--kg-card-begin: html-->
<ul>
  <li><strong>Key cache entries per tenant</strong>, using tenant-aware prefixes inside versioned named caches, with TTLs controlled by the <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">Cache-Control</code> headers on the responses you write.</li>
  <li><strong>Invalidate exact entries surgically</strong>, as long as your application can reconstruct the keys it wrote. This is exact-key deletion, not wildcard purge or tenant-wide key listing.</li>
  <li><strong>Decorate cache or origin responses per tenant</strong> in <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">onClientResponse</code>, adding headers or security policies without waking up the origin.</li>
  <li><strong>Build dynamic caching patterns</strong> the CDN doesn&apos;t offer out of the box, like microcaching rendered pages or using explicit <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">POST</code> / <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">DELETE</code> handlers to refresh and purge known cache entries.</li>
</ul>
<!--kg-card-end: html-->
<p>If you run a platform where your customers&apos; content flows through your zones, this lets you turn edge caching from a fixed behavior into part of your product logic.</p><h2 id="serverless-crud-apps-at-the-edge">Serverless CRUD apps at the edge</h2><p>Until now, a dynamic multi-user app didn&apos;t really fit the after-cache model. Sessions, per-user responses, and cache decisions all need code on every request, so you&apos;d end up disabling caching and losing the main benefit of running at the edge.</p><p>With pre-cache execution, the pieces fall into place:</p>
<!--kg-card-begin: html-->
<ul>
  <li><strong>Auth and sessions</strong> run in <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">onClientRequest</code>, on every request</li>
  <li><strong>You decide what&apos;s cacheable</strong>, using the Cache API to cache rendered pages or API responses for seconds or minutes, and serving everything else dynamically</li>
  <li><strong>Compute stays request-based</strong>, so a side project that gets no traffic costs nothing in compute, and a production app scales with traffic</li>
  <li><strong>Pair Edge Scripting with <a href="https://bunny.net/blog/meet-bunny-database-the-sql-service-that-just-works/">Bunny Database</a> for state</strong>, so you can build full-stack serverless apps, with read replication to serve reads close to your users</li>
</ul>
<!--kg-card-end: html-->
<p>And since both Edge Scripting and Bunny Database are supported by the <a href="https://docs.bunny.net/cli">bunny.net CLI</a>, you (and your agent) can go from local to prod without leaving the terminal.</p><h2 id="you-choose-where-your-code-runs">You choose where your code runs</h2><p>Post-cache execution stays the default, and it&apos;s still the most cost-effective way to run delivery logic: when cached traffic doesn&apos;t need script logic, the CDN can serve it directly without invoking your code. Pre-cache execution is the opt-in mode for the routes and Pull Zones where you need per-request control.</p><p>Other platforms typically put compute in front of the cache by default, so even cache hits can pass through your code path. With bunny.net you can now choose to keep compute out of the hot path when the cache is enough, or move it in front of the cache when your application needs control.</p><h2 id="getting-started">Getting started</h2><p>Both pre-cache execution and the Cache API are now available in <a href="https://docs.bunny.net/product-release-stages">public preview</a>.</p><p>The Cache API works in any Edge Script today. There&apos;s nothing to enable. Pre-cache execution is a toggle in the dashboard:</p><ul><li><strong>Middleware scripts:</strong> in your Pull Zone, under <strong>General &#x2192; Origin &#x2192; Run script before cache</strong>.</li><li><strong>Standalone scripts:</strong> the same toggle in the Pull Zone, or enable it directly in the script&apos;s settings.</li></ul><p>For more details, check out the docs:</p><ul><li><a href="https://docs.bunny.net/scripting/before-cache">Pre-cache execution</a></li><li><a href="https://docs.bunny.net/scripting/cache-api">Cache API reference</a></li></ul><p>Happy building and let us know what you think on <a href="https://discord.com/invite/bunnynet">Discord</a>!</p>]]></content:encoded></item><item><title><![CDATA[HopStart cohort #3: the next startups building on bunny.net]]></title><description><![CDATA[HopStart is our startup program with a straightforward goal: let early-stage founders get on with building, without infrastructure eating into their time and budget. Two rounds in, we're back with a third cohort.]]></description><link>https://bunny.net/blog/hopstart-cohort-3-the-next-startups-building-on-bunny-net/</link><guid isPermaLink="false">6a61b4b7160dc403fbfcf5c1</guid><category><![CDATA[News]]></category><dc:creator><![CDATA[Marek Nalikowski]]></dc:creator><pubDate>Thu, 23 Jul 2026 06:35:42 GMT</pubDate><media:content url="https://bunny.net/blog/content/images/2026/07/Hop-Start-Cohort.png" medium="image"/><content:encoded><![CDATA[<img src="https://bunny.net/blog/content/images/2026/07/Hop-Start-Cohort.png" alt="HopStart cohort #3: the next startups building on bunny.net"><p>HopStart is our startup program with a straightforward goal: let early-stage founders get on with building, without infrastructure eating into their time and budget. Two rounds in, we&apos;re back with a third cohort.</p><p>As always, each cohort has three winners. Each team receives up to <strong>$50,000 USD in credits</strong> to spend on bunny.net, plus hands-on time with our team and an early look at features before we release them. Choosing only three is never easy. What ultimately tips the scale for us is whether a team is solving a real problem for its users, and whether bunny.net is a genuine fit for how they build.</p><p>Here are the three startups joining HopStart cohort #3.</p><h2 id="1st-place-screenity">1st place: Screenity</h2><p><strong>Founder:</strong> Alyssa X</p><p><strong>Year founded:</strong> 2025</p><p><strong>URL:</strong> <a href="https://www.screenity.io/">https://www.screenity.io/</a></p><p><strong>Credits received:</strong> $50,000 for one year</p><p><strong>In the founder&apos;s words:</strong> &quot;Screenity is the European alternative to Loom that goes well beyond recording a clip and sending a link. It&apos;s a screen recorder and video editor all in one, working almost like a deck. It&apos;s privacy-friendly all the way down, and is hosted entirely in Europe (unheard of in this space!), all built independently by one person.&quot;</p><h2 id="2nd-place-ciphera">2nd place: Ciphera</h2><p><strong>Founder:</strong> Usman Baig</p><p><strong>Year founded:</strong> 2024</p><p><strong>URL:</strong> <a href="https://www.ciphera.net/">https://www.ciphera.net/</a></p><p><strong>Credits received:</strong> $25,000 for one year</p><p><strong>In the founder&apos;s words:</strong> &quot;Ciphera is a privacy-first analytics and identity platform built for website owners who refuse to compromise between actionable data and user privacy. Our flagship product, Pulse, is a cookie-free web analytics tool that gives site owners the metrics they need &#x2014; pageviews, referrers, geolocation, and device breakdowns &#x2014; without collecting personal data or requiring consent banners. The impact is straightforward: we&apos;re making it economically viable for small and mid-sized websites to stop feeding the ad-tech surveillance pipeline.&quot;</p><h2 id="3rd-place-orkestr">3rd place: orkestr</h2><p><strong>Founder:</strong> Stefan Iancu</p><p><strong>Year founded:</strong> 2026</p><p><strong>URL:</strong> <a href="https://orkestr.eu/">https://orkestr.eu/</a></p><p><strong>Credits received:</strong> $10,000 for one year</p><p><strong>In the founder&apos;s words:</strong> &quot;orkestr is a platform-as-a-service that removes DevOps overhead for developers, freelancers, and small teams. Push to Git, and we handle builds, deploys, databases, SSL, and scaling. No Kubernetes, no infrastructure engineer required. Teams that need real data sovereignty are forced to choose between developer experience and compliance, and usually end up self-hosting, which means hiring DevOps talent they cannot afford. We close that gap: orkestr runs entirely on EU infrastructure with a fully EU sub-processor chain, so customers get Heroku-grade simplicity without legal exposure to non-EU jurisdiction.&quot;</p><h2 id="applying-for-cohort-4">Applying for cohort #4</h2><p>Applications for the next cohort are open. If you&apos;re building something you&apos;re proud of and can see bunny.net fitting into it, submit your application using <a href="https://bunny.net/HopStart/#HopStart-Form">the short form</a> before <strong>August 31</strong>.</p><p>If you applied before and didn&apos;t make it, it&apos;s worth trying again. Every application round is different, and yours may be a stronger fit this time.</p><p>We&apos;ll announce cohort #4 in October.</p><p>Looking forward to seeing what you&#x2019;re building!</p>]]></content:encoded></item><item><title><![CDATA[New Bunny Shield protections for critical WordPress vulnerabilities]]></title><description><![CDATA[New managed WAF rules in Bunny Shield block exploit attempts targeting critical WordPress CVEs before they reach your origin.]]></description><link>https://bunny.net/blog/new-bunny-shield-protections-for-critical-wordpress-vulnerabilities/</link><guid isPermaLink="false">6a5f7738160dc403fbfcf5ac</guid><category><![CDATA[Security]]></category><dc:creator><![CDATA[Joe Connolly]]></dc:creator><pubDate>Tue, 21 Jul 2026 14:29:06 GMT</pubDate><media:content url="https://bunny.net/blog/content/images/2026/07/image--37-.png" medium="image"/><content:encoded><![CDATA[<img src="https://bunny.net/blog/content/images/2026/07/image--37-.png" alt="New Bunny Shield protections for critical WordPress vulnerabilities"><p>Two recently disclosed vulnerabilities have drawn significant attention across the security community, including a critical unauthenticated remote code execution (RCE) vulnerability and a related SQL injection vulnerability.</p><p>To help protect customers while they update their WordPress installations, Bunny Shield now includes new WAF protections for both CVEs, blocking known exploitation attempts before they reach your origin.</p><p>These protections help reduce your exposure, but they are not a substitute for patching. WordPress has released security updates for the affected versions, and updating your installation should remain your highest priority.</p><h2 id="affected-versions">Affected versions</h2><p>WordPress has released fixes in the following versions:</p><ul><li><strong>7.0.2</strong></li><li><strong>6.9.5</strong></li><li><strong>6.8.6</strong></li><li><strong>7.1 Beta 2</strong></li></ul><p>Versions earlier than <strong>6.8</strong> are not affected.</p><p>The WordPress project is treating these vulnerabilities with the highest priority and is automatically deploying updates to supported installations wherever possible. Even so, it&apos;s worth confirming that your sites have successfully updated and are running a patched release.</p><h2 id="understanding-the-vulnerabilities">Understanding the vulnerabilities</h2><p>The advisory includes two related vulnerabilities affecting different parts of WordPress.</p><h3 id="cve-2026-60137sql-injection">CVE-2026-60137 - SQL Injection</h3><p>This vulnerability affects <strong>WordPress 6.8 and later</strong> and allows specially crafted input to manipulate database queries.</p><p><strong>Severity:</strong> High</p><h3 id="cve-2026-63030unauthenticated-remote-code-execution">CVE-2026-63030 - Unauthenticated Remote Code Execution</h3><p>This vulnerability affects <strong>WordPress 6.9 and later</strong>. Under specific conditions, if a persistent object cache is not enabled, an attacker can execute arbitrary code through the WordPress REST API batch endpoint without authentication.</p><p>No login or user interaction is required to exploit this vulnerability.</p><p><strong>Severity:</strong> Critical</p><p>Because the RCE vulnerability builds on the SQL injection vulnerability, the available fixes differ slightly between versions:</p><ul><li><strong>6.8.6</strong> fixes the SQL injection vulnerability</li><li><strong>6.9.5</strong>, <strong>7.0.2</strong>, and <strong>7.1 Beta 2</strong> address both vulnerabilities</li></ul><h2 id="how-bunny-shield-protects-your-site">How Bunny Shield protects your site</h2><p>Bunny Shield now includes dedicated managed WAF rules for both CVEs.</p><p>While our existing WAF engine already detected many exploit attempts targeting these CVEs, we&apos;ve added dedicated managed rules to improve detection accuracy and provide more precise protection against the disclosed attack paths.</p><p>These rules inspect incoming requests before they reach your WordPress installation, identifying and blocking known exploit attempts targeting both vulnerabilities.</p><p>If your website is already protected by Bunny Shield, no additional configuration is required.</p><h2 id="protection-while-you-update">Protection while you update</h2><p>Security updates are always the permanent fix, but deploying them isn&apos;t always instantaneous. Large WordPress fleets, maintenance windows, compatibility testing, and operational requirements can all delay updates, leaving a window of opportunity for attackers.</p><p>Bunny Shield helps close that gap by identifying and blocking malicious requests at the edge before they ever reach your application. This provides an additional layer of protection against publicly known exploit techniques while you complete your update rollout.</p><p>This approach, often referred to as virtual patching, reduces the risk of exploitation during the period between a vulnerability being disclosed and every affected system being updated. It complements software updates, but never replaces them.</p><h2 id="what-you-should-do">What you should do</h2><p>If you manage WordPress sites, we recommend taking the following steps:</p><ul><li>Update to a patched version of WordPress as soon as possible.</li><li>Verify that Bunny Shield is enabled for your website.</li><li>Review your security logs for blocked requests related to these vulnerabilities</li><li>Continue monitoring WordPress security advisories for any additional guidance.</li></ul><h2 id="staying-ahead-of-new-attack-techniques">Staying ahead of new attack techniques</h2><p>Public vulnerability disclosures are often followed by new proof-of-concept exploits and variations that attempt to bypass existing detections. As those techniques evolve, so do our protections.</p><p>We&apos;ll continue monitoring these vulnerabilities and updating Bunny Shield&apos;s managed protections as new attack techniques emerge, helping keep your applications protected as the threat landscape evolves.</p><h2 id="protect-your-sites-with-bunny-shield">Protect your sites with Bunny Shield</h2><p>While keeping WordPress up to date is your best defense, Bunny Shield adds an extra layer of protection by blocking known exploit attempts before they reach your origin.</p><p>If you&apos;re already using Bunny Shield, these managed protections are available automatically. If you&apos;re not, you can get started in minutes and protect your applications against WordPress vulnerabilities, bots, DDoS attacks, and a wide range of web threats.</p><p>Enable Bunny Shield today to protect your WordPress sites against emerging threats, bots, DDoS attacks, and other common web attacks. <a href="https://dash.bunny.net/auth/login">Log in</a> to your bunny.net account, or <a href="https://dash.bunny.net/auth/register">sign up</a> today to start shielding your websites.</p>]]></content:encoded></item><item><title><![CDATA[Introducing Bunny Sandbox: a disposable room where AI can build]]></title><description><![CDATA[Spin up a fully isolated cloud environment with an AI agent already inside, in one command. Let it run real code, ship what it builds, and throw the room away when you are done.]]></description><link>https://bunny.net/blog/introducing-bunny-sandbox-a-disposable-room-where-ai-can-build/</link><guid isPermaLink="false">6a58ddb1160dc403fbfcf564</guid><category><![CDATA[Compute]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[Amir Keshavarz]]></dc:creator><pubDate>Thu, 16 Jul 2026 13:59:11 GMT</pubDate><media:content url="https://bunny.net/blog/content/images/2026/07/bunny.net-Sandboxes-support-added-to-CLI.png" medium="image"/><content:encoded><![CDATA[<img src="https://bunny.net/blog/content/images/2026/07/bunny.net-Sandboxes-support-added-to-CLI.png" alt="Introducing Bunny Sandbox: a disposable room where AI can build"><p>In 2026, AI agents do not just suggest code. They run it. They install packages, execute shell commands, edit files, clone repos, and reach out across the network, all on their own. That autonomy is the entire point. An agent that has to stop and ask permission before every command is not saving you much time.</p><p>But to be useful, an agent needs real access. And nobody is reading every command it runs. Most of the time, that is fine. The agent does exactly what you asked and moves on. The problem is what happens the rest of the time. Every so often an agent deletes the wrong directory, leaks a key it should never have seen, pulls a package that turns out to be malicious, or gets talked into something by a poisoned web page it read halfway through a task.</p><p>Here is the pragmatic way to think about it. The odds of any single run going wrong are low. But you are not doing a single run. You are doing hundreds or thousands, and the cost when one goes wrong is not small. It is your laptop, your production credentials, or your customers&apos; data. A low chance of an unbounded loss is still a real, recurring risk. You do not want to bet your environment on it every time.</p><p>So people improvise. They run the agent on their own machine and hope for the best. They hand-roll a throwaway VM and wire up the isolation by hand every time. Or they clamp the agent down so tightly it can no longer do the thing they hired it for. None of these is a good answer.</p><p>The good answer is old and boring: give the code its own room. Let the agent do whatever it needs in there. If it makes a mess, the mess stops at the walls. A sandbox is a disposable room where AI can do real work, and if it wrecks the place, you throw the room away.</p><h2 id="we-have-been-building-rooms-like-this-for-a-long-time">We have been building rooms like this for a long time</h2><p>Running other people&apos;s code, somewhere in the world, quickly and cheaply, is not a new problem for us. It is a problem bunny.net has worked on from the start. Our Magic Containers already deploy your apps across a global network of servers spanning six continents, from Ashburn to Tokyo to S&#xE3;o Paulo to Sydney, and they distribute those apps automatically based on where your traffic actually is.</p><p>A sandbox, it turns out, is that same capability with two additions: an AI agent already sitting inside, and a room you can throw away when you&apos;re done. So we built it.</p><h2 id="meet-bunny-sandbox">Meet Bunny Sandbox</h2><p>Bunny Sandbox uses Magic Containers underneath. In one command, you spin up a fully isolated cloud environment with an AI coding agent like Claude Code already installed, and more agents on the way. The agent does real work inside the box, and you can put that work on the internet just as easily. Same isolation and edge power as Magic Containers, none of the setup.</p><p>Every sandbox is a clean Ubuntu container with Node.js, Bun, Python, and Claude Code ready to go, plus a 10 GB persistent workspace so your files survive a restart. You do not manage a server. You do not wire up isolation. You do not clean up afterwards. You get a room, the agent gets to work, and when you are done, the room goes away.</p><h2 id="spin-one-up-from-your-terminal">Spin one up from your terminal</h2><p>Here is the whole loop, from nothing to a live URL, in the bunny.net CLI.</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#8A8A8A;"># Install the CLI</span>
<span style="color:#ECEC93;">curl</span> -fsSL <span style="color:#BDE052;">&lt;https://cli.bunny.net/install.sh&gt;</span> | <span style="color:#ECEC93;">sh</span>

<span style="color:#8A8A8A;"># Log in</span>
bunny <span style="color:#ECEC93;">login</span>

<span style="color:#8A8A8A;"># Create an isolated sandbox (Claude Code is already inside)</span>
bunny sandbox <span style="color:#ECEC93;">create</span> my-sandbox
</pre>
<br>
<!--kg-card-end: html-->
<p>That gives you a fresh, isolated environment on our edge in seconds. You get back an SSH address and a public hostname that goes live once you expose a port. The CLI remembers your sandboxes locally, so you can reconnect any time without re-creating.</p>
<!--kg-card-begin: html-->
<p>
  Claude Code needs one thing before it can get to work: your own Anthropic credentials. Either bake your API key in at create time, or just run <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">claude</code> inside the box and log in once. Both stick around across restarts.
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#8A8A8A;"># .env holds ANTHROPIC_API_KEY</span>
bunny sandbox <span style="color:#ECEC93;">create</span> my-sandbox <span style="color:#ECEC93;">--env-file</span> .env
</pre>
<br>
<!--kg-card-end: html-->
<p>Now put it to work.</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#8A8A8A;"># Drop into the box and let the agent build</span>
bunny sandbox <span style="color:#ECEC93;">ssh</span> my-sandbox

<span style="color:#8A8A8A;"># Or fire off a one-off command without opening a session</span>
bunny sandbox <span style="color:#ECEC93;">exec</span> my-sandbox <span style="color:#ECEC93;">--</span> <span style="color:#ECEC93;">npm</span> test
</pre>
<br>
<!--kg-card-end: html-->
<p>Need to move files? One path is local, the other points at the sandbox:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#8A8A8A;"># Copy a file into the sandbox</span>
bunny sandbox <span style="color:#ECEC93;">cp</span> ./app.js my-sandbox:/workplace/app.js

<span style="color:#8A8A8A;"># And pull one back out</span>
bunny sandbox <span style="color:#ECEC93;">cp</span> my-sandbox:/workplace/out.log ./out.log
</pre>
<br>
<!--kg-card-end: html-->
<p>When the agent builds something worth showing, expose a port as a public HTTPS URL. When you are finished, destroy the whole thing.</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#8A8A8A;"># Put the dev server on port 3000 online</span>
bunny sandbox <span style="color:#ECEC93;">url</span> <span style="color:#ECEC93;">add</span> my-sandbox <span style="color:#D1949E;">3000</span>

<span style="color:#8A8A8A;"># Done? Throw the room away.</span>
bunny sandbox <span style="color:#ECEC93;">delete</span> my-sandbox
</pre>
<br>
<!--kg-card-end: html-->
<p>No control plane to babysit, no separate platform to wire up. When you delete a sandbox, it&apos;s gone, and the Magic Containers app underneath goes with it. One thing to keep in mind: a sandbox keeps running, and keeps costing you, until you delete it, so tear down the ones you&apos;re finished with.</p><figure class="kg-card kg-image-card"><img src="https://bunny.net/blog/content/images/2026/07/sandbox-demo--3-.jpg" class="kg-image" alt="Introducing Bunny Sandbox: a disposable room where AI can build" loading="lazy" width="1250" height="670" srcset="https://bunny.net/blog/content/images/size/w600/2026/07/sandbox-demo--3-.jpg 600w, https://bunny.net/blog/content/images/size/w1000/2026/07/sandbox-demo--3-.jpg 1000w, https://bunny.net/blog/content/images/2026/07/sandbox-demo--3-.jpg 1250w" sizes="(min-width: 720px) 720px"></figure><h2 id="you-can-drive-it-from-code-too">You can drive it from code too</h2>
<!--kg-card-begin: html-->
<p>
  The same operations (and more) are available as a library. Install <a href="https://www.npmjs.com/package/@bunny.net/sandbox"><code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">@bunny.net/sandbox</code></a>, and you can create a sandbox, write files into it, run commands, expose a port, and tear it down, all from your own program. That is how you go from spinning up one sandbox by hand to orchestrating many of them: a test matrix, a fleet of agent workers, a sandbox per user.
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
<span style="color:#569CD6;">import</span> { Sandbox } <span style="color:#569CD6;">from</span> <span style="color:#BDE052;">&quot;@bunny.net/sandbox&quot;</span>;

<span style="color:#569CD6;">const</span> sandbox = <span style="color:#569CD6;">await</span> Sandbox.create({
  apiKey: process.env.BUNNYNET_API_KEY,
  name: <span style="color:#BDE052;">&quot;hi-server&quot;</span>,
  region: <span style="color:#BDE052;">&quot;AMS&quot;</span>,
});

<span style="color:#569CD6;">await</span> sandbox.writeFiles([
  {
    path: <span style="color:#BDE052;">&quot;server.js&quot;</span>,
    content: Buffer.from(<span style="color:#BDE052;">`
      const http = require(&quot;http&quot;);
      http.createServer((req, res) =&gt; {
        res.writeHead(200, { &quot;Content-Type&quot;: &quot;text/plain&quot; });
        res.end(&quot;hi&quot;);
      }).listen(3000, () =&gt; console.log(&quot;listening on 3000&quot;));
    `</span>),
  },
]);

<span style="color:#8A8A8A;">// Start the server detached so it runs in the background.</span>
<span style="color:#569CD6;">const</span> server = <span style="color:#569CD6;">await</span> sandbox.runCommand({
  cmd: <span style="color:#BDE052;">&quot;node&quot;</span>,
  args: [<span style="color:#BDE052;">&quot;server.js&quot;</span>],
  detached: <span style="color:#569CD6;">true</span>,
});

<span style="color:#8A8A8A;">// Expose port 3000 as a public CDN URL.</span>
<span style="color:#569CD6;">const</span> url = <span style="color:#569CD6;">await</span> sandbox.exposePort(<span style="color:#D1949E;">3000</span>);
console.log(<span style="color:#BDE052;">`Open: ${url}`</span>);

<span style="color:#8A8A8A;">// Kill the server after 60 seconds.</span>
setTimeout(() =&gt; server.kill(), <span style="color:#D1949E;">60_000</span>);

<span style="color:#8A8A8A;">// Blocks until the server process exits (via the kill above).</span>
<span style="color:#569CD6;">const</span> finished = <span style="color:#569CD6;">await</span> server.wait();
console.log(finished.exitCode);

<span style="color:#8A8A8A;">// Tear down the sandbox now that the server is done.</span>
<span style="color:#569CD6;">await</span> sandbox.delete();
</pre>
<br>
<!--kg-card-end: html-->
<h2 id="isolation-you-do-not-have-to-think-about">Isolation you do not have to think about</h2><p>This is the part that lets you stop babysitting the agent.</p><p>Every Bunny Sandbox runs on gVisor. In plain terms, gVisor slips a user-space kernel between the code and the real host. To anything running inside, the sandbox looks and behaves like an ordinary Linux machine. But that code never talks directly to our kernel. It talks to gVisor, which decides what is allowed. This is a real boundary, not a normal container with the lights dimmed.</p><p>Every sandbox is walled off from every other one and from the host underneath it. An agent that goes off the rails is trapped in a room it cannot escape. That is the whole promise: the blast radius of a bad command is the sandbox, and only the sandbox.</p><h2 id="from-idea-to-public-url-without-the-detour">From idea to public URL without the detour</h2><p>Because Bunny Sandbox uses Magic Containers and runs on the same global edge, whatever the agent builds can go live on the same network that already serves traffic worldwide. There is no export step, no second platform, no repackaging. One command turns a port inside the box into a real, shareable HTTPS URL. Build it and ship it in the same room.</p><h2 id="pay-for-the-room-while-you-are-in-it">Pay for the room while you are in it</h2><p>Sandboxes are disposable by design: spin up, let the agent work, tear down. A sandbox is billed at standard Magic Containers pricing:</p><ul><li><strong>Compute:</strong> $0.02 per CPU core per hour, billed per second, up to 8 cores</li><li><strong>Memory:</strong> $0.005 per GB per hour, billed in 64 MB increments</li><li><strong>Workspace:</strong> $0.10 per GB per month on the allocated 10 GB</li><li><strong>Anycast IP:</strong> $2 per month, billed per minute (every sandbox gets one, for SSH and public URLs)</li></ul><p>Because it&apos;s per-second and per-use, cost tracks what the agent actually does. A couple of active hours of coding work run on the order of cents of compute and memory, plus a little IP time. Heavier workloads cost more, idle ones less. Delete a sandbox and it stops costing you anything.</p><h2 id="when-you-actually-need-this-and-when-you-do-not">When you actually need this (and when you do not)</h2><p>We will be straight with you, because a sandbox is a tool, not a religion.</p><p>You probably do not need a sandbox to run code you wrote and trust on your own laptop. Carry on. Sandboxes earn their keep in three situations: when the code is untrusted or agent-generated, when you are running many of these at once, or when you want to hand a running environment to your own users without handing them your infrastructure.</p><p>And one honest caveat. Isolation is a boundary, not a force field. It contains what runs inside. It does not turn bad code into good code or a leaked secret back into a safe one. Give each sandbox only the credentials the task genuinely requires. The room keeps the mess in. It is still on you not to put your crown jewels in the room.</p><h2 id="we-are-just-getting-started">We are just getting started</h2><p>Bunny Sandbox is live today, and this is the first version. Here is what is coming:</p><ul><li>More agents. Claude Code is where we started, not where we stop. We are expanding to more coding agents so you can pick the one that fits the job.</li><li>Deeper integration with the rest of bunny.net, so a sandbox can reach for the services you already use without extra plumbing.</li><li>More code syncing, so moving work between your machine and the box stops being a chore.</li><li>A built-in gateway, so putting your work online no longer means paying for a separate IP address.</li><li>More SDKs. Python and Rust are on the way, so you can drive sandboxes from those languages too.</li></ul><p>Tell us what you want to build in a box, and we will help build the box.</p><h2 id="try-it">Try it</h2><p>Want to let an agent run without watching every command it types? Give it a room. Create your first Bunny Sandbox in a couple of commands. The <a href="https://docs.bunny.net/cli/commands/sandbox" rel="noreferrer">docs</a> have the rest.</p><p>The CLI is open source, and the code&apos;s on <a href="https://github.com/BunnyWay/cli">GitHub</a> &#x2014; thanks to everyone who&apos;s contributed so far. Come tell us what you&apos;re building in <a href="https://discord.com/invite/bunnynet">Discord</a>.</p><p>AI needs a safe place to build. Now it has one on a network built to make the internet hop faster. Happy building!</p>]]></content:encoded></item></channel></rss>