<?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>Mon, 17 Aug 2026 04:59:29 GMT</lastBuildDate><atom:link href="https://bunny.net/blog/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><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 an 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><item><title><![CDATA[Manage DNS from the terminal with the bunny.net CLI]]></title><description><![CDATA[Last month we made Bunny DNS free. Today we’re making it even better. You (and your agent) can now manage and automate DNS from your terminal with the bunny.net CLI.]]></description><link>https://bunny.net/blog/manage-dns-from-the-terminal-with-bunny-net-cli/</link><guid isPermaLink="false">6a4f99f5160dc403fbfcf4eb</guid><category><![CDATA[News]]></category><dc:creator><![CDATA[Jamie Barton]]></dc:creator><pubDate>Thu, 09 Jul 2026 13:46:34 GMT</pubDate><media:content url="https://bunny.net/blog/content/images/2026/07/bunny.net-dns-added-to-CLI.png" medium="image"/><content:encoded><![CDATA[<img src="https://bunny.net/blog/content/images/2026/07/bunny.net-dns-added-to-CLI.png" alt="Manage DNS from the terminal with the bunny.net CLI"><p>The terminal has moved way beyond local dev. It&#x2019;s where applications get deployed, infrastructure gets automated, CI pipelines run, and, increasingly, where agents take action on your behalf. That raises the bar for what a CLI has to be. A thin wrapper around an API isn&apos;t enough. It needs to be predictable, scriptable, discoverable, and easy for both people and agents.</p><p>That&apos;s been the focus of the bunny.net CLI since it launched. The <a href="https://bunny.net/blog/introducing-the-bunny-net-cli/">first release</a> shipped with full Database support, letting you create databases and query them without leaving the terminal. A few weeks later, <a href="https://bunny.net/blog/build-and-deploy-scripts-at-the-edge-with-the-bunny-net-cli/">Edge Scripting landed</a>, so you could scaffold, deploy, and manage serverless functions the same way. Since then, we&apos;ve kept folding more of the platform into the same workflow.</p><p>Today, <a href="https://docs.bunny.net/dns">Bunny DNS</a> joins it.</p><p>Bunny DNS is our authoritative DNS hosting, and the routing engine behind our global CDN. It hosts your zones and records like any DNS provider, but it&apos;s also scriptable, so you can run your own code at query time to decide how a name answers, instead of just returning static records. The CLI can now manage both your records and the scripts that answer them.</p><h2 id="a-two-minute-tour"><strong>A two-minute tour</strong></h2><p>The goal we set ourselves for this release was simple: take a domain from nothing to live on bunny.net, pointed at something real, without opening the dashboard. Here&apos;s the whole arc.</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 the zone</span>
bunny dns zones <span style="color:#ECEC93;">add</span> example.com
&#x2713; Created DNS zone example.com (ID: <span style="color:#D1949E;">819216</span>).

Now update your nameservers at &lt;your registrar&gt; to:

 kiki.bunny.net
 coco.bunny.net

<span style="color:#8A8A8A;"># Verify nameservers are detected</span>
bunny dns zones <span style="color:#ECEC93;">ns</span> example.com
&#x2713; Nameservers detected and pointing to Bunny DNS for example.com.

<span style="color:#8A8A8A;"># Point the apex at your origin, and www straight at a pull zone</span>
bunny dns records <span style="color:#ECEC93;">add</span> example.com <span style="color:#BDE052;">&apos;@&apos;</span> A <span style="color:#D1949E;">198.51.100.1</span>
bunny dns records <span style="color:#ECEC93;">add</span> example.com www PULLZONE my-site
</pre>
<br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>Once your registrar picks up the new nameservers, the domain is live: the apex resolved directly, and <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">www</code> served from your pull zone across the bunny.net network. Your DNS and CDN are configured together in four commands.</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>That&apos;s the scripted path. When you&apos;d rather explore, run <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny dns records add example.com</code> with no type and the CLI walks you through it, including presets that set up a whole group of records at once:</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;">
&#x2714; What would you like to add? &#x203A; A preset (email providers, verification, security)
? Choose a preset: &#x203A; - Use arrow-keys. Return to submit.
&#x276F;   Google Workspace  Email
    Gmail-hosted email: MX, SPF, optional DKIM and DMARC.
    Microsoft 365 (Outlook)  Email
    Zoho Mail  Email
    Mailgun  Email
    Resend  Email
    Proton Mail  Email
    Bluesky handle  Verification
    DMARC policy  Email security
    CAA (restrict certificate issuance)  Email security
    No email (anti-spoofing)  Email security
&#x2714; Google DKIM TXT value (Admin console) (blank to skip) &#x2026;
&#x2714; DMARC report email (blank to skip) &#x2026;
This will add <span style="color:#D1949E;">2</span> record(s) to &lt;yourdomain&gt;:

 Type   Name   Value                                 Priority
 MX     @      smtp.google.com                       <span style="color:#D1949E;">1</span>
 TXT    @      v=spf1 include:_spf.google.com ~all

? Add these <span style="color:#D1949E;">2</span> record(s)? &#x203A; (Y/n)
&#x2713; Applied Google Workspace to &lt;yourdomain&gt;: <span style="color:#D1949E;">2</span> record(s) added.
</pre>
<br>
<!--kg-card-end: html-->
<p>Choosing Google Workspace fills in the MX and SPF records for you, then asks for the two things it can&apos;t infer: an optional DKIM value from your Google admin console, and an optional DMARC report address. DKIM and DMARC are the parts of email security people most often skip, because they&apos;re easy to get wrong when configured by hand, so folding them into the preset means new domains start with sender authentication already in place.<br><br>The CLI shows exactly which records it will create and waits for a yes before writing any of them. Presets cover the other major mail providers too, including Microsoft 365, Zoho, Mailgun, Resend, and Proton Mail, alongside domain verification and standalone security records like DMARC and CAA.<br><br>Presets are the kind of thing a human reaches for while setting a domain up. An agent can skip straight past them with explicit flags.</p><h2 id="dns-was-always-meant-to-be-automated">DNS was always meant to be automated</h2><p>DNS sits at the start of almost every project. Before a request reaches your application, your CDN, your edge script, or your origin, DNS decides where it goes. That makes it one of the most important parts of your infrastructure, and one of the most useful things to automate.<br><br>Creating a zone for a new project, adding records as part of a deployment, pointing a hostname at a script to route requests, exporting a zone before you change it, tearing down temporary records when a preview environment is destroyed. Each is part of a workflow rather than a one-off dashboard task, and now they&apos;re all commands you can script, run in CI, or hand to an agent.<br><br>And automating it doesn&apos;t run up a usage bill. Since <a href="https://bunny.net/blog/were-making-bunny-dns-free/" rel="noopener noreferrer">Bunny DNS dropped per-query pricing</a>, DNS hosting is now included for up to 500 domains per account, with no per-query charges and no query limits. No matter how often your pipelines and agents hit it, the queries are free.<br><br>Here are the DNS commands the CLI now supports.</p><h2 id="zones">Zones</h2>
<!--kg-card-begin: html-->
<p>You&apos;ve already seen <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">zones add</code>, which creates a zone and prints the bunny.net nameservers to set at your registrar. The rest of the lifecycle is where you&apos;d expect:</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 dns zones <span style="color:#ECEC93;">list</span>
bunny dns zones <span style="color:#ECEC93;">show</span> example.com
bunny dns zones <span style="color:#ECEC93;">remove</span> example.com
</pre>
<br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>A zone is identified by its domain name or its numeric ID, so <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">example.com</code> and the zone ID are interchangeable wherever a command takes one.</p>
<!--kg-card-end: html-->
<h3 id="link-a-directory-to-a-zone">Link a directory to a zone</h3>
<!--kg-card-begin: html-->
<p>Passing the domain on every command gets repetitive. Link the working directory to a zone once and the rest of the <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">dns</code> commands know which zone 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 dns zones <span style="color:#ECEC93;">link</span> example.com
</pre>
<br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>This writes a small <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">.bunny/dns.json</code> manifest into your project. From then on, zone and record commands run against the linked zone without naming it. It&apos;s the same pattern as <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny db link</code>, <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny scripts link</code>, and soon <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny storage link</code>, and as with those, the manifest is per-developer state, so add <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">.bunny/</code> to your <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">.gitignore</code>. When a command falls back to the interactive picker, it offers to link the directory for you.</p>
<!--kg-card-end: html-->
<h2 id="records">Records</h2>
<!--kg-card-begin: html-->
<p>Records live inside a zone. <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">records add</code> takes the name, type, and value, with the zone apex written as <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">&apos;@&apos;</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 dns records <span style="color:#ECEC93;">add</span> example.com api A <span style="color:#D1949E;">198.51.100.1</span>
bunny dns records <span style="color:#ECEC93;">add</span> example.com <span style="color:#BDE052;">&apos;@&apos;</span> MX mail.example.com <span style="color:#D1949E;">10</span>
bunny dns records <span style="color:#ECEC93;">list</span> example.com
</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;">bunny dns records add</code> with the type left off and it walks you through an interactive wizard instead. Bunny DNS supports the record types you&apos;d expect alongside a few bunny-specific ones, including <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">PULLZONE</code> to point a name straight at a pull zone and <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">SCRIPT</code> to answer it with Scriptable DNS.</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>Updating and removing records works by record ID, which you can read from <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">records list</code>, or you can leave the ID off and pick from a list. Only the fields you pass are changed:</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 dns records <span style="color:#ECEC93;">update</span> example.com <span style="color:#D1949E;">123</span> <span style="color:#ECEC93;">--value</span> <span style="color:#D1949E;">198.51.100.2</span>
bunny dns records <span style="color:#ECEC93;">remove</span> example.com <span style="color:#D1949E;">123</span>
</pre>
<!--kg-card-end: html-->
<h3 id="migrate-a-zone-with-bind-files">Migrate a zone with BIND files</h3><p>Moving a domain onto Bunny DNS, or taking a backup before a big change, comes down to two commands. Export writes a standard BIND zone file, and import reads one back in:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny dns records <span style="color:#ECEC93;">export</span> example.com <span style="color:#ECEC93;">--save</span>
bunny dns records <span style="color:#ECEC93;">import</span> example.com ./example.com.zone
</pre>
<br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p><code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">--save</code> writes <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">example.com.zone</code> alongside you. Omit it to print the zone to stdout and pipe it wherever you like.</p>
<!--kg-card-end: html-->
<h3 id="dnssec-and-zone-settings">DNSSEC and zone settings</h3><p>Turn on DNSSEC and the CLI prints the DS record to hand to your registrar:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny dns zones dnssec <span style="color:#ECEC93;">enable</span> example.com
</pre>
<br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p><a href="https://bunny.net/blog/were-making-bunny-dns-free/">Bunny DNS implements DNSSEC</a> without exposing your zone&apos;s structure, so this is one you can switch on without the usual hesitation. Query logging and per-zone statistics are a command away too, with <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny dns zones logging</code> and <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny dns zones stats</code>.</p>
<!--kg-card-end: html-->
<h2 id="scriptable-dns-from-the-cli">Scriptable DNS from the CLI</h2>
<!--kg-card-begin: html-->
<p>Scriptable DNS runs your code at query time to decide how a name should answer, which is how you build geo routing, weighted answers, failover, and closest-region selection. It&apos;s a separate runtime from Edge Scripts, so it has its own command group under <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny dns scripts</code>.</p>
<!--kg-card-end: html-->
<p>The flow mirrors Edge Scripting. Scaffold a project from an example, deploy the script, and attach it to a hostname:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny dns scripts <span style="color:#ECEC93;">init</span> geo-router <span style="color:#ECEC93;">--example</span> geo <span style="color:#ECEC93;">--deploy</span>
cd geo-router
bunny dns scripts <span style="color:#ECEC93;">deploy</span> handleQuery.js
bunny dns scripts <span style="color:#ECEC93;">attach</span> example.com geo <span style="color:#ECEC93;">--script</span> &lt;id&gt;
</pre>
<br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p><code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">init</code> drops a <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">handleQuery</code> entry file. The starters cover the common routing shapes (<code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">geo</code>, <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">closest</code>, <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">weighted</code>, <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">failover</code>, and <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">pullzone</code>), plus an <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">empty</code> one to begin from scratch:</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;">/// &lt;reference types=&quot;@bunny.net/scriptable-dns-types&quot; /&gt;</span>

<span style="color:#8A8A8A;">/** @param {DnsRequest} query */</span>
<span style="color:#569CD6;">export</span> <span style="color:#569CD6;">default</span> <span style="color:#569CD6;">function</span> handleQuery(query) {
  <span style="color:#569CD6;">const</span> { geoLocation, serverZone } = query.request;

  <span style="color:#569CD6;">return</span> <span style="color:#569CD6;">new</span> TxtRecord(
    <span style="color:#BDE052;">`country=${geoLocation.country} zone=${serverZone} asn=${geoLocation.asn}`</span>,
    <span style="color:#D1949E;">30</span>,
  );
}
</pre>
<br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>That reference line at the top is new too: <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">@bunny.net/scriptable-dns-types</code> ships ambient types for the Scriptable DNS runtime, so your editor autocompletes queries, records, and helpers even in plain JavaScript. No more guessing, no docs tab.</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>A script does nothing to your DNS until it backs a record, which is what <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">attach</code> handles: it adds a <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">SCRIPT</code> record so <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">geo.example.com</code> is answered by your code.</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>Because that record changes how a name resolves, <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">attach</code> always asks before it writes, and attaching at the apex prints a louder warning and lists anything already there first. In a pipeline, where there&apos;s no one to confirm, it refuses to write unless you pass <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">--force</code>.</p>
<!--kg-card-end: html-->
<p>That turns DNS into something you build with. Records in a table become routing logic that lives alongside the rest of your infrastructure.</p><h2 id="works-with-your-agent-and-ci">Works with your agent and CI</h2><p>An agent driving a tool needs three things: to discover what it can do, to call it and get a predictable result, and to recover when something goes wrong. The DNS commands are built for all three, the same way the rest of the CLI is.</p>
<!--kg-card-begin: html-->
<p>Discovery comes from a consistent command structure and the skill files we ship alongside the CLI. An agent doesn&apos;t have to reverse-engineer the dashboard. It reads the skill, finds that zones, records, and scripts each follow the same shape, and knows that <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny dns &lt;command&gt; --help</code> will spell out the flags.</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>Reliable calls come from being flags-first and deterministic. Pass the domain, the record, and the values and you get the same result every time, with no prompt waiting in the way. Linked manifests mean an agent isn&apos;t juggling zone IDs in its context, and <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">--output json</code> turns any command into something it can parse rather than scrape.</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>Recovery comes from how the CLI fails. Errors are written in plain language with a hint at the fix, exit codes are meaningful, and under <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">--output json</code> the error comes back as structured JSON too. An agent can tell a fixable mistake from an internal one and respond, instead of retrying blindly. Prompts and pickers are TTY-aware so they step out of the way in CI, and destructive commands like <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">records remove</code> and <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">scripts attach</code> take <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">--force</code> for the same reason.</p>
<!--kg-card-end: html-->
<p>The result is that the commands you run are the commands your automation runs, and the commands your agent runs. No separate integration to maintain, no protocol to configure.</p><h2 id="one-cli-for-the-platform">One CLI for the platform</h2><p>Every release brings another part of bunny.net into the workflow developers already use:</p><ul><li>Databases</li><li>Edge Scripts</li><li>DNS and Scriptable DNS</li><li>Agentic workflows across all of them</li></ul><p>Coming up next are Magic Containers and Storage.</p><p>Get started:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
npm <span style="color:#ECEC93;">install</span> <span style="color:#ECEC93;">-g</span> @bunny.net/cli
bunny <span style="color:#ECEC93;">login</span>
bunny dns zones <span style="color:#ECEC93;">add</span> example.com
</pre>
<br>
<!--kg-card-end: html-->
<p>The CLI is open source and built in public. If something&apos;s broken, <a href="https://github.com/BunnyWay/cli/issues" rel="noopener noreferrer">open an issue</a>. If you&apos;ve built something with it, come and tell us about it in <a href="https://discord.com/invite/bunnynet" rel="noopener noreferrer">Discord</a>.</p>]]></content:encoded></item><item><title><![CDATA[What hundreds of billions of requests reveal about edge security in 2026]]></title><description><![CDATA[Hundreds of billions of requests pass through the bunny.net edge network every day from websites and applications around the world. That gives us a unique view into how attacks are evolving, what malicious traffic really looks like, and the techniques attackers continue to rely on.]]></description><link>https://bunny.net/blog/what-hundreds-of-billions-of-requests-reveal-about-edge-security-in-2026/</link><guid isPermaLink="false">6a465ac8160dc403fbfcf4c9</guid><category><![CDATA[Security]]></category><dc:creator><![CDATA[Joe Connolly]]></dc:creator><pubDate>Thu, 02 Jul 2026 13:00:00 GMT</pubDate><media:content url="https://bunny.net/blog/content/images/2026/07/bunny-edge-security-report.png" medium="image"/><content:encoded><![CDATA[<img src="https://bunny.net/blog/content/images/2026/07/bunny-edge-security-report.png" alt="What hundreds of billions of requests reveal about edge security in 2026"><p>Hundreds of billions of requests pass through the bunny.net edge network every day from websites and applications around the world. That gives us a unique view into how attacks are evolving, what malicious traffic really looks like, and the techniques attackers continue to rely on.</p><p>To share those insights, we&apos;ve published the <strong>bunny.net 2026 Edge Security Report</strong>.</p><p>Built from <strong>149 days of production telemetry</strong> across our global edge network, the report explores the attack trends, traffic patterns, and architectural lessons we&apos;ve observed while helping protect applications at internet scale.</p><p><a href="https://bunny.net/edge-security-report-2026/" rel="noreferrer"><strong>Hop into the 2026 Edge Security Report &#x2192;</strong></a></p><h2 id="what-youll-learn">What you&apos;ll learn</h2><p>Rather than relying on surveys or industry estimates, every finding is backed by real production traffic observed across the bunny.net edge network.</p><p>The report covers:</p><ul><li>The biggest edge security trends shaping 2026.</li><li>The attack techniques we see most often in production.</li><li>How automated traffic is changing, and what that means for modern security.</li><li>Common architectural pitfalls that can leave applications exposed.</li><li>Real-world attack case studies and the lessons they reveal.</li></ul><p>Whether you&apos;re responsible for infrastructure, security, or platform engineering, the report gives you a data-backed view of how today&#x2019;s attacks behave in production, so you can stay one hop ahead.</p><p><a href="https://bunny.net/edge-security-report-2026/" rel="noreferrer"><strong>Download the full Edge Security Report and explore the findings for yourself.</strong></a></p>]]></content:encoded></item><item><title><![CDATA[We’re making Bunny DNS free: because a faster internet won’t build itself]]></title><description><![CDATA[Your DNS bill shouldn't scale with your traffic. So we removed query fees from Bunny DNS entirely. No per-request billing, no query limits, free DNS hosting for up to 500 domains, and nothing critical locked behind some enterprise plan.]]></description><link>https://bunny.net/blog/were-making-bunny-dns-free/</link><guid isPermaLink="false">6a3a7966160dc403fbfcf4a9</guid><category><![CDATA[News]]></category><dc:creator><![CDATA[Dejan Grofelnik Pelzel]]></dc:creator><pubDate>Wed, 24 Jun 2026 05:58:00 GMT</pubDate><media:content url="https://bunny.net/blog/content/images/2026/06/DNS_-Free-DNS-blog-post--1-.png" medium="image"/><content:encoded><![CDATA[<img src="https://bunny.net/blog/content/images/2026/06/DNS_-Free-DNS-blog-post--1-.png" alt="We&#x2019;re making Bunny DNS free: because a faster internet won&#x2019;t build itself"><p>At bunny.net, our mission has always been ambitious but focused: <strong>help make the internet hop faster.</strong></p><p>To do that, we&#x2019;ve built a massive global network spanning 119 locations and counting. Today, this network powers over 1.5 million websites and consistently delivers some of the fastest content delivery around the globe. But while deploying thousands of servers globally is an impressive feat on its own, the hardware itself does not explain how bunny.net is able to deliver such an impressive level of performance.</p><p>The real secret hides under the hood, embedded in the routing engine that directs every request, every user, and sends traffic exactly where it needs to go. That engine is Bunny DNS.</p><h2 id="from-internal-engine-to-200-billion-customer-queries-per-month">From internal engine to 200 billion customer queries per month</h2><p>Originally, <a href="https://docs.bunny.net/dns">Bunny DNS</a> was built with one simple goal: to build the most advanced routing engine possible, capable of analyzing every DNS query and directing it to the optimal destination for serving your content. Even to this day, it&#x2019;s what makes Bunny CDN achieve it&#x2019;s exceptional performance.</p><p>Four years ago, we took everything we had learned from designing and running this system and turned it into a product our users could use themselves. With Bunny DNS, we&#x2019;ve upgraded DNS from being a basic record lookup table into a globally distributed, smart routing engine. Instead of just returning static records, it allows developers to use latency data, health checks, and even JavaScript to dynamically determine exactly where requests should go.</p><p>We applied our traditional mantra. We made it affordable, scalable, and added a generous free tier. The response was incredible, and today, Bunny DNS powers over 300,000 domains and handles nearly 200 billion queries every single month.</p><p>But as we looked closer at our mission, we realized something needed to change.</p><h2 id="why-dns-shouldnt-cost-extra">Why DNS shouldn&apos;t cost extra</h2><p>Dealing with infrastructure costs is already notoriously complex. You shouldn&apos;t have to stress about pricing tiers or whether a sudden spike of a million queries is going to result in an unpredictable bill.</p><p>If we truly believe in our mission to help make the internet hop faster, then the fundamental system that sits between your users and your services shouldn&apos;t be a premium add-on. It should be accessible to everyone.</p><p><strong>So, we&#x2019;ve eliminated DNS query fees entirely.</strong></p><p>Bunny DNS no longer charges for DNS queries and includes free DNS hosting for up to 500 domains per account. There are no query limits, no per-request billing, and no critical features hidden behind enterprise plans. (Yes, that includes smart records and health monitoring too.)</p><p>As with all bunny.net services, accounts using the platform are subject to our standard $1/month minimum spend, but DNS itself no longer incurs any usage-based charges.</p><h2 id="the-entry-point-for-everything-else">The entry point for everything else</h2><p>Making Bunny DNS free doesn&apos;t mean we&apos;re losing interest in it. Quite the opposite. More than ever, we view DNS as the core product that glues our entire platform together. It&#x2019;s the starting line for everything else your application does.</p><p>Getting that starting line set up is now easier than ever. If you&apos;re migrating from somewhere else, our new automatic zone scanning checks your domain&apos;s most common record names and types, reconstructing your zone so you only have to make a few tweaks instead of starting from scratch. (You can also just upload a BIND file if you prefer.)</p><p>Once your records are in place, the real magic happens. With 1-Click Acceleration, you can enable the CDN directly from your DNS records. We&#x2019;ll spin up a Pull Zone behind the scenes and instantly start routing requests through our edge network. Once traffic is flowing, 1-Click Security lets you enable Bunny Shield instantly to filter traffic at the edge, blocking common exploits and absorbing DDoS attacks before they ever touch your origin server.</p><p>Performance, security, and routing are now unified in one place, rather than stitched together after the fact. Our goal is to keep evolving this with even more advanced record types.</p><h2 id="beyond-making-it-free-we%E2%80%99re-also-making-it-better">Beyond making it free, we&#x2019;re also making it better</h2><p>Saving on costs is great, but ultimately, what drives us at bunny.net is building incredible products. While all of that was happening, we&#x2019;ve also been making steady changes to DNS itself. There haven&#x2019;t been many major releases, but rather a lot of smaller improvements over time.</p><h3 id="ipv6-is-no-longer-optional">IPv6 is no longer optional</h3><p>More and more networks default to IPv6 now, especially on mobile. So we made sure everything on our side just works in a dual-stack world.</p><p>If you&#x2019;re using Bunny DNS, your nameserver records already resolve over both IPv4 and IPv6. There&#x2019;s nothing to configure, nothing to migrate. It just works the way it probably should have a while ago.</p><h3 id="we%E2%80%99ve-added-dnssec-without-the-usual-trade-offs">We&#x2019;ve added DNSSEC, without the usual trade-offs</h3><p>DNSSEC is one of those things people want in theory but hesitate to adopt in practice.</p><p>Part of that is the complexity, but part of it is also that traditional DNSSEC can expose information about your zone that you might not want to share.</p><p>We implemented DNSSEC with NSEC Black Lies to get around that. You still get the validation and protection against tampering, but without making it easy for someone to walk your entire domain structure.</p><p>It&#x2019;s one of those details most people won&#x2019;t notice directly, but it does change how comfortable you can be with <a href="https://docs.bunny.net/dns/dnssec">turning DNSSEC on</a>.</p><h3 id="we%E2%80%99ve-modernized-record-types">We&#x2019;ve modernized record types</h3><p>DNS has moved far beyond simple address records. Modern applications increasingly rely on DNS not just to point a name at an IP address, but to describe how clients should connect, how certificates should be validated, and how security settings should be managed over time.</p><p>That&#x2019;s why we&#x2019;ve been expanding Bunny DNS with support for more advanced record types.</p><p>We&#x2019;ve added support for HTTPS and SVCB records, which let you hint how clients should connect to your services. TLSA records are there if you&#x2019;re using DANE and want tighter control over certificate validation. And CDS and CDNSKEY help automate DNSSEC key management so you&#x2019;re not rotating things by hand.</p><p>None of this is particularly flashy, but it&#x2019;s the kind of stuff you end up needing once you move past a basic setup.</p><h2 id="helping-you-build-faster">Helping you build faster</h2><p>We take our mission seriously, and the &#x201C;help&#x201D; part of it is perhaps the most important. By dropping usage charges for DNS and integrating it deeply with the rest of our stack, starting with CDN and <a href="https://docs.bunny.net/shield">Shield</a>, we want to help you build faster, safer, and more resilient applications without worrying about arbitrary limits.</p><p>In a world where everyone simply wants to ship as many features as fast as possible, we&#x2019;re focused on something else: making Bunny DNS incredible to use and seamlessly integrated into everything else we do, so you can build faster, sleep easier, and ultimately create build better user experiences for everyone.</p><p>If you haven&#x2019;t tried Bunny DNS in a while, now&#x2019;s a good time to take another look. You can add your zones, point your domains, and leave it at that, or start layering on CDN and Shield when you need them.</p><p>It&#x2019;s free now, so you can <a href="https://dash.bunny.net/auth/login">log in</a> or <a href="https://dash.bunny.net/auth/register">sign up</a> and start using it straight away.</p>]]></content:encoded></item><item><title><![CDATA[Bring Bunny Storage into DatoCMS without the copy-paste]]></title><description><![CDATA[Bunny Storage and DatoCMS now work together natively. The bunny.net Picker is available in the DatoCMS plugin directory: browse folders, search, drag to upload, and preview files without ever leaving your content editor.]]></description><link>https://bunny.net/blog/bring-bunny-storage-into-datocms-without-the-copy-paste/</link><guid isPermaLink="false">6a2fcc87160dc403fbfcf46f</guid><category><![CDATA[News]]></category><dc:creator><![CDATA[Jamie Barton]]></dc:creator><pubDate>Mon, 15 Jun 2026 10:28:18 GMT</pubDate><media:content url="https://bunny.net/blog/content/images/2026/06/Bring-Bunny-Storage-into-DatoCMS--without-the-copy-paste--2-.png" medium="image"/><content:encoded><![CDATA[<img src="https://bunny.net/blog/content/images/2026/06/Bring-Bunny-Storage-into-DatoCMS--without-the-copy-paste--2-.png" alt="Bring Bunny Storage into DatoCMS without the copy-paste"><p>Picking the right asset shouldn&apos;t mean juggling two dashboards.</p><p>Until recently, using Bunny Storage with DatoCMS looked something like this:</p><ul><li>Log into Storage</li><li>Find the file</li><li>Copy the path</li><li>Switch to DatoCMS</li><li>Paste it into a JSON field <em>and hope you got the URL structure right</em></li><li><em>Repeat for every asset</em></li></ul><p>It worked. It just didn&apos;t feel like it should. Content editors deserve better.</p>
<!--kg-card-begin: html-->
<figure class="kg-card kg-embed-card" style="width:100%;max-width:100%;margin:2rem 0;">
  <div style="position:relative;width:100%;max-width:100%;padding-bottom:56.25%;height:0;overflow:hidden;">
    <iframe src="https://player.mediadelivery.net/embed/426067/c52f09d6-7fec-4ca1-a964-638918326d54?autoplay=false&amp;loop=false&amp;muted=false&amp;preload=true&amp;responsive=true" loading="lazy" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" allow="accelerometer;gyroscope;autoplay;encrypted-media;picture-in-picture;fullscreen;" allowfullscreen>
    </iframe>
  </div>
</figure>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<div style="height:2rem;"></div>
<!--kg-card-end: html-->
<p>The setup came out of a conversation with a customer who&apos;d built their own version using Edge Scripting and manual JSON fields. The pieces were all there, but the day-to-day experience of choosing a file was rough on editors and even rougher to maintain across projects. We kept thinking about it and decided to build something better for everyone using Bunny Storage with DatoCMS.</p><p>The result is the <a href="https://www.datocms.com/marketplace/plugins/i/@bunny.net/datocms-plugin" rel="noreferrer"><strong>bunny.net Picker</strong></a>, a DatoCMS plugin that turns asset selection into a single, native step inside your content editor.</p><p>Once installed, editors get a proper file picker right where they need it. Browse folders in your Storage Zone. Search the current folder. Drag files in to upload. Preview images and media before selecting. Open the selected file in a new tab when you want to double-check it. Everything happens inside DatoCMS against your live Storage Zone.</p><p>When you set up your DatoCMS schema, you choose how each field should behave. Add a JSON field, then pick the presentation that fits the content:</p><ul><li><strong>bunny.net Asset</strong> for fields that hold a single file</li><li><strong>bunny.net Assets</strong> for fields that hold several</li></ul><figure class="kg-card kg-image-card"><img src="https://bunny.net/blog/content/images/2026/06/CleanShot-2026-04-28-at-10.50.11@2x.png" class="kg-image" alt="Bring Bunny Storage into DatoCMS without the copy-paste" loading="lazy" width="1630" height="1407" srcset="https://bunny.net/blog/content/images/size/w600/2026/06/CleanShot-2026-04-28-at-10.50.11@2x.png 600w, https://bunny.net/blog/content/images/size/w1000/2026/06/CleanShot-2026-04-28-at-10.50.11@2x.png 1000w, https://bunny.net/blog/content/images/size/w1600/2026/06/CleanShot-2026-04-28-at-10.50.11@2x.png 1600w, https://bunny.net/blog/content/images/2026/06/CleanShot-2026-04-28-at-10.50.11@2x.png 1630w" sizes="(min-width: 720px) 720px"></figure><p>Either presentation accepts any file type your Storage Zone permits, so the same picker works for hero images, product photography, PDFs, video, audio, fonts, or anything else you keep in bunny.net.</p><p>For developers, the plugin stores a clean, predictable JSON value in the field. A single asset looks like this after parsing:</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;path&quot;</span>: <span style="color:#BDE052;">&quot;folder/image.jpg&quot;</span>,
  <span style="color:#D1949E;">&quot;filename&quot;</span>: <span style="color:#BDE052;">&quot;image.jpg&quot;</span>,
  <span style="color:#D1949E;">&quot;size&quot;</span>: <span style="color:#D1949E;">153240</span>,
  <span style="color:#D1949E;">&quot;contentType&quot;</span>: <span style="color:#BDE052;">&quot;image/jpeg&quot;</span>,
  <span style="color:#D1949E;">&quot;url&quot;</span>: <span style="color:#BDE052;">&quot;https://my-zone.b-cdn.net/folder/image.jpg&quot;</span>,
  <span style="color:#D1949E;">&quot;guid&quot;</span>: <span style="color:#BDE052;">&quot;8e3b3b4e-1f6a-4b2a-9b8a-2f1e6d7c8a9b&quot;</span>,
  <span style="color:#D1949E;">&quot;lastChanged&quot;</span>: <span style="color:#BDE052;">&quot;2025-04-30T10:21:00.000&quot;</span>
}
</pre>
<br>
<!--kg-card-end: html-->
<p>Multi-asset fields return an array of the same shape:</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;path&quot;</span>: <span style="color:#BDE052;">&quot;folder/image.jpg&quot;</span>,
    <span style="color:#D1949E;">&quot;filename&quot;</span>: <span style="color:#BDE052;">&quot;image.jpg&quot;</span>,
    <span style="color:#D1949E;">&quot;size&quot;</span>: <span style="color:#D1949E;">153240</span>,
    <span style="color:#D1949E;">&quot;contentType&quot;</span>: <span style="color:#BDE052;">&quot;image/jpeg&quot;</span>,
    <span style="color:#D1949E;">&quot;url&quot;</span>: <span style="color:#BDE052;">&quot;https://my-zone.b-cdn.net/folder/image.jpg&quot;</span>,
    <span style="color:#D1949E;">&quot;guid&quot;</span>: <span style="color:#BDE052;">&quot;8e3b3b4e-1f6a-4b2a-9b8a-2f1e6d7c8a9b&quot;</span>,
    <span style="color:#D1949E;">&quot;lastChanged&quot;</span>: <span style="color:#BDE052;">&quot;2025-04-30T10:21:00.000&quot;</span>
  }
]
</pre>
<br>
<!--kg-card-end: html-->
<p>The same value comes through the DatoCMS GraphQL API, so you can query the field like any other piece of content:</p><figure class="kg-card kg-image-card"><img src="https://bunny.net/blog/content/images/2026/06/datocms-graphql-response.png" class="kg-image" alt="Bring Bunny Storage into DatoCMS without the copy-paste" loading="lazy" width="2000" height="977" srcset="https://bunny.net/blog/content/images/size/w600/2026/06/datocms-graphql-response.png 600w, https://bunny.net/blog/content/images/size/w1000/2026/06/datocms-graphql-response.png 1000w, https://bunny.net/blog/content/images/size/w1600/2026/06/datocms-graphql-response.png 1600w, https://bunny.net/blog/content/images/size/w2400/2026/06/datocms-graphql-response.png 2400w" sizes="(min-width: 720px) 720px"></figure><p>That&apos;s the whole integration. No edge script required, no per-project duplication, no URL stitching in the CMS.</p><p>Configuration is equally lightweight. Point the plugin at your Storage Zone name, API key, Pull Zone hostname, and region, and you&apos;re done.</p><figure class="kg-card kg-image-card"><img src="https://bunny.net/blog/content/images/2026/06/CleanShot-2026-04-28-at-05.02.36@2x.png" class="kg-image" alt="Bring Bunny Storage into DatoCMS without the copy-paste" loading="lazy" width="1424" height="1178" srcset="https://bunny.net/blog/content/images/size/w600/2026/06/CleanShot-2026-04-28-at-05.02.36@2x.png 600w, https://bunny.net/blog/content/images/size/w1000/2026/06/CleanShot-2026-04-28-at-05.02.36@2x.png 1000w, https://bunny.net/blog/content/images/2026/06/CleanShot-2026-04-28-at-05.02.36@2x.png 1424w" sizes="(min-width: 720px) 720px"></figure><p>All Bunny Storage regions are supported, including Frankfurt, London, New York, Los Angeles, Singapore, Stockholm, S&#xE3;o Paulo, Johannesburg, and Sydney. Localized JSON fields work out of the box, and existing fields using the older single-asset editor keep the same stored value format, so nothing breaks when you upgrade.</p><h2 id="image-transformations-with-bunny-optimizer">Image transformations with Bunny Optimizer</h2>
<!--kg-card-begin: html-->
<p>
  If your Pull Zone has <a href="https://docs.bunny.net/optimizer/quickstart">Bunny Optimizer</a> enabled, you can transform <strong>images</strong> on the fly by appending query parameters to the stored <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">url</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;">
<span style="color:#569CD6;">const</span> asset = JSON.parse(record.bunnyAsset)
<span style="color:#569CD6;">const</span> thumbnail = <span style="color:#BDE052;">`${asset.url}?width=400&amp;height=300&amp;aspect_ratio=4:3`</span>
<span style="color:#569CD6;">const</span> webp = <span style="color:#BDE052;">`${asset.url}?format=webp&amp;quality=80`</span>
</pre>
<br>
<!--kg-card-end: html-->
<p>Common parameters:</p>
<!--kg-card-begin: html-->
<ul>
  <li><code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">width</code>, <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">height</code> to resize images</li>
  <li><code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">aspect_ratio</code> to crop to a ratio, for example <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">16:9</code></li>
  <li><code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">format</code> to convert to <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">webp</code>, <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">avif</code>, <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">jpg</code>, or <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">png</code></li>
  <li><code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">quality</code> for compression quality from <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">0</code> to <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">100</code></li>
</ul>
<!--kg-card-end: html-->
<p>See the <a href="https://docs.bunny.net/optimizer/quickstart">Bunny Optimizer docs</a> for the full list of options. Optimizer <strong>must be enabled</strong> on your Pull Zone for these parameters to take effect.</p><p>Building this also turned out to be a genuinely good developer experience. The DatoCMS Plugin SDK lets you develop with React and Vite while connected locally to your project, which made iterating on the picker feel closer to building a normal app than wrestling with a CMS extension. If you want to extend the plugin or build your own, <a href="https://www.datocms.com/docs/plugin-sdk/build-your-first-plugin">their Plugin SDK docs</a> are a good place to start. All of the code is <a href="https://github.com/jamie-at-bunny/datocms-bunny-picker">open source on GitHub</a>, and <a href="http://npmjs.org/@bunny.net/datocms-plugin">available on NPM</a>.</p><p>The <a href="http://bunny.net">bunny.net</a> Picker is available now in the DatoCMS plugin directory. Install it, point it at your Storage Zone, and give your editors the picker they should have had all along.</p>]]></content:encoded></item><item><title><![CDATA[How to tell if scrapers are eating your bandwidth]]></title><description><![CDATA[If you aren't using Bunny Shield and have noticed anomalies in your bandwidth, read this guide to diagnose what's causing it.]]></description><link>https://bunny.net/blog/how-to-tell-if-scrapers-are-eating-your-bandwidth/</link><guid isPermaLink="false">6a2c02ad160dc403fbfcf455</guid><category><![CDATA[Tips and Tricks]]></category><dc:creator><![CDATA[Dino Kukic]]></dc:creator><pubDate>Fri, 12 Jun 2026 13:05:50 GMT</pubDate><media:content url="https://bunny.net/blog/content/images/2026/06/how-to-tell-if-scrapers-are-eating-your-bandwidth.png" medium="image"/><content:encoded><![CDATA[<img src="https://bunny.net/blog/content/images/2026/06/how-to-tell-if-scrapers-are-eating-your-bandwidth.png" alt="How to tell if scrapers are eating your bandwidth"><p><a href="https://bunny.net/shield/">Bunny Shield</a> makes identifying and blocking bots trivial. However, if you aren&#x2019;t using it and have noticed anomalies in your bandwidth, we&#x2019;ve prepped this guide to help you determine whether the reason is a pop star tweeting about your website or, unfortunately, a surge in bot traffic that&apos;s now costing you real money.</p><h2 id="not-all-bots-are-the-bad-guys">Not all bots are the bad guys</h2><p>Before you start blocking things, it&apos;s worth understanding what&apos;s actually out there. The term &quot;bot&quot; covers everything from the crawler that helps get your content in Google to the script copying your entire product catalog to sell to a competitor. Treating them all the same is how you accidentally remove your site from search results.</p><p>Here&apos;s the roughly sorted landscape.</p><h3 id="search-engine-crawlers">Search engine crawlers</h3><p><a href="https://developers.google.com/search/docs/crawling-indexing/googlebot">Googlebot</a>, <a href="https://www.bing.com/webmasters/help/which-crawlers-does-bing-use-8c184ec0">Bingbot</a>, and, yeah, mostly Googlebot. They crawl and index your content and then serve it for relevant queries in the search engine results page (SERP). For the most part, you want these, and blocking them might harm your site unless you don&#x2019;t need traffic coming from search engines at all. One caveat here is that Bingbot increasingly feeds Copilot, Applebot feeds Apple Intelligence, and Google&apos;s crawl feeds Gemini. So they are being used for other things as well, and if those use cases aren&apos;t an issue, this is actually a good thing. Your site is crawled once (periodically) for multiple purposes. It become an issue when you want one, but don&#x2019;t want the other.</p><h3 id="ai-search-bots">AI search bots</h3><p><a href="https://developers.openai.com/api/docs/bots#:~:text=Description%20%26%20details-,OAI%2DSearchBot,-OAI%2DSearchBot%20is">OAI-SearchBot</a>, <a href="https://developers.openai.com/api/docs/bots#:~:text=com/gptbot.json-,ChatGPT%2DUser,-OpenAI%20also%20uses">ChatGPT-User</a>, <a href="https://docs.perplexity.ai/docs/resources/perplexity-crawlers#:~:text=Description-,PerplexityBot,-PerplexityBot%20is%20designed">PerplexityBot</a>, <a href="https://support.claude.com/en/articles/8896518-does-anthropic-crawl-data-from-the-web-and-how-can-site-owners-block-the-crawler#:~:text=directed%20web%20search.-,Claude%2DSearchBot,-Claude%2DSearchBot%20navigates">Claude-SearchBot</a>, and others. These fetch your page in real time to answer a user&apos;s current question, often with a citation back to you. They&apos;re more like search crawlers than training crawlers because they can send real visitors your way.</p><h3 id="ai-training-crawlers">AI training crawlers</h3><p><a href="https://app.notion.com/p/url">GPTBot</a>, <a href="https://app.notion.com/p/url">ClaudeBot</a>, <a href="https://app.notion.com/p/url">Meta-ExternalAgent</a>, and the rest. They collect content to train large language models. They can use real bandwidth and generally don&apos;t send traffic back the way a search crawler does, though that may be changing as AI assistants increasingly cite and link their sources. Whether that&apos;s worth the bandwidth depends on how you value having your content represented in the models.</p><h3 id="agentic-and-assistant-bots">Agentic and assistant bots</h3><p>These are bots completing a task on behalf of a specific user, such as booking, buying, comparing, summarizing, and more. These often represent a real person with real intent, so blocking them can mean blocking a customer. The category is immature and hard to identify cleanly.</p><h3 id="marketing-intelligence-crawlers">Marketing intelligence crawlers</h3><p><a href="https://ahrefs.com/robot#:~:text=Our%20bots-,AhrefsBot,-User%2Dagent%20string">AhrefsBot</a>, <a href="https://www.semrush.com/bot/">SemrushBot</a>, and similar. They crawl your site to build the commercial SEO datasets behind tools marketers use to track rankings, find backlinks, and size up competitors. The value to you is indirect and slightly circular. These crawlers help build the same SEO datasets you might use to analyze competitors. Whether that&#x2019;s worth the crawl is a judgment call.</p><h3 id="social-link-preview-bots">Social / link-preview bots</h3><p><a href="https://developers.facebook.com/docs/sharing/webmasters/web-crawlers/#:~:text=with%20your%20site.-,FacebookExternalHit,-The%20primary%20purpose">FacebookExternalHit</a>, Twitterbot, LinkedInBot, Slackbot, Discordbot, etc. They fetch a page to build the preview card when someone shares your link. These are triggered by real people sharing your content. If you block them, your links may render as ugly bare URLs everywhere. They&apos;re generally low volume but potentially high value.</p><h3 id="commercial-vertical-scrapers">Commercial / vertical scrapers</h3><p>Price-comparison engines, job aggregators, review aggregators. Whether you want them depends entirely on your business. A price comparison bot is great if you want to be compared and a problem if a competitor is using it to undercut you.</p><h3 id="research-archival-crawlers">Research / archival crawlers</h3><p>The Internet Archive&apos;s crawler, academic datasets, and preservation projects. Usually benign and often a public good. Common Crawl is the one that complicates the picture a little bit because it&apos;s a long-running open dataset used widely in research, but it&apos;s also been a common source of training data for language models. This means that site owners who want to limit AI training sometimes block its crawler (<a href="https://commoncrawl.org/ccbot">CCBot</a>) too, even though its purpose is broader than that.</p><h3 id="monitoring-bots">Monitoring bots</h3><p>Uptime checkers (Pingdom, UptimeRobot), performance monitors (Catchpoint, Datadog) and your own health checks. You want the ones you recognize, especially your own. However, those are usually low volume and mostly harmless.</p><h3 id="malicious-bots">Malicious bots</h3><p>Content thieves, hostile price and data scrapers, credential-stuffing, and brute-force bots, vulnerability scanners, spam bots, and inventory-hoarding scalpers. This is usually what you want to identify, and the only hard part is detecting them because they actively try to look like everything else on this list.</p><p>So the most important thing is that any of these can be faked**.** A request claiming to be Googlebot might be a scraper wearing a sheep&#x2019;s skin.</p><h2 id="how-to-diagnose-it-from-your-logs">How to diagnose it from your logs</h2><p>Everything below runs against standard access logs. Just adjust field positions for your log format. Also, all of these are artificially generated examples, so real logs will have a little bit more nuance.</p><h3 id="growth-or-lack-of-in-analytics-sessions-and-conversions">Growth (or lack of) in analytics sessions and conversions</h3><p>Before you dig into the logs, you can just look at your bandwidth compared to analytics sessions. For example:</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;"> </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;"> Last month </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;"> This month </th> <th style="padding:12px 14px; border-bottom:1px solid rgba(24, 61, 109, 0.18); color:white;"> Change </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;">Bandwidth</td> <td style="padding:12px 14px; border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">2.1 TB</td> <td style="padding:12px 14px; border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">3.8 TB</td> <td style="padding:12px 14px; vertical-align:middle; color:#183d6d;"><strong>+81%</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;">Analytics sessions</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;">48,200</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;">49,100</td> <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;"><strong>+2%</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;">Conversions</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;">1,840</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;">1,810</td> <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;"><strong>-2%</strong></td> </tr> </tbody> </table>
<!--kg-card-end: html-->
<p>Bandwidth is up 81% while sessions remain roughly flat. Most analytics tools run via JavaScript, and most bots don&apos;t execute JavaScript, so they&apos;re invisible to your analytics but still consume bandwidth.</p><h3 id="request-rate-per-client">Request rate per client</h3><p>Find out who&apos;s making the most requests:</p><pre><code>awk &apos;{print $1}&apos; access.log | sort | uniq -c | sort -rn | head -10
</code></pre><pre><code>  84213 47.128.44.19
  61887 47.128.44.20
  58122 47.128.44.21
   9043 66.249.66.1
   2287 81.150.12.4
    412 81.150.12.4
    389 92.40.177.22
    301 213.205.241.9
    288 51.171.38.4
    274 78.149.203.11
</code></pre><p>The top three IPs sit in one tight block, <code>47.128.44.x,</code> and each is making 50,000-80,000 requests, while your actual visitors trail off into the low hundreds. The 9,043 requests from <code>66.249.66.1</code> are from Googlebot (we&#x2019;ll go through its legitimacy later). The three at the top are a coordinated scrape from a single subnet.</p><p>One thing worth doing prior to actually eyeballing this is calculating the median requests per IP first, as this will give you a baseline for what traffic normally looks like on your website. You want to identify clients making 10 or 100 times the median.</p><h3 id="the-asset-loading-signature">The asset-loading signature</h3><p>This is one of the cleanest tells you have. When a real browser loads a page, it fetches the HTML and then everything the page references, such as CSS, JavaScript, fonts, and images. A scraper usually grabs only the HTML.</p><p>Pull everything requested by a suspect IP:</p><pre><code>grep &quot;^47.128.44.19&quot; access.log | awk &apos;{print $7}&apos; | sort | uniq -c | sort -rn | head -10
</code></pre><p>For the scraper, it&apos;s all pages, no assets:</p><pre><code>   2104 /products/12841
   2103 /products/12842
   2101 /products/12843
   2099 /products/12844
</code></pre><p>Now run the same command against a real visitor, and the shape is completely different:</p><pre><code>     14 /products/12841
      9 /css/main.a3f9.css
      9 /js/app.8c21.js
      7 /fonts/inter.woff2
     22 /images/product-12841-thumb.webp
      6 /api/cart/count
</code></pre><p>The real browser pulls the page plus its stylesheet, scripts, fonts, images, and a cart API call, so practically everything needed to actually render and use the page. The scraper pulls pages and nothing else. That ratio, HTML-only versus HTML-plus-assets, is hard to fake convincingly because faking it means doing real rendering work, which defeats the point of scraping cheaply.</p><h3 id="no-javascript-execution">No JavaScript execution</h3><p>This is closely related, but it&apos;s worth confirming separately. Most scrapers don&apos;t run JavaScript at all, so they never hit the endpoints your frontend fires after the page loads, such as analytics scripts, lazy-loaded API calls, and tracking pixels.</p><p>Count the client-side instrumentation hits from your suspect:</p><pre><code>grep &quot;^47.128.44.19&quot; access.log | grep -E &quot;/api/|/track|/analytics|/beacon&quot; | wc -l
</code></pre><p>For a scraper, this comes back as <code>0</code>. A real browser session fires these constantly, so anything above zero, often well into the dozens per session, is a strong signal of real browser activity.</p><h3 id="sequential-systematic-url-patterns">Sequential / systematic URL patterns</h3><p>Humans browse associatively. They follow what interests them, jump around, double back, and so on. Scrapers walk in straight lines.</p><p>Look at a suspect&apos;s requests in time order:</p><pre><code>grep &quot;^47.128.44.19&quot; access.log | awk &apos;{print $4, $7}&apos; | head -12
</code></pre><p>The scraper sweeps through IDs in perfect order, about two per second, with no pauses:</p><pre><code>[10:42:01] /products/12841
[10:42:01] /products/12842
[10:42:02] /products/12843
[10:42:02] /products/12844
[10:42:03] /products/12845
[10:42:03] /products/12846
[10:42:04] /products/12847
[10:42:04] /products/12848
[10:42:05] /products/12849
[10:42:05] /products/12850
[10:42:06] /products/12851
[10:42:06] /products/12852
</code></pre><p>A real visitor&apos;s path looks nothing like that:</p><pre><code>[10:41:55] /
[10:42:03] /products/winter-jacket
[10:42:31] /products/winter-jacket?color=navy
[10:42:58] /cart
[10:43:12] /products/wool-scarf
[10:44:40] /products/winter-jacket
</code></pre><p>Homepage, a product, a variant of that product, a different product, the cart, and then back to the first one, with irregular gaps of eight to forty seconds where a person was actually reading.</p><h3 id="user-agent-distribution">User-agent distribution</h3><p>Now look at what everything is claiming to be:</p><pre><code>awk -F&apos;&quot;&apos; &apos;{print $6}&apos; access.log | sort | uniq -c | sort -rn | head -12
</code></pre><pre><code> 204417 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36
 184992 python-requests/2.31.0
  61003 Mozilla/5.0 (compatible; GPTBot/1.2; +https://openai.com/gptbot)
  44781 Mozilla/5.0 (compatible; ClaudeBot/1.0; +mailto:claudebot@anthropic.com)
  29550 Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/)
  18204 Go-http-client/2.0
  12876 Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)
   9043 Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)
   3001 Scrapy/2.11 (+https://scrapy.org)
   2422 curl/8.4.0
    880 (empty)
</code></pre><p>Reading down the list:</p><ul><li>The 204k &quot;Chrome&quot; entries look human, but this line almost certainly hides spoofed bots because impersonating Chrome is the easiest disguise there is.</li><li>184k <code>python-requests</code><strong>.</strong> That&apos;s a scripting library, and having a Python HTTP client as the second-busiest &quot;user&quot; is a lot.</li><li>GPTBot, ClaudeBot, and AhrefsBot: bots that honestly declare themselves. You can decide what to do with each by name.</li><li><code>Go-http-client</code>, <code>Scrapy</code>, <code>curl</code>, and the blank entry are almost all automation. A lot of curl requests could be individual people looking to pull something from the terminal.</li></ul><p>The honest bots label themselves, and the lazy scrapers don&apos;t bother to hide. The tricky ones might be hiding inside that first &quot;Chrome&quot; line, which is why user agent header is just one of the things to check.</p><h3 id="verifying-identity-with-a-dns-lookup">Verifying identity with a DNS lookup</h3><p>When a request claims to be a known crawler, you can verify its identity. The standard method is a reverse-then-forward DNS check.</p><p>For a request claiming to be Googlebot:</p><pre><code>$ host 66.249.66.1
1.66.249.66.in-addr.arpa domain name pointer crawl-66-249-66-1.googlebot.com.

$ host crawl-66-249-66-1.googlebot.com
crawl-66-249-66-1.googlebot.com has address 66.249.66.1
</code></pre><p>The IP reverse-resolves to a <code>googlebot.com</code> hostname, and that hostname forward-resolves back to the same IP. The round trip matches, and the domain is correct, so it&#x2019;s a verified Googlebot.</p><p>Now here&apos;s a request that also claimed to be Googlebot, from one of our scraper IPs:</p><pre><code>$ host 47.128.44.19
19.44.128.47.in-addr.arpa domain name pointer ec2-47-128-44-19.ap-southeast-1.compute.amazonaws.com.
</code></pre><p>It claimed to be Googlebot, but the IP reverse-resolves to an AWS EC2 instance in Singapore, not <code>googlebot.com</code>. Google does not crawl from EC2 instances. The user-agent was identical to the real Googlebot&apos;s, but it&#x2019;s not actually Google&#x2019;s.</p><h3 id="published-ip-ranges">Published IP ranges</h3><p>Most major crawlers publish the IP ranges they operate from, which gives you a faster check than a DNS round trip at scale. OpenAI publishes theirs as a JSON file:</p><pre><code>$ curl -s &lt;https://openai.com/gptbot.json&gt; | head
{
  &quot;creationTime&quot;: &quot;2025-10-30T11:00:00.000000&quot;,
  &quot;prefixes&quot;: [
    {
      &quot;ipv4Prefix&quot;: &quot;132.196.86.0/24&quot;
    },
    {
      &quot;ipv4Prefix&quot;: &quot;172.182.202.0/25&quot;
    },
    {
      &quot;ipv4Prefix&quot;: &quot;172.182.204.0/24&quot;
    ...
  ]
}
</code></pre><p>If a request claims to be GPTBot but its source IP isn&apos;t in OpenAI&apos;s published ranges, then it isn&apos;t GPTBot. The same approach works for Anthropic, Perplexity, Microsoft, and Google. The only maintenance cost is keeping the lists current, since they change.</p><h3 id="asn-lookup">ASN lookup</h3><p>Look at where traffic originates by network, not just by individual IP:</p><pre><code>awk &apos;{print $1}&apos; access.log | sort -u | asn-lookup | sort | uniq -c | sort -rn | head
</code></pre><pre><code>  189442  AS16509  Amazon-AES
   72103  AS14061  DigitalOcean
   41996  AS24940  Hetzner
   38201  AS5089   Virgin Media
   31774  AS2856   BT
   22018  AS5607   Sky UK
</code></pre><p>The top three sources by volume are all data centers while real consumer audiences come from residential and mobile ISPs like Virgin, BT, and Sky. So if you are seeing the most requests from AWS and Hetzner, these are likely not real users. One important thing to mention is that legitimate crawlers like Googlebot also run from data centers, so this signal isn&apos;t conclusive on its own. However, you can identify legit crawlers in different ways.</p><h3 id="geographic-and-timing-anomalies">Geographic and timing anomalies</h3><p>Requests by country:</p><pre><code>  142883  SG
   38201  GB
   12009  US
    8841  DE
</code></pre><p>This one depends entirely on your situation, but the logic is simple: if you&apos;re a UK e-commerce store and your single largest source of traffic is Singapore, by a wide margin, you&apos;re almost certainly looking at bots rather than a sudden surge of overseas customers.</p><p>Timing tells the same kind of story. Here&apos;s a suspect&apos;s requests per hour across a day:</p><pre><code>00:00  &#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;  5,012
01:00  &#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;  4,998
02:00  &#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;  5,031
03:00  &#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;  4,987
...
13:00  &#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;  5,004
...
23:00  &#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;  5,019
</code></pre><p>An almost flat line with roughly 5,000 requests an hour at 3 a.m. and 1 p.m. alike. Human behavior has some daily rhythm that makes traffic quieter overnight, build through the morning, and reach its peak in the afternoon and evening. A perfectly flat 24-hour line is a machine crawling at a constant rate. Real human traffic would look more like this:</p><pre><code>00:00  &#x2588;&#x2588;&#x2588;&#x2588;  890
03:00  &#x2588;&#x2588;  410
08:00  &#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;  3,100
13:00  &#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;  5,200
20:00  &#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;  4,600
23:00  &#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;  2,000
</code></pre><h2 id="the-issue-with-this-approach">The issue with this approach</h2><p>Every check above happened after the fact, on traffic you&apos;ve already paid for. The scraper in these examples may have run 200,000+ requests before you grepped your way to it. IPs rotate, published ranges change, new crawlers appear, and the ones trying to hide adapt to whatever you&apos;re filtering on. So it&#x2019;s a lot of work, and most of it is reactive.</p><p>So the real goal here is to understand your traffic well enough to make deliberate calls: which bots you want, which you don&apos;t, and where to draw the line. Once you can see clearly what&apos;s hitting your site, you&apos;re in a position to do something about it.</p>]]></content:encoded></item><item><title><![CDATA[Build and deploy scripts at the edge with the bunny.net CLI]]></title><description><![CDATA[You can now write JS/TS scripts, develop and test them locally, deploy to our global network, manage environment variables, and step through deployment history, all from your terminal.]]></description><link>https://bunny.net/blog/build-and-deploy-scripts-at-the-edge-with-the-bunny-net-cli/</link><guid isPermaLink="false">6a291e8b160dc403fbfcf427</guid><category><![CDATA[News]]></category><dc:creator><![CDATA[Jamie Barton]]></dc:creator><pubDate>Wed, 10 Jun 2026 08:22:55 GMT</pubDate><media:content url="https://bunny.net/blog/content/images/2026/06/deploy-scripts-at-the-edge-with-the-bunny.net-CLI.png" medium="image"/><content:encoded><![CDATA[<img src="https://bunny.net/blog/content/images/2026/06/deploy-scripts-at-the-edge-with-the-bunny.net-CLI.png" alt="Build and deploy scripts at the edge with the bunny.net CLI"><p>A few weeks ago we <a href="https://bunny.net/blog/introducing-the-bunny-net-cli/">introduced the bunny.net CLI</a>, one command-line tool for managing your entire bunny.net stack. That first release shipped with full Database support, and we promised more would follow. </p><p>Today, <a href="https://docs.bunny.net/scripting"><strong>Edge Scripting</strong></a> lands in the CLI.</p><p>You can scaffold a new script from a template, develop and test it locally with Node or Deno, deploy a built bundle to the global network, manage environment variables, and step through deployment history from your terminal.</p><h2 id="a-two-minute-tour">A two-minute tour</h2><p>If you&apos;ve already got the CLI installed and you&apos;re logged in, three commands will get you to a deployed function:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny scripts init --name my-edge-app --type standalone --template <span style="color:#BDE052;">&quot;Return JSON&quot;</span> --deploy
<span style="color:#ECEC93;">cd</span> my-edge-app
<span style="color:#ECEC93;">npm</span> run build
bunny scripts deploy dist/index.js
</pre>
<br>
<!--kg-card-end: html-->
<p>If you haven&apos;t installed the CLI yet, do that first:</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
</pre>
<br>
<!--kg-card-end: html-->
<h2 id="scaffold-a-project-from-a-template">Scaffold a project from a template</h2>
<!--kg-card-begin: html-->
<p>
  <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny scripts init</code> creates a new edge script project on your machine. Run it with no flags and the CLI walks you through it interactively, asking you to pick a template, name the project, and choose whether to install dependencies:
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<div style="background-color:#1e1e1e; padding:1em; border-radius:6px; font-family:monospace; color:#ffffff;">

bunny scripts init

</div>
<br>
<!--kg-card-end: html-->
<p>You can also pass everything as flags, which is handy when you&apos;re in a hurry or when an agent is driving:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny scripts init \
  --name my-edge-app \
  --type standalone \
  --template <span style="color:#BDE052;">&quot;Return JSON&quot;</span>
</pre>
<br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  Add <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">--deploy</code> to also create the remote script on bunny.net and link the directory in the same step. Without it, <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">init</code> only scaffolds the local project, and you create the remote script later with <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny scripts create</code>.
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  The built-in templates are <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">Empty</code>, <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">Return JSON</code>, and <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">Simple Middleware</code>. Each one comes with a working project layout and sensible defaults so you can build and deploy straight away.
</p>
<!--kg-card-end: html-->
<p>You can also bring your own template from a Git repo:</p>
<!--kg-card-begin: html-->
<div style="background-color:#1e1e1e; padding:1em; border-radius:6px; font-family:monospace; color:#ffffff;">

bunny scripts init --name my-edge-app --repo user/my-template

</div>
<br>
<!--kg-card-end: html-->
<p>Custom templates always prompt before installing dependencies, since a template&apos;s install scripts run on your machine. Review the repo before you let it install.</p><h2 id="run-it-locally">Run it locally</h2><p>Edge Scripts are JavaScript and TypeScript projects built on Web-standard APIs. You develop them locally with your own editor and the debugging tools you already use.<br><br>The CLI auto-detects your package manager from the template&apos;s lockfile (bun, npm, pnpm, or yarn) and uses the template&apos;s own dev and build scripts:</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;">cd</span> my-edge-app
<span style="color:#ECEC93;">npm install</span>
<span style="color:#ECEC93;">npm</span> run dev
</pre>
<!--kg-card-end: html-->
<p>The CLI doesn&apos;t yet ship a custom runtime emulator. It wraps the tools you&apos;d reach for anyway, so local development stays fast and predictable.</p><h2 id="link-a-directory-to-a-script">Link a directory to a script</h2><p>Once you&apos;ve got something you want to deploy, connect the working directory to a script on bunny.net:</p>
<!--kg-card-begin: html-->
<div style="background-color:#1e1e1e; padding:1em; border-radius:6px; font-family:monospace; color:#ffffff;">

bunny scripts <span style="color:#ECEC93;">link</span>
</div>
<br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  Run it once, and it writes a small <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">.bunny/script.json</code> manifest into your project that records which script on bunny.net this directory belongs to. From that point on, every other <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">scripts</code> command knows what you&apos;re targeting:
</p>

<ul>
  <li><code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny scripts deploy</code> deploys this project</li>
  <li><code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny scripts env set API_KEY=...</code> writes to this script&apos;s environment</li>
  <li><code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny scripts deployments list</code> shows this script&apos;s history</li>
</ul>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  The script ID stays out of your shell history, and you don&apos;t pass it on every command. Run <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny scripts link</code> with no flags to choose from a list, or <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny scripts link --id 12345</code> to link directly.
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  The manifest holds per-developer state, so it shouldn&apos;t be committed. <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny scripts init</code> adds <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">.bunny/</code> to your <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">.gitignore</code> automatically. If you ran <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny scripts link</code> or <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny scripts create</code> in an existing repo, add <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">.bunny/</code> to <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">.gitignore</code> yourself, then anyone who clones the repo links their own script.
</p>
<!--kg-card-end: html-->
<p>For those deploying scripts in CI, you can skip the project linking by passing the Edge Script ID when you deploy:</p>
<!--kg-card-begin: html-->
<div style="background-color:#1e1e1e; padding:1em; border-radius:6px; font-family:monospace; color:#ffffff;">

bunny scripts deploy dist/index.js <span style="color:#D1949E;">12345</span>
</div>
<!--kg-card-end: html-->
<h2 id="build-and-deploy">Build and deploy</h2>
<!--kg-card-begin: html-->
<p>
  Edge Scripts deploy as a single built bundle. The CLI doesn&apos;t build for you, so run the template&apos;s own build step first, then point <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">deploy</code> at the output:
</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:#ECEC93;">npm</span> run build
bunny scripts deploy dist/index.js
</pre>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">deploy</code> uploads the file as a new release and publishes it live by default. On success it prints the script&apos;s live hostname:
</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;">
&#x2713; Published my-edge-app
  https://my-edge-app.bunny.run
</pre>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  If you want to upload a release without publishing it, stage it with <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">--skip-publish</code> and publish later.
</p>
<!--kg-card-end: html-->
<h2 id="put-it-on-your-own-domain">Put it on your own domain</h2>
<!--kg-card-begin: html-->
<p>
  Every script gets a <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny.run</code> URL, but you can also bring your own domain. We&apos;ve made it super easy with a new command:
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<div style="background-color:#1e1e1e; padding:1em; border-radius:6px; font-family:monospace; color:#ffffff;">

bunny scripts domains <span style="color:#ECEC93;">add</span> shop.example.com

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

<!--kg-card-begin: html-->
<p>
  <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny scripts domains add</code> sets up HTTPS on your own domain. bunny.net issues SSL certificates for free and can do so as soon as your DNS points to the network, so the command prints the exact CNAME record to create, along with the follow-up command to run. Once DNS has propagated, request your certificate:
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<div style="background-color:#1e1e1e; padding:1em; border-radius:6px; font-family:monospace; color:#ffffff;">

bunny scripts domains ssl shop.example.com

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

<!--kg-card-begin: html-->
<p>
  HTTPS is enforced by default, so anyone arriving over <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">http://</code> is redirected.
</p>
<!--kg-card-end: html-->
<p>List a script&apos;s domains any time to check their SSL status:</p>
<!--kg-card-begin: html-->
<div style="background-color:#1e1e1e; padding:1em; border-radius:6px; font-family:monospace; color:#ffffff;">

bunny scripts domains list

</div>
<!--kg-card-end: html-->
<h2 id="manage-environment-variables-and-secrets">Manage environment variables and secrets</h2><p>Most edge functions need configuration values like API tokens, feature flags, or region hints. The CLI handles two kinds. Plain <strong>variables</strong> are stored on bunny.net and can be read back. <strong>Secrets</strong> are encrypted, and their values can never be read back once set:</p>
<!--kg-card-begin: html-->
<pre style="background-color:#202020; padding:1em; border-radius:6px; font-family:monospace; overflow:auto; color:#FFFFFF;">
bunny scripts <span style="color:#ECEC93;">env set</span> DATABASE_URL <span style="color:#BDE052;">&quot;libsql://...&quot;</span>
bunny scripts <span style="color:#ECEC93;">env set</span> API_KEY <span style="color:#BDE052;">&quot;sk_...&quot;</span> --secret
bunny scripts <span style="color:#ECEC93;">env</span> list
bunny scripts <span style="color:#ECEC93;">env</span> remove API_KEY
</pre>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  Names are uppercased automatically, and a name can be a variable or a secret but not both. <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">env list</code> shows secret values as blank, since the API never returns them.
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  To pull plain variables into a local file for development, use <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">env pull</code>. It writes <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">NAME=VALUE</code> lines to <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">.bunny/.env</code>. Secrets are never included, so keep their source values somewhere safe:
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<div style="background-color:#1e1e1e; padding:1em; border-radius:6px; font-family:monospace; color:#ffffff;">

bunny scripts <span style="color:#ECEC93;">env</span> pull

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

<!--kg-card-begin: html-->
<p>
  Listing supports <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">--output json</code>, useful for diffing or syncing with another tool:
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<div style="background-color:#1e1e1e; padding:1em; border-radius:6px; font-family:monospace; color:#ffffff;">

bunny scripts <span style="color:#ECEC93;">env</span> list --output json | jq <span style="color:#BDE052;">&apos;.[].name&apos;</span>
</div>
<!--kg-card-end: html-->
<h2 id="deployment-history">Deployment history</h2><p>Every release is recorded, and the CLI gives you a view into that history:</p>
<!--kg-card-begin: html-->
<div style="background-color:#1e1e1e; padding:1em; border-radius:6px; font-family:monospace; color:#ffffff;">

bunny scripts deployments list

</div>
<br>
<!--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:#D1949E;">ID</span>       Status      Author   Released   Published
<span style="color:#D1949E;">14021</span>    &#x25CF; Live      jamie    2m ago     yes
<span style="color:#D1949E;">14008</span>    &#x25CB; Archived  jamie    1h ago     yes
<span style="color:#D1949E;">13990</span>    &#x25CB; Archived  jamie    1d ago     yes
</pre>
<!--kg-card-end: html-->
<p>The live release is marked with a filled dot, and archived releases are marked with an open one. If a release is live and the script has a linked pull zone, the hostname is printed at the end.<br><br>Need to roll back? You don&apos;t have to rebuild anything. The bundle for every past release is already on bunny.net, so grab the ID of a known-good one from that list and republish it:</p>
<!--kg-card-begin: html-->
<div style="background-color:#1e1e1e; padding:1em; border-radius:6px; font-family:monospace; color:#ffffff;">

bunny scripts deployments publish <span style="color:#D1949E;">13990</span>
</div>
<br>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  This flips which release is live without re-uploading anything, so a bad deploy can be undone in seconds. <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">deploy</code> ships new code. <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">publish</code> re-promotes a release that has already shipped.
</p>
<!--kg-card-end: html-->
<h2 id="see-how-its-doing">See how it&apos;s doing</h2>
<!--kg-card-begin: html-->
<p>
  Once a script is serving traffic, you&apos;ll want to know how much traffic it&apos;s handling and what it&apos;s costing. <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny scripts stats</code> pulls request, CPU, and cost totals for the script and draws a per-day bar chart of requests served right in your terminal:
</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 scripts stats

Key                   Value
Script                bunny-cards
Period                last <span style="color:#D1949E;">30</span> days
Total Requests        <span style="color:#D1949E;">1,455</span>
Total CPU             <span style="color:#D1949E;">43,336</span>ms
Avg CPU / Execution   <span style="color:#D1949E;">29.78</span>ms
Total Cost            <span style="color:#ECEC93;">$0.00</span>

Requests served
May <span style="color:#D1949E;">18</span>, <span style="color:#D1949E;">2026</span>  &#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;    <span style="color:#D1949E;">0</span>
May <span style="color:#D1949E;">19</span>, <span style="color:#D1949E;">2026</span>  &#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;  <span style="color:#D1949E;">504</span>
May <span style="color:#D1949E;">20</span>, <span style="color:#D1949E;">2026</span>  &#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;  <span style="color:#D1949E;">383</span>
May <span style="color:#D1949E;">21</span>, <span style="color:#D1949E;">2026</span>  &#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;    <span style="color:#D1949E;">0</span>
May <span style="color:#D1949E;">22</span>, <span style="color:#D1949E;">2026</span>  &#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;    <span style="color:#D1949E;">0</span>
May <span style="color:#D1949E;">23</span>, <span style="color:#D1949E;">2026</span>  &#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;    <span style="color:#D1949E;">0</span>
May <span style="color:#D1949E;">24</span>, <span style="color:#D1949E;">2026</span>  &#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;    <span style="color:#D1949E;">0</span>
May <span style="color:#D1949E;">25</span>, <span style="color:#D1949E;">2026</span>  &#x2588;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;    <span style="color:#D1949E;">4</span>
May <span style="color:#D1949E;">26</span>, <span style="color:#D1949E;">2026</span>  &#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;    <span style="color:#D1949E;">0</span>
May <span style="color:#D1949E;">27</span>, <span style="color:#D1949E;">2026</span>  &#x2588;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;    <span style="color:#D1949E;">5</span>
May <span style="color:#D1949E;">28</span>, <span style="color:#D1949E;">2026</span>  &#x2588;&#x2588;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;   <span style="color:#D1949E;">37</span>
May <span style="color:#D1949E;">29</span>, <span style="color:#D1949E;">2026</span>  &#x2588;&#x2588;&#x2588;&#x2588;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;   <span style="color:#D1949E;">90</span>
May <span style="color:#D1949E;">30</span>, <span style="color:#D1949E;">2026</span>  &#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;    <span style="color:#D1949E;">0</span>
May <span style="color:#D1949E;">31</span>, <span style="color:#D1949E;">2026</span>  &#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;    <span style="color:#D1949E;">0</span>
Jun <span style="color:#D1949E;">1</span>, <span style="color:#D1949E;">2026</span>   &#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;    <span style="color:#D1949E;">0</span>
Jun <span style="color:#D1949E;">2</span>, <span style="color:#D1949E;">2026</span>   &#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;    <span style="color:#D1949E;">0</span>
Jun <span style="color:#D1949E;">3</span>, <span style="color:#D1949E;">2026</span>   &#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;    <span style="color:#D1949E;">0</span>
Jun <span style="color:#D1949E;">4</span>, <span style="color:#D1949E;">2026</span>   &#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;  <span style="color:#D1949E;">223</span>
Jun <span style="color:#D1949E;">5</span>, <span style="color:#D1949E;">2026</span>   &#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;  <span style="color:#D1949E;">191</span>
Jun <span style="color:#D1949E;">6</span>, <span style="color:#D1949E;">2026</span>   &#x2588;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;   <span style="color:#D1949E;">11</span>
Jun <span style="color:#D1949E;">7</span>, <span style="color:#D1949E;">2026</span>   &#x2588;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;    <span style="color:#D1949E;">5</span>
Jun <span style="color:#D1949E;">8</span>, <span style="color:#D1949E;">2026</span>   &#x2588;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;&#x2591;    <span style="color:#D1949E;">2</span>
</pre>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  By default, it covers the last 30 days. Narrow the window with <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">--from</code> and <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">--to</code>, or switch to an hourly breakdown when you&apos;re chasing a spike:
</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 scripts stats --from 2026-05-01 --to 2026-05-31
bunny scripts stats --hourly
</pre>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  Like everything else, it uses your linked script when you don&apos;t pass an ID, and <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">--output json</code> gives you the data to pipe into whatever you use to track usage.
</p>
<!--kg-card-end: html-->
<h2 id="works-with-your-agent-and-ci">Works with your agent and CI</h2>
<!--kg-card-begin: html-->
<p>
  Prompts and spinners are TTY-aware, so they disappear in CI. Commands support <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">--output json</code>, and destructive commands like <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">delete</code> and <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">env remove</code> take <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">--force</code> so they don&apos;t block on a confirmation prompt in a pipeline.
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  In CI you can skip the global install and run the CLI through <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">npx</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;">
# .github/workflows/deploy.yml
- <span style="color:#ECEC93;">run:</span> npm ci
- <span style="color:#ECEC93;">run:</span> npm run build
- <span style="color:#ECEC93;">run:</span> npx @bunny.net/cli scripts deploy dist/index.js ${{ secrets.SCRIPT_ID }}
  <span style="color:#ECEC93;">env:</span>
    <span style="color:#ECEC93;">BUNNYNET_API_KEY:</span> ${{ secrets.BUNNYNET_API_KEY }}
</pre>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  When you scaffold with <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">bunny scripts init --deploy</code>, the command prints the <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">SCRIPT_ID</code> to add as a repo secret, so CI deploys to the right script without committing the link manifest.
</p>
<!--kg-card-end: html-->

<!--kg-card-begin: html-->
<p>
  AI coding assistants use the CLI directly, either from a global install or through <code style="background-color:#1e1e1e; color:#EB5757; padding:2px 6px; border-radius:4px;">npx @bunny.net/cli</code>. We ship Edge Scripting skills alongside it, so Claude Code, Cursor, Windsurf, or any agent that runs shell commands can scaffold a project, link it, set environment variables, and deploy.
</p>
<!--kg-card-end: html-->
<h2 id="whats-next">What&apos;s next</h2><p>Edge Scripting joins Bunny Database in the CLI today. Storage, Magic Containers, and the rest of the bunny.net stack are coming next.</p><p>Get started and create your first Edge Script with 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 scripts init
</pre>
<!--kg-card-end: html-->
<p>The CLI is open source and developed in public. If something&apos;s broken, <a href="https://github.com/BunnyWay/cli/issues" rel="noopener noreferrer">open an issue</a>. If you want to share what you&apos;ve built, find us in <a href="https://discord.com/invite/bunnynet" rel="noopener noreferrer">Discord</a>.</p>]]></content:encoded></item><item><title><![CDATA[Built on bunny.net - May 2026]]></title><description><![CDATA[Here's the first edition of a monthly roundup highlighting the projects, packages, and writeups we've spotted across the community]]></description><link>https://bunny.net/blog/built-on-bunny-net-may-2026/</link><guid isPermaLink="false">6a197100160dc403fbfcf3ac</guid><category><![CDATA[Tips and Tricks]]></category><dc:creator><![CDATA[Dino Kukic]]></dc:creator><pubDate>Fri, 29 May 2026 11:10:47 GMT</pubDate><media:content url="https://bunny.net/blog/content/images/2026/05/Built-on-bunny-May-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://bunny.net/blog/content/images/2026/05/Built-on-bunny-May-1.png" alt="Built on bunny.net - May 2026"><p>We pay attention to what people build on bunny.net. Some of it genuinely impresses us, and all of it deserves a shoutout. So, here&apos;s the first edition of a monthly roundup highlighting the projects, packages, and writeups we&apos;ve spotted across the community. If you&apos;re planning your own setup, there&#x2019;s plenty here worth exploring. It&#x2019;s also our way of saying thanks to the people building with bunny.net.</p><p>If you&apos;ve shipped, written, or open-sourced something, tag us or send us a message. We&apos;d love to take a look for next month&apos;s roundup.</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="kakuso"><a href="https://kaku.so/">kaku.so</a></h3><p>A statically rendered Japanese dictionary with 290k entries translated into 11 languages, plus built-in spaced repetition for practice. The scale of the project is genuinely impressive: more than 10 million files, 400 GB+ of data, and a 12-hour build process. The entire stack moved from Azure PostgreSQL to bunny.net after <a href="https://bunny.net/database/">Bunny Database</a> launched.</p><h3 id="yaanch"><a href="https://yaan.ch/">yaan.ch</a></h3><p>A drop-in replacement for hCaptcha, reCAPTCHA and Friendly Captcha. Invisible, GDPR-compliant, not proof-of-work. The marketing pages and backend run on <a href="https://bunny.net/storage/">Bunny Storage</a> and <a href="https://bunny.net/edge-scripting/">Edge Scripting</a>. The Rust anti-bot engine runs on <a href="https://bunny.net/magic-containers/">Magic Containers</a>, and the database will move to Bunny DB once it exits public preview.</p><h3 id="convex-self-hosted-on-magic-containers">Convex self-hosted on Magic Containers</h3><p><a href="https://www.linkedin.com/in/lipaonline/">Patrick Faust</a> got the Convex self-hosted runtime running on bunny.net Magic Containers and documented the process in two LinkedIn posts: <a href="https://www.linkedin.com/posts/lipaonline_big-milestone-unlocked-convex-just-approved-activity-7443538966836035584-C0h8/">one when it was approved</a>, and <a href="https://www.linkedin.com/posts/lipaonline_convex-self-hosted-is-live-on-bunny-magic-activity-7449773848343846913-Wn6H/">another when it went live</a>.</p><h3 id="nitrofilm"><a href="https://nitro.film/">Nitro.film</a></h3><p>Apple TV app built by a solo founder on <a href="https://bunny.net/stream/">Bunny Stream</a>, with much of the development process <a href="https://www.linkedin.com/posts/harrylang_buildinpublic-solofounder-claudecode-activity-7450683849157795841-ozKS">shared publicly on LinkedIn</a> as part of a &#x2018;build in public journey&#x2019;.</p><h3 id="kraken">Kraken</h3><p><a href="https://www.linkedin.com/in/robin-mb/" rel="noreferrer">Robin Dost</a> has been building <a href="https://kraken.malwarebox.eu/">Kraken</a>, a CTI platform that tracks adversary infrastructure, including domains, IPs, and dead drops, over time so analysts don&#x2019;t lose visibility after initial discovery. It&#x2019;s currently running live in evaluation and hosted in Europe on bunny.net.</p><h3 id="the-imago-platform">The Imago Platform</h3><p><a href="https://www.linkedin.com/in/helander/">Magnus Helander</a> is building the <a href="https://www.imagoplatform.ai/">Imago Platform</a> on <a href="https://bunny.net/cdn/">Bunny CDN</a>, sitting at <a href="https://www.linkedin.com/posts/helander_buildingthe-imago-platformwe-could-have-activity-7450298367915618304-hlKR/">99.995% uptime since launch</a>. He also built and shipped a Claude skill for writing Edge Scripts on bunny.net.</p><h3 id="moving-to-bunnynet">Moving to bunny.net</h3><p>Three writeups from the past month, all worth reading if you&apos;re thinking about your own setup.</p><ul><li><strong>Johanna Larsson</strong> - <a href="https://jola.dev/posts/dropping-cloudflare" rel="noreferrer">moved a Phoenix/Elixir blog to bunny.net</a> with a full walkthrough and code examples.</li><li><strong>Alec Armbruster</strong> - built a <a href="https://alec.is/posts/cloudflare-tunnels-on-bunny-net/">tunnels-style setup running on bunny.net</a>.</li><li><strong>David Drugeon-Hamon</strong> - documented a <a href="https://david.drugeon-hamon.bzh/blog/2026/04/migration-github-codeberg/">GitHub-to-Codeberg switch with bunny.net in the stack</a>.</li></ul><h2 id="shipped-for-bunnynet">Shipped for bunny.net</h2><h3 id="cdn"><strong>CDN</strong></h3><p>A community-built API client I recently came across:</p><ul><li><a href="https://pypi.org/project/bunny-cdn-sdk/"><code>bunny-cdn-sdk</code></a> - typed Python SDK and CLI covering Bunny CDN and Bunny Storage.</li></ul><h3 id="stream-and-video">Stream and video</h3><ul><li><a href="https://www.npmjs.com/package/bunnycdn-stream"><code>bunnycdn-stream</code></a> - TypeScript library for the Bunny Stream API.</li><li><a href="https://www.npmjs.com/package/playstack"><code>playstack</code></a> - React video player that supports Bunny Stream.</li><li><a href="https://www.npmjs.com/package/@cliff-studio/sanity-plugin-bunny-input"><code>sanity-plugin-bunny-input</code></a> - input component for Sanity Studio.</li></ul><h3 id="storage">Storage</h3><ul><li><a href="https://www.npmjs.com/package/@seshuk/payload-storage-bunny"><code>payload-storage-bunny</code></a> - Payload CMS adapter for Bunny Storage and Bunny Stream with auto-purging and resumable uploads via tus.</li><li><a href="https://www.npmjs.com/package/bunny-transfer"><code>bunny-transfer</code></a> - rsync-style CLI for moving files in and out of storage zones.</li><li><a href="https://www.npmjs.com/package/upload-to-bunny"><code>upload-to-bunny</code></a> - directory uploader aimed at CI pipelines.</li></ul><h3 id="shield-and-security">Shield and security</h3><ul><li><a href="https://pypi.org/project/octorules-bunny/"><code>octorules-bunny</code></a> - <a href="https://bunny.net/shield/">Shield</a> WAF rules as code, built and used in production by Doctena, an EU healthcare scheduling company.</li></ul><h3 id="working-with-ai-agents">Working with AI agents</h3><ul><li><a href="https://www.npmjs.com/package/bunnycdn-mcp"><code>bunnycdn-mcp</code></a> - MCP server for bunny.net.</li><li><code>bunny-edge</code> Claude plugin - Magnus Helander&#x2019;s Claude skill for writing Edge Scripts on bunny.net. Install with <code>/plugin install bunny-edge@mheland</code>.</li></ul><p>That&#x2019;s it for May. 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. Join our <a href="https://discord.com/invite/bunnynet?ref=bunny.net">Discord here</a>.</p>]]></content:encoded></item><item><title><![CDATA[How we sped up deploys, updates, and undeploys in Magic Containers]]></title><description><![CDATA[We reduced deploy and update latency from tens of seconds to under five by combining event-driven acceleration with control-loop reliability.]]></description><link>https://bunny.net/blog/how-we-sped-up-deploys-updates-and-undeploys-in-magic-containers/</link><guid isPermaLink="false">6a182c5d160dc403fbfcf38c</guid><category><![CDATA[Compute]]></category><dc:creator><![CDATA[Anton Zvonko Gazvoda]]></dc:creator><pubDate>Thu, 28 May 2026 12:02:48 GMT</pubDate><media:content url="https://bunny.net/blog/content/images/2026/05/Faster-Deploys--Undeploys--and-Updates-for-Magic-Containers.png" medium="image"/><content:encoded><![CDATA[<img src="https://bunny.net/blog/content/images/2026/05/Faster-Deploys--Undeploys--and-Updates-for-Magic-Containers.png" alt="How we sped up deploys, updates, and undeploys in Magic Containers"><p>Magic Containers became programmable with the introduction of the Public API. The next step was making it react in real time.</p><p>In practice, operations like deploys and updates could take tens of seconds to complete. Not because anything was failing, but because of how control loops work.</p><p>Each part of the system waits for the next reconciliation cycle, and those delays stack up across components.</p><p>As workloads become more short-lived and automated, lifecycle speed becomes an increasingly important part of the developer experience. Spinning up a container, scaling it, or tearing it down should feel immediate.</p><p>This is the problem we set out to solve.</p><h2 id="the-problem-control-loops-introduce-latency">The problem: control loops introduce latency</h2><p>Magic Containers is built around a&#xA0;<strong>control loop architecture</strong>.</p><p>Each component continuously reconciles&#xA0;<em>desired state &#x2192; actual state</em>:</p><ul><li><strong>Application Provisioner</strong>&#xA0;&#x2192; selects regions for deployment</li><li><strong>Controller Manager</strong>&#xA0;&#x2192; ensures the desired number of replicas</li><li><strong>Scheduler</strong>&#xA0;&#x2192; assigns pods to nodes</li><li><strong>Local Container Manager (LCM)</strong>&#xA0;&#x2192; ensures containers run on the assigned node</li></ul><p>Each of these components runs independently in a loop:</p><pre><code class="language-python">while&#xA0;True:
	observe_state()
  diff&#xA0;=&#xA0;desired_state&#xA0;-&#xA0;actual_state
  if&#xA0;diff:
	  reconcile(diff)
  sleep(interval)
</code></pre><p>This model is extremely reliable and forms the backbone of many distributed systems, including Kubernetes.</p><p>But it comes with a tradeoff.</p><p><strong>Latency compounds across loops.</strong></p><p>Each loop runs on an interval of&#xA0;<strong>~5 to 10 seconds</strong>.</p><p>That means a single operation doesn&#x2019;t execute immediately. Instead, it waits for the next loop iteration.</p><p>Now chain multiple components together:</p><pre><code class="language-python">User action
	&#x2192; waits for Provisioner loop (up to 10s)
	&#x2192; waits for Controller loop (up to 10s)
	&#x2192; waits for Scheduler loop (up to 10s)
  &#x2192; waits for LCM loop (up to 10s)
</code></pre><p>In the worst case, this stacks up to&#xA0;<strong>tens of seconds before a container is fully running</strong>.</p><p>Even under typical conditions, deploys and updates felt noticeably delayed.</p><p>Nothing was technically incorrect. The system always converged to the correct state, but the&#xA0;<em>experience</em>&#xA0;lagged behind what modern workflows expect.</p><h3 id="what-we-considered-and-rejected">What we considered (and rejected)</h3><p>We explored several approaches before settling on the final solution.</p><p><strong>1. Decreasing loop intervals</strong></p><p>Reducing intervals from ~10 seconds to sub-second.</p><p><strong>Why we rejected it:</strong></p><ul><li>Significant increase in CPU usage across all control plane components</li><li>Higher pressure on state storage and coordination systems</li><li>Still fundamentally polling-based, meaning latency never reaches zero</li></ul><p><strong>2. Removing loops entirely</strong></p><p>Moving to a purely event-driven system.</p><p><strong>Why we rejected it:</strong></p><ul><li>Loops are critical as a&#xA0;<strong>safety mechanism</strong></li><li>They continuously verify and correct drift between desired and actual state</li><li>Without them, missed events could lead to permanent inconsistencies</li></ul><p><strong>3. Hybrid model (chosen)</strong></p><p>Keep loops for correctness and safety, but introduce&#xA0;<strong>events for immediacy</strong>.</p><h2 id="the-solution-event-driven-acceleration">The solution: event-driven acceleration</h2><p>We introduced a&#xA0;<strong>message broker with event queues</strong>&#xA0;between components.</p><p>The key idea: <em>Loops ensure consistency. Events provide speed.</em></p><p>Instead of waiting for the next loop iteration, components now react immediately when something changes.</p><p><strong>Before: loop-driven propagation</strong></p><pre><code class="language-python">[User Action]
&#x2193;
(wait for Provisioner loop)
&#x2193;
(wait for Controller loop)
&#x2193;
(wait for Scheduler loop)
&#x2193;
(wait for LCM loop)
&#x2193;
[Container Running]
</code></pre><p><strong>After: event-accelerated flow</strong></p><pre><code class="language-python">[User Action]
&#x2193;
[Event: Application Created] &#x2192; Queue
&#x2193;
[Provisioner triggered immediately]
&#x2193;
[Event: Provisioning Complete]
&#x2193;
[Controller Manager triggered]
&#x2193;
[Event: Pods Created]
&#x2193;
[Scheduler triggered]
&#x2193;
[Event: Pod Scheduled]
&#x2193;
[LCM triggered]
&#x2193;
[Container Running]
</code></pre><h3 id="how-it-works">How It Works</h3><p>Each component now listens for specific events and reacts instantly.</p><p><strong>Example event</strong></p><pre><code class="language-json">{
    &quot;type&quot;: &quot;application.created&quot;,
    &quot;app_id&quot;: &quot;app_123&quot;,
    &quot;regions&quot;: [&quot;eu-central&quot;, &quot;us-east&quot;],
    &quot;timestamp&quot;: 1713949200
}
</code></pre><p><strong>Provisioner</strong></p><pre><code class="language-go">func handleApplicationCreated(event Event) {
    regions := selectRegions(event)
    publish(&quot;provisioning.completed&quot;, regions)
}
</code></pre><p><strong>Controller Manager</strong></p><pre><code class="language-go">func handleProvisioningComplete(event Event) {
    createReplicas(event.app_id, desiredReplicas)
    publish(&quot;replicas.created&quot;, event.app_id)
}
</code></pre><p><strong>Scheduler</strong></p><pre><code class="language-go">func handleReplicasCreated(event Event) {
    node := selectNode(event)
    publish(&quot;pod.scheduled&quot;, node)
}
</code></pre><p><strong>Local Container Manager (LCM)</strong></p><pre><code class="language-go">func&#xA0;handlePodScheduled(event&#xA0;Event) {  
		startContainer(event.node,&#xA0;event.pod)
}
</code></pre><h3 id="important-loops-still-exist">Important: loops still exist</h3><p>The control loops were&#xA0;<strong>not removed</strong>.</p><p>They still run continuously to:</p><ul><li>Detect drift (e.g., crashed containers or missing replicas)</li><li>Reconcile inconsistencies</li><li>Act as a fallback if events are delayed or lost</li></ul><pre><code class="language-go">// simplified reconciliation loop
for {
    diff := computeDiff(desiredState, actualState)
    if diff != nil {
        reconcile(diff)
    }
    sleep(5 * time.Second)
}
</code></pre><p>This hybrid design gives us:</p><ul><li><strong>Fast reaction time (events)</strong></li><li><strong>Strong consistency guarantees (loops)</strong></li></ul><p>These events don&#x2019;t just drive internal components, they also power real-time updates across the platform, including the Dashboard.</p><h2 id="from-control-plane-to-user-experience">From control plane to user experience</h2><p>Reducing backend latency is only part of the story.</p><p>Before this change, even when operations completed, the <strong>Dashboard still relied on polling</strong>&#xA0;to fetch updates. This introduced an additional delay between something happening in the system and the user actually seeing it.</p><p>In practice, this meant:</p><ul><li>Deploy finishes &#x2192; UI updates a few seconds later</li><li>Scaling event happens &#x2192; user sees it after the next refresh cycle</li></ul><p>To solve this, we extended the same event-driven model all the way to the frontend.</p><h3 id="real-time-updates-via-websockets">Real-time updates via WebSockets</h3><p>We introduced&#xA0;<strong>WebSocket-based event streaming</strong>&#xA0;between the control plane and the Dashboard.</p><p>Instead of polling for state changes, the UI now subscribes to live updates:</p><pre><code class="language-python">Client &#x2192; opens WebSocket connection
&#x2192; subscribes to application events
</code></pre><p>Whenever something changes:</p><pre><code class="language-python">[Control Plane Event]
&#x2193;
[Message Broker]
&#x2193;
[WebSocket Gateway]
&#x2193;
[Dashboard UI updates instantly]
</code></pre><h3 id="what-this-changes">What this changes</h3><p>This removes the final layer of perceived latency.</p><h4 id="before">Before</h4><ul><li>Backend finishes &#x2192; UI polls &#x2192; user sees update later</li></ul><h4 id="after">After</h4><ul><li>Backend finishes &#x2192; event emitted &#x2192; UI updates instantly</li></ul><h4 id="result">Result</h4><ul><li>Deploy progress updates feel&#xA0;<strong>real time</strong></li><li>Scaling actions are visible&#xA0;<strong>immediately</strong></li><li>State transitions (creating &#x2192; running &#x2192; scaling) feel&#xA0;<strong>continuous</strong></li></ul><h3 id="why-this-matters">Why this matters</h3><p>Without this step, the platform would be technically fast but still&#xA0;<em>feel slow</em>.</p><p>By pushing events all the way to the UI, we aligned:</p><ul><li><strong>System speed</strong></li><li><strong>User perception</strong></li></ul><h2 id="the-impact">The impact</h2><p>By eliminating waiting between steps, we removed the largest source of latency.</p><p><strong>Before vs. after</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;"> Operation </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;"> Before (loop-driven) </th> <th style="padding:12px 14px; border-bottom:1px solid rgba(24, 61, 109, 0.18); color:white;"> After (event-driven) </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;">Deploy</td> <td style="padding:12px 14px; border-right:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;">10&#x2013;40s</td> <td style="padding:12px 14px; vertical-align:middle; color:#183d6d;"><strong>&lt; 5s</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;">Update</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;">10&#x2013;40s</td> <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;"><strong>&lt; 4s</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;">Undeploy</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;">~60s+</td> <td style="padding:12px 14px; border-top:1px solid rgba(24, 61, 109, 0.18); vertical-align:middle; color:#183d6d;"><strong>~60s (grace period)</strong></td> </tr> </tbody> </table> <br>
<!--kg-card-end: html-->
<h3 id="what-changed-technically">What changed technically</h3><ul><li>Removed dependency on loop timing for forward progress</li><li>Reduced end-to-end latency by&#xA0;<strong>an order of magnitude</strong></li><li>Maintained correctness via continuous reconciliation</li></ul><h3 id="why-this-matters-1">Why this matters</h3><p>This fundamentally changes how Magic Containers behaves.</p><ul><li><strong>CI/CD pipelines speed up.</strong> Infrastructure is no longer the slowest step</li><li><strong>Ephemeral workloads become practical.</strong> Create &#x2192; run &#x2192; destroy flows now complete in seconds</li><li><strong>Event-driven systems feel natural.</strong> Infrastructure now reacts at the same speed as application logic</li></ul><h3 id="tradeoffs-and-challenges">Tradeoffs and challenges</h3><p><strong>1. Event ordering</strong></p><p>Ensuring correct sequencing across distributed components.</p><p><strong>Solution:</strong></p><ul><li>Idempotent handlers</li></ul><p><strong>2. Reliability</strong></p><p>Events can fail or be delayed.</p><p><strong>Solution:</strong></p><ul><li>Retry mechanisms</li><li>Dead-letter queues</li><li>Control loops as fallback</li></ul><p><strong>3. Observability</strong></p><p>Async systems are harder to debug.</p><p><strong>Solution:</strong></p><ul><li>Correlation IDs</li><li>Event tracing across components</li></ul><h2 id="what%E2%80%99s-next">What&#x2019;s next</h2><p>We&#x2019;re already exploring:</p><ul><li>Optimizing image download times to reduce startup time</li><li>Automated build and updates directly from your GitHub repository</li><li>Access to recent log history alongside live logs for easier troubleshooting</li></ul><h2 id="final-thoughts">Final thoughts</h2><p>Magic Containers started as a loop-driven control plane designed for correctness.</p><p>With the introduction of event-driven acceleration, it now reacts immediately to changes, without relying on the next reconciliation cycle.</p><p>The result is a system that converges just as reliably, but gets there much faster.</p>]]></content:encoded></item><item><title><![CDATA[STACKIT and bunny.net partner to offer fast, private, regulation-ready content delivery]]></title><description><![CDATA[bunny.net teamed up with STACKIT, the cloud provider of Schwarz Digits, to deliver a high-performance, regulation-ready, EU-sovereign content delivery network (CDN) and edge security ecosystem built specifically for European businesses.]]></description><link>https://bunny.net/blog/stackit-and-bunny-net-partner-to-offer-fast-private-regulation-ready-content-delivery/</link><guid isPermaLink="false">6a168a5e160dc403fbfcf374</guid><category><![CDATA[News]]></category><category><![CDATA[Privacy]]></category><dc:creator><![CDATA[Graeme Inglis]]></dc:creator><pubDate>Wed, 27 May 2026 06:17:07 GMT</pubDate><media:content url="https://bunny.net/blog/content/images/2026/05/Stackit-and-bunny.net-Partnership-blog.png" medium="image"/><content:encoded><![CDATA[<img src="https://bunny.net/blog/content/images/2026/05/Stackit-and-bunny.net-Partnership-blog.png" alt="STACKIT and bunny.net partner to offer fast, private, regulation-ready content delivery"><p>European infrastructure teams are facing a massive challenge.</p><p>With regulations like GDPR, NIS2, and the EU Data Act tightening across the continent, compliance is no longer a checkbox. It dictates your entire architecture. Historically, the industry narrative held that guaranteeing data sovereignty meant sacrificing raw edge performance.</p><p>We weren&#x2019;t willing to trade performance for sovereignty. Now, you don&#x2019;t have to either.</p><p>bunny.net teamed up with STACKIT, the cloud provider of Schwarz Digits, to deliver a high-performance, regulation-ready, EU-sovereign content delivery network (CDN) and edge security ecosystem built specifically for European businesses.</p><h2 id="uncompromising-performance-under-european-law">Uncompromising performance under European law</h2><p>This partnership brings together two teams obsessed with digital independence. If you aren&apos;t familiar with STACKIT, it is the cloud backbone of Schwarz Digits, the IT organization behind Schwarz Group. Operating out of its German headquarters, it has built an enterprise-grade cloud ecosystem focused entirely on data privacy and sovereign control.</p><p>By combining STACKIT&#x2019;s cloud infrastructure with bunny.net&#x2019;s global edge network, we are launching an integrated ecosystem that meets the needs of both your legal team and your DevOps leads.</p><p>When you route your traffic through STACKIT CDN, your data remains fully within EU jurisdiction. No hidden tracking cookies, no data monetization, and no third-party analytics processing your users&apos; information.</p><h2 id="engineering-edge-security-and-scale">Engineering edge security and scale</h2><p>We built this ecosystem to handle demanding, enterprise-scale workloads optimized for compliance.</p><p>Here is what this sovereign architecture delivers out of the box:</p><ul><li><strong>Global edge performance:</strong> Connect your STACKIT-hosted applications directly to a global edge network backed by 250 Tbps+ of backbone capacity, ensuring consistent sub-30 ms latency across Europe and beyond.</li><li><strong>Integrated edge protection:</strong> Mitigate risks before traffic hits your infrastructure. The platform injects robust security tools right at the edge, including a web application firewall (WAF), Distributed Denial-of-Service (DDoS) mitigation, and global rate limiting.</li><li><strong>Transparent cost infrastructure:</strong> Scale your application without worrying about complex pricing tiers or vendor lock-in. Keep track of your carrots with a predictable, transparent, pay-as-you-go cost model.</li></ul><h2 id="building-an-independent-digital-future">Building an independent digital future</h2><p>Your company&#x2019;s growth shouldn&#x2019;t be punished for protecting user privacy. By anchoring your traffic in a trust-first ecosystem like Stackit CDN, you can optimize your delivery pipeline, satisfy strict regulatory requirements, and maintain absolute control over your data.</p><p>Ready to see how the new sovereign edge can transform your stack? Hop on over to the <a href="https://stackit.com/en/products/network/stackit-cdn?_gl=1*1dqzp6s*_up*MQ">STACKIT platform</a> to learn more, or connect with our team to find out how to hop ahead securely.</p>]]></content:encoded></item></channel></rss>