# Abuse Procedure Source: https://bunny.net/docs/account/abuse-procedure How bunny.net handles abuse reports for content hosted or delivered through our platform. bunny.net is a platform service provider acting as a passive distributor of third-party information. We are not a publisher and do not moderate content except as set out in our published procedures (updated from time to time). bunny.net takes no responsibility to our Customers or users for data and content uploaded by Customers or third parties, and is not liable for that content. We take abuse reports seriously while giving customers a fair opportunity to review and address complaints. This procedure outlines how we handle general abuse reports, including how we verify complaints and how customers can respond. We operate on a good-faith, voluntary basis, aiming to meet industry best practice while taking into account relevant national and international regulatory guidance and applicable legislation. We have no obligation to respond to allegations of abuse or to take action, and we are not liable for our acts or omissions other than as set out in our [Terms of Service](https://bunny.net/tos). We reserve the right to amend or withdraw this procedure at any time. ## What is abuse? We consider the following to be abuse, or complaints of alleged abuse: * Misinformation (sometimes known as "fake news") * Violation of our Terms of Service (such as publishing illegal content, or harmful activity such as hacking, phishing, or malware) * Violent threats and harassment * Child Sexual Abuse Material (CSAM) Misinformation may be described as verifiably false or misleading information created, presented, and disseminated for economic gain or to intentionally deceive the public, and which may cause public harm. We do **not** consider the following to be abuse: * Disagreements over political or news articles * Attempts to silence commentary or alternative points of view * Defamatory, aggressive, or insulting content * Reviews of service providers or products ## Our process Reports can be submitted via our [abuse form](https://bunny.net/abuse/). ### Receiving and reviewing the report When we receive an abuse report related to content hosted or delivered through our platform, our team reviews it to determine: * Whether the allegation is valid and specific (vague or generic complaints may require further clarification) * Whether the content violates applicable laws or our Acceptable Use Policy We do not take immediate action unless the content is clearly illegal (such as phishing, malware, or CSAM). For all other cases, we first notify the customer and allow them to review the allegations. ### Notifying the customer Once we verify the report, we notify the customer responsible for the content by email, and the report is visible in the customer dashboard. The notice includes: * A summary of the allegation, including the details provided by the reporter * A request to review the report and take action if necessary * A response timeline, typically 48 hours for all abuse reports except those related to misinformation Until the response period has passed, no action is taken on the content. We do not, as a matter of routine, provide the customer with personal data relating to the complainant. ### Customer response options * **Address the report:** Modify or remove the content, or provide additional information to clarify the situation. * **Dispute the report:** Explain why the content should not be removed, and share any supporting documentation (such as licenses or legal permissions). We review the response and, if necessary, work with the customer to ensure compliance with applicable regulations or our Acceptable Use Policy. ### No immediate suspension or blocking Unlike DMCA or CSAM reports, we do not suspend Pull Zones or remove content for general abuse allegations. Our goal is to work with the customer to verify and resolve the issue. However, if a customer fails to respond in time, or the content clearly violates our policies, we may take further action. ### Repeat offenses If a customer repeatedly receives abuse reports and does not take appropriate action, we may require faster response times for future reports, restrict access to certain services or features, or suspend or terminate the account. We always attempt to work with the customer first before escalating. ## Frequently asked questions **Will my Pull Zone or content be suspended for fake news or misinformation?** No. We do not suspend Pull Zones or remove content solely based on abuse reports alleging fake news or misinformation. **Will my Pull Zone or content be suspended immediately?** No. We do not suspend Pull Zones or remove content immediately for general abuse reports. We give customers an opportunity to review and address the complaint first. **What happens if I don't respond to an abuse report?** If no response is received within the given timeframe, we may take further action, including potential suspension of the specific URL. This depends on the type of abuse report. **Can I dispute an abuse report?** Yes. If you believe a report is incorrect or invalid, submit a response explaining why, along with supporting evidence. bunny.net will review the response and decide, at its own discretion and with no liability, whether to uphold the allegation or reinstate the content in whole or in part. **What types of abuse reports require immediate action?** Reports related to illegal activity (such as CSAM, phishing, or malware) may require immediate content blocking and further escalation. All other reports follow this procedure. # Account Disabled or Suspended Source: https://bunny.net/docs/account/account-suspended Why a bunny.net account becomes disabled or suspended, and how to resolve each state. If you see a message that your account is disabled or suspended, this guide explains the possible reasons and how to resolve them. There are two distinct states. ## Temporarily disabled This is usually a temporary status that you can resolve by addressing the underlying issue. **Common causes:** * **Trial expired:** Trial accounts are automatically disabled when the trial period ends, unless you top up your account. This prevents unexpected charges. * **Negative balance:** If charges accrue without successful payment for several days, the account may be disabled until the outstanding balance is cleared. **Resolution:** 1. Log in to your account. You'll be guided through recharging or resolving your balance. 2. If you need help, [contact support](https://bunny.net/contact/). ## Suspended A suspension is more serious. It can result from automated risk detection or a manual review following a violation of our [Terms of Service](https://bunny.net/tos) or [Acceptable Use Policy](https://bunny.net/acceptable-use/). **Common causes:** * **Signup risk detected**, based on signals such as: * Use of VPNs, proxies, or anonymized IP addresses during signup * Mismatches between IP address and billing information * Registrations from regions flagged as high-risk by our payment processor * **Policy violations**, such as: * Attempted fraud or abuse of platform resources * Hosting prohibited content or engaging in restricted activities **Resolution:** 1. [Contact support](https://bunny.net/contact/) to request details about the suspension. 2. Be prepared to provide identification, account verification, or additional context to assist the review. Depending on the nature and severity of the issue, some accounts suspended for policy violations may not be reinstated. Accounts created using anonymizing services such as VPNs or proxies may be permanently restricted. If your account was suspended under these conditions, you may need to create a new account using a different email address and a direct, non-anonymized connection. ## Need help? If you're unsure which state your account is in or what to do next, [contact support](https://bunny.net/contact/). We can help review your account status and determine the right resolution. # API Keys Source: https://bunny.net/docs/account/api-keys Manage your bunny.net API key for programmatic access. Your API key lets you programmatically update your zones and account settings via the [bunny.net API](/docs/api-reference). You can find your API key at [Account > API Key](https://dash.bunny.net/account/api-key). API key ## Viewing your API key Your API key is hidden by default. Click the eye icon to reveal it, or the copy icon to copy it to your clipboard. Reveal API key ## Regenerating your API key If your API key is compromised, click the refresh icon to generate a new one. You'll be asked to confirm before the key is reset. Reset API key Resetting your API key will disable the old key immediately. Any applications or scripts using the old key will stop working. ## Keep your API key safe Your API key has full access to your account. Never share it publicly or commit it to version control. This is your account-level API key. Storage zones, Stream libraries, and databases each have their own separate API keys for accessing those specific services. # Appearance Source: https://bunny.net/docs/account/appearance Choose between light and dark mode for the bunny.net dashboard. You can customize the appearance of the bunny.net dashboard by choosing between light and dark mode. ## Change your theme Navigate to [Account Settings](https://dash.bunny.net/account/settings) in your dashboard. Scroll to the **Appearance** section. Click the light or dark mode icon to switch themes. Theme selection Your theme preference is saved automatically and applies across all your dashboard sessions. # Change Password Source: https://bunny.net/docs/account/change-password Update your bunny.net account password. You can change your password at any time from the Account Settings page. ## Change your password Navigate to [Account Settings](https://dash.bunny.net/account/settings) and click **Change Password**, or go directly to the [Change Password](https://dash.bunny.net/account/settings/changepassword) page. Change password Enter your current password, then enter and confirm your new password. Your password must contain: * At least 12 characters * One special character * One lowercase letter * One uppercase letter * One number Click **Save** to update your password. For additional account security, consider enabling [two-factor authentication](/docs/account/two-factor-authentication). ## Forgot your password? If you've forgotten your password and can't log in, use the **Forgot Password** link on the [login page](https://dash.bunny.net) to reset it via email. # Close Account Source: https://bunny.net/docs/account/close-account How to close your bunny.net account. If you'd like to stop using bunny.net, you can close your account from the [Account Settings](https://dash.bunny.net/account/settings) page, or go directly to the [Close Account](https://dash.bunny.net/account/settings/close) page. Thinking about leaving? We'd love to hear from you first. We actively listen to feedback and build what our users need to scale. If something's missing for you, there's a good chance we're already working on it. [Get in touch](https://bunny.net/contact) before you go. ## Before closing your account Closing your account is permanent and cannot be reversed. All your data will be deleted and your account balance cannot be refunded. Before proceeding: * Delete all your Pull Zones and Storage Zones * Download any files you want to keep * Export any data or configurations you may need ## Close your account Navigate to [Account Settings](https://dash.bunny.net/account/settings) and click **Close My Account**, or go directly to the [Close Account](https://dash.bunny.net/account/settings/close) page. Close account section Select a reason for closing your account. This feedback helps us improve the service. Click **Close My Account Forever** to complete the account closure. # Data Processing Agreement Source: https://bunny.net/docs/account/data-processing-agreement View and accept the GDPR data processing agreement. The [Data Processing Agreement](https://dash.bunny.net/account/dpa) page lets you view, accept, and download the GDPR data processing agreement for your account. ## Accepting the agreement The agreement is pre-filled with your account details. Review the terms and click **Accept** to sign the agreement. ## Downloading the agreement Once accepted, you can download a copy of the signed agreement for your records. Data Processing
Agreement # DMCA Procedure Source: https://bunny.net/docs/account/dmca-procedure How bunny.net handles DMCA and copyright takedown requests for customer content. bunny.net takes DMCA and copyright requests seriously, protecting intellectual property rights while giving customers a fair chance to respond. This page outlines how we handle DMCA and copyright notices related to customer content. ## Receiving and verifying the report When we receive a DMCA or copyright report, we first verify its authenticity. Our team reviews the report to confirm it meets the legal requirements of a valid claim, ensuring it is not fraudulent and that it pertains to content hosted on our platform. ## Notifying the customer Once we verify a claim, we notify the customer responsible for the content. Our support team sends an official notice by email, and it is also available in your dashboard. The notice includes the details of the report and clear instructions on the next steps. Customers have **48 hours** to respond from the time the email is sent, with two options: * **Option 1: Remove the content.** If you believe the claim is valid, remove the infringing content from your account. In the dashboard, open the Abuse Case to verify the content has been removed. Once confirmed, the case is marked resolved. * **Option 2: File a counter-claim.** If you believe the report is incorrect or invalid, file a counter-claim within the same 48-hour window. A counter-claim is a legal statement contesting the validity of the request, and it triggers the next steps in the resolution process. ## Blocking the content If no action is taken within the initial 48 hours (the content is neither removed nor a counter-claim filed), the content is blocked and the Pull Zone may be suspended. The content stays inaccessible to the public until the matter is resolved. When a counter-claim is submitted, the content stays blocked while the reporter is notified. The reporter has **10 to 14 days** to respond or initiate legal action. If they don't within that window, we unblock the content after the 14-day period. ## Illegal or highly sensitive content Some content types are treated differently from standard claims: * **Illegal content** (such as phishing or malware): We block it immediately. The DMCA process does not apply, and the content remains blocked pending further investigation or legal action. * **Sports streams:** Because of copyright and broadcasting rights, we immediately block content reported as an unlicensed sports stream. To unblock it, you must provide proof that you are licensed to stream the content. Without proof, it remains blocked. ## Restoring content after a takedown Once a valid takedown has occurred, you may not restore or re-upload the removed content, whether under the same name or a different one. If content is repeatedly re-uploaded after being taken down, we will issue further notices and require its removal again. Continued re-uploads may result in suspension of the entire account, to ensure compliance and protect intellectual property rights. ## After a report is filed Once a report is filed and the content is blocked or a counter-claim is submitted, the next steps are governed by legal requirements. If the reporter does not respond or take legal action within the 10 to 14 day window after a counter-claim, the content is unblocked and restored. If legal action is initiated, the content remains blocked until the dispute is resolved. # Personal Details Source: https://bunny.net/docs/account/index Manage your personal details, billing address, and company information. The [Account Settings](https://dash.bunny.net/account/settings) page lets you manage your personal information, billing address, and company details. ## Personal details Update your basic account information: * **First name** (required) * **Last name** (required) * **Email** (required) - Your primary login email * **Abuse report email** - Separate email for receiving abuse reports * **Billing email** - Separate email for invoices and billing updates If you configure a billing email, invoices will only be sent to that address and no longer to your main email. Personal details
form ## Billing address Your billing address appears on invoices. All fields are required: * **Street address** * **City** * **ZIP code** * **Country** Billing details
form ## Company Optional fields for business accounts: * **Company name** - Appears on invoices * **VAT number** - For EU VAT-registered businesses Company details
form After making changes, click **Update Account Details** to save. # Integrations Source: https://bunny.net/docs/account/integrations Manage connected GitHub accounts and linked scripts. The [Integrations](https://dash.bunny.net/account/integrations) page shows your connected GitHub accounts and any scripts linked to them. Integrations ## Connected GitHub accounts When you connect a GitHub account, you can deploy Edge Scripts directly from your repositories. Each connected account shows the scripts that are linked to it. ## Viewing connected scripts Click on a connected GitHub account to see the scripts linked to it. ## Unlinking a GitHub account To disconnect a GitHub account, click on it and select **Unlink**. This removes the connection between your bunny.net account and GitHub. Unlinking a GitHub account does not delete your Edge Scripts. The scripts will remain but will no longer sync with your repository. # Add a Team Member Source: https://bunny.net/docs/account/team-management/add-team-member Invite new users to your bunny.net account with customized permissions. You can add team members to your bunny.net account, giving them their own login credentials and specific permissions to access parts of your dashboard. ## Prerequisites * You must be the account owner or have the **Manage users** permission ## Add a new team member Go to [Account > Manage Team](https://dash.bunny.net/account/users) in your dashboard. Click the **Add New Team Member** button. If this is your first team member, you'll see a **Create Your First Team Member** button. Create First Team Member Fill in the required fields: * **First Name** - The team member's first name * **Last Name** - The team member's last name * **Email** - The email address they'll use to log in A temporary password will be automatically generated. Make sure to copy and share this securely with the team member. Choose which areas of the dashboard the team member can access: * **Manage zones** - CDN pull zones, storage zones, DNS zones, and Stream libraries * **Billing & legal** - Billing information, invoices, and payment methods * **Support tickets** - Create and manage support tickets * **Abuse center** - View and respond to abuse cases * **Manage users** - Add, edit, and remove other team members Click **Save** to create the team member account. ## After adding a team member Once you've added a team member: 1. Share the temporary password with them securely 2. They can log in at [dash.bunny.net](https://dash.bunny.net) using their email and the temporary password 3. They should change their password after their first login Team members will only see menu items and features they have permissions for. Items outside their permissions are hidden from their dashboard view. # Team Management Source: https://bunny.net/docs/account/team-management/index Add team members to your bunny.net account and manage their permissions. Team Management allows you to invite additional users to your bunny.net account, each with their own login credentials and customizable permissions. This is useful for organizations where multiple people need access to different parts of the dashboard. ## Key features * **Individual logins** - Each team member has their own email and password * **Granular permissions** - Control exactly what each team member can access * **Centralized management** - View and manage all team members from one place ## Managing your team You can manage your team from the [Manage Team](https://dash.bunny.net/account/users) page in your dashboard. Navigate to **Account > Manage Team** to: * View all team members and their permissions * Add new team members * Edit existing team member permissions * Remove team members ## Available permissions When adding or editing team members, you can assign the following permissions: | Permission | Description | | --------------- | ------------------------------------------------------------------------ | | Manage zones | Access to CDN pull zones, storage zones, DNS zones, and Stream libraries | | Billing & legal | Access to billing information, invoices, and payment methods | | Support tickets | Ability to create and manage support tickets | | Abuse center | Access to view and respond to abuse cases | | Manage users | Ability to add, edit, and remove other team members | Team members cannot access features they don't have permissions for. The corresponding menu items will not be visible in their dashboard. # Team Permissions Source: https://bunny.net/docs/account/team-management/permissions Understand the available permissions for team members in your bunny.net account. When you add or edit a team member, you can assign specific permissions that control what they can access in the bunny.net dashboard. Team members only see menu items and features they have permissions for. ## Available permissions | Permission | Description | | --------------- | ----------------------------------------------------------------------------- | | Manage zones | Access to CDN pull zones, storage zones, DNS zones, and Stream libraries | | Billing & legal | Access to billing information, invoices, payment methods, and legal documents | | Support tickets | Ability to create, view, and manage support tickets | | Abuse center | Access to view and respond to abuse cases | | Manage users | Ability to add, edit, and remove other team members | ## Permission details ### Manage zones Team members with this permission can: * Create, configure, and delete CDN pull zones * Manage storage zones and upload files * Configure DNS zones and records * Access Stream video libraries and manage video content * Configure Edge Scripting * Manage Shield settings This is the primary permission for team members who need to manage your content delivery infrastructure. ### Billing & legal Team members with this permission can: * View current balance and usage * Access billing history and download invoices * Manage payment methods and auto-recharge settings * View and accept legal agreements (such as the Data Processing Agreement) Only grant this permission to team members who need access to financial information and payment settings. ### Support tickets Team members with this permission can: * Create new support tickets * View and respond to existing tickets * Access support ticket history This is useful for team members who may need to contact bunny.net support on behalf of your organization. ### Abuse center Team members with this permission can: * View active abuse cases * Respond to abuse notifications * Access abuse case history This permission is useful for team members responsible for content moderation or compliance. ### Manage users Team members with this permission can: * Add new team members * Edit existing team member permissions * Remove team members from the account Be careful when granting this permission. Team members with Manage users access can modify permissions for other users, including granting themselves additional permissions. ## Permission combinations You can assign any combination of permissions to a team member. Common configurations include: | Role | Recommended permissions | | --------------- | ----------------------------- | | Developer | Manage zones | | Finance/Admin | Billing & legal | | Support lead | Support tickets, Abuse center | | Account manager | All permissions | ## Viewing team member permissions You can see which permissions each team member has from the [Manage Team](https://dash.bunny.net/account/users) overview page. Each team member's row displays their assigned permissions. # Remove a Team Member Source: https://bunny.net/docs/account/team-management/remove-team-member Remove users from your bunny.net account or update their permissions. You can remove team members from your account or edit their permissions at any time from the Manage Team page. ## Prerequisites * You must be the account owner or have the **Manage users** permission ## Remove a team member Go to [Account > Manage Team](https://dash.bunny.net/account/users) in your dashboard. Locate the team member you want to remove in the list. You can see each member's name, email, and current permissions. Click the **...** (three dots) action menu on the team member's row, and click **Delete** from the dropdown menu. Delete Confirm the deletion when prompted. Removing a team member immediately revokes their access. They will no longer be able to log in to your account. ## Edit team member permissions If you need to change a team member's permissions rather than remove them entirely: Go to [Account > Manage Team](https://dash.bunny.net/account/users) in your dashboard. Locate the team member whose permissions you want to update. Click the **...** (three dots) action menu on the team member's row, and click **Edit** from the dropdown menu. Edit Check or uncheck the permissions as needed: * **Manage zones** * **Billing & legal** * **Support tickets** * **Abuse center** * **Manage users** Click **Save User** to apply the updated permissions. Permission changes take effect immediately. The team member's dashboard view will update to reflect their new access level on their next page load. # Two-Factor Authentication Source: https://bunny.net/docs/account/two-factor-authentication Add an extra layer of security to your bunny.net account. Two-factor authentication (2FA) adds an extra layer of security to your account. When enabled, you'll need to enter a PIN code from your authentication app each time you log in. ## Enable two-factor authentication Navigate to [Account Settings](https://dash.bunny.net/account/settings) and click **Enable Two-Factor Authentication**, or go directly to the [Two-Factor Setup](https://dash.bunny.net/account/settings/twofactorsetup) page. Open your authenticator app (such as Google Authenticator, Authy, or 1Password) and scan the QR code displayed. Enter the 6-digit code from your authenticator app to validate the setup. After validation, you'll receive backup codes. Store these in a safe place - you can use them to access your account if you lose access to your authenticator app. Two-factor authentication
settings ## Backup codes Backup codes are one-time use codes that let you log in if you lose access to your authenticator app. Each code can only be used once. Store your backup codes securely. If you lose both your authenticator app and your backup codes, you'll need to contact support to regain access to your account. ## Supported authenticator apps Any TOTP-compatible authenticator app works with bunny.net, including: * Google Authenticator * Authy * 1Password * Microsoft Authenticator * Bitwarden ## Disable two-factor authentication To disable 2FA, return to Account Settings and click **Disable Two-Factor Authentication**. You'll need to enter a code from your authenticator app to confirm. Disabling two-factor authentication reduces the security of your account. Only disable it if necessary. # Authentication Source: https://bunny.net/docs/api-reference/authentication Learn how to authenticate requests to the bunny.net APIs. To authenticate requests, include your API key in the request headers. ## Header | Header | Value | | ----------- | ------------ | | `AccessKey` | Your API key | The API key is required for performing account-specific actions, such as managing zones or other resources. You can have only one API key associated with your account, which can be viewed anytime in the dashboard. You can learn how to locate your API key in the [dashboard](https://dash.bunny.net). The Stream and Edge Storage APIs use the same `AccessKey` header, but require their own credentials. Use the library API key for Stream, or the storage zone password for Edge Storage, instead of your account API key. ## Example ```bash theme={null} curl --request GET \ --url https://api.bunny.net/dnszone \ --header 'AccessKey: YOUR_API_KEY' ``` Ensure your API key is stored securely and never shared publicly to prevent unauthorized access to your account. # Logging API Reference Source: https://bunny.net/docs/api-reference/cdn-logging/index Access raw CDN request logs for your pull zones via HTTP. The Logging API provides access to raw request logs for all pull zones with logging enabled. Logs appear in near real-time and are retained for 3 days. ## Base URL ``` https://logging.bunnycdn.com ``` ## Authentication Authenticate using the `AccessKey` header or a bearer JWT: ```bash theme={null} curl --request GET \ --url https://logging.bunnycdn.com/v2/pullzones/{pullZoneId}/logs \ --header 'AccessKey: YOUR_API_KEY' ``` ```bash theme={null} curl --request GET \ --url https://logging.bunnycdn.com/v2/pullzones/{pullZoneId}/logs \ --header 'Authorization: Bearer YOUR_JWT' ``` Find your API key in the [account settings](https://dash.bunny.net/account/settings) under **API Keys**. ## API versions Two versions of the API are available: | Version | Format | Description | | --------------------------------------------------------------------------------- | -------------- | --------------------------------------------------------------------------------------- | | [v2](/docs/api-reference/cdn-logging/logging-v2/query-cdn-access-logs-for-a-pull-zone) | JSON | Structured JSON with rich filtering, pagination, and per-field search. **Recommended.** | | [v1](/docs/api-reference/cdn-logging/logging-v1/query-logs-legacy) | Pipe-delimited | Streams a raw pipe-delimited file for a given day. Preserved for existing integrations. | ## Cache status values Both API versions return the following cache status values: | Value | Description | | ------------- | -------------------------------------------------------------------------------- | | `HIT` | Response served directly from cache | | `MISS` | Response not in cache; fetched from origin | | `BYPASS` | Cache was skipped (e.g. due to request headers, cookies, or configuration) | | `REVALIDATED` | Cached response was validated with origin and reused | | `STALE` | Stale cached response served (typically due to origin being unavailable or slow) | | `UPDATING` | Stale content served while a background cache update is in progress | | `-` | No cache interaction (e.g. non-cacheable methods like `POST`) | ## Rate limits Both API versions enforce a per-pull-zone rate limit of **30 requests per 10 seconds**. Exceeding this returns a `429` response. ## Error codes | HTTP Status | Code | Description | | ----------- | ------------------ | ----------------------------------------------------------- | | 400 | `invalid_request` | One or more query parameters failed validation | | 401 | `unauthorized` | No authentication credentials provided | | 403 | `forbidden` | Credentials are invalid, or pull zone is suspended/disabled | | 404 | `logging_disabled` | Logging is not enabled for this pull zone | | 429 | `rate_limited` | Per-pull-zone rate limit exceeded | | 500 | `internal_error` | Unexpected server error | # Query logs (legacy) Source: https://bunny.net/docs/api-reference/cdn-logging/logging-v1/query-logs-legacy https://logging.bunnycdn.com/docs/all/swagger.json get /{date}/{pullZoneId}.log # Query CDN access logs for a pull zone. Source: https://bunny.net/docs/api-reference/cdn-logging/logging-v2/query-cdn-access-logs-for-a-pull-zone https://logging.bunnycdn.com/docs/all/swagger.json get /v2/pullzones/{pullZoneId}/logs Authenticate with either an `Authorization` bearer JWT or an `AccessKey` header. Filter pushdown happens in ClickHouse where possible; `country` and free-text `search` are applied in-process after fetch. # Get affiliate details Source: https://bunny.net/docs/api-reference/core/affiliate/get-affiliate-details https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /billing/affiliate # List API Keys Source: https://bunny.net/docs/api-reference/core/api-keys/list-api-keys https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /apikey # Get useraudit Source: https://bunny.net/docs/api-reference/core/auditlog/get-useraudit https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /user/audit/{date} # Download Payment Request Invoice PDF Source: https://bunny.net/docs/api-reference/core/billing/download-payment-request-invoice-pdf https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /billing/payment-request-invoice/{id}/pdf # Get Billing Details Source: https://bunny.net/docs/api-reference/core/billing/get-billing-details https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /billing Get the billing status details # Get Billing Summary Source: https://bunny.net/docs/api-reference/core/billing/get-billing-summary https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /billing/summary # Get Billing Summary Document Source: https://bunny.net/docs/api-reference/core/billing/get-billing-summary-document https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /billing/summary/{billingRecordId}/pdf # Get Pending Payment Requests Source: https://bunny.net/docs/api-reference/core/billing/get-pending-payment-requests https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /billing/payment-requests # Get Country List Source: https://bunny.net/docs/api-reference/core/countries/get-country-list https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /country # Add DNS Record Source: https://bunny.net/docs/api-reference/core/dns-zone/add-dns-record https://core-api-public-docs.b-cdn.net/docs/v3/public.json put /dnszone/{zoneId}/records # Add DNS Zone Source: https://bunny.net/docs/api-reference/core/dns-zone/add-dns-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /dnszone # Check the DNS zone availability Source: https://bunny.net/docs/api-reference/core/dns-zone/check-the-dns-zone-availability https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /dnszone/checkavailability # Delete DNS Record Source: https://bunny.net/docs/api-reference/core/dns-zone/delete-dns-record https://core-api-public-docs.b-cdn.net/docs/v3/public.json delete /dnszone/{zoneId}/records/{id} # Delete DNS Zone Source: https://bunny.net/docs/api-reference/core/dns-zone/delete-dns-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json delete /dnszone/{id} # Disable DNSSEC on a DNS Zone Source: https://bunny.net/docs/api-reference/core/dns-zone/disable-dnssec-on-a-dns-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json delete /dnszone/{id}/dnssec # Enable DNSSEC on a DNS Zone Source: https://bunny.net/docs/api-reference/core/dns-zone/enable-dnssec-on-a-dns-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /dnszone/{id}/dnssec # Get DNS Query Statistics Source: https://bunny.net/docs/api-reference/core/dns-zone/get-dns-query-statistics https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /dnszone/{id}/statistics # Get DNS Zone Source: https://bunny.net/docs/api-reference/core/dns-zone/get-dns-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /dnszone/{id} # Get dnszone export Source: https://bunny.net/docs/api-reference/core/dns-zone/get-dnszone-export https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /dnszone/{id}/export # Get the latest DNS record scan result for a DNS Zone Source: https://bunny.net/docs/api-reference/core/dns-zone/get-the-latest-dns-record-scan-result-for-a-dns-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /dnszone/{zoneId}/records/scan # Import DNS Records Source: https://bunny.net/docs/api-reference/core/dns-zone/import-dns-records https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /dnszone/{zoneId}/import # Issue new wildcard certificate Source: https://bunny.net/docs/api-reference/core/dns-zone/issue-new-wildcard-certificate https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /dnszone/{zoneId}/certificate/issue # List DNS Zone Records Source: https://bunny.net/docs/api-reference/core/dns-zone/list-dns-zone-records https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /dnszone/{zoneId}/records # List DNS Zones Source: https://bunny.net/docs/api-reference/core/dns-zone/list-dns-zones https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /dnszone # Trigger a background scan for pre-existing DNS records. Can use ZoneId for existing zones or Domain for pre-zone creation scenarios. Source: https://bunny.net/docs/api-reference/core/dns-zone/trigger-a-background-scan-for-pre-existing-dns-records-can-use-zoneid-for-existing-zones-or-domain-for-pre-zone-creation-scenarios https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /dnszone/records/scan # Update DNS Record Source: https://bunny.net/docs/api-reference/core/dns-zone/update-dns-record https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /dnszone/{zoneId}/records/{id} # Update DNS Zones Source: https://bunny.net/docs/api-reference/core/dns-zone/update-dns-zones https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /dnszone/{id} # Errors Source: https://bunny.net/docs/api-reference/core/errors Standard HTTP error codes returned by the bunny.net API. This section outlines the standard HTTP error codes returned by the API. These errors provide insight into why a request may have failed and how to address the issue. Each error code corresponds to a specific type of problem encountered during the processing of the request. ## Error response When an error occurs, the API returns a JSON object with details about the error: ```json theme={null} { "ErrorKey": "pullZone.not_found", "Field": "PullZone", "Message": "The requested Pull Zone was not found" } ``` | Field | Description | | ---------- | ------------------------------------------ | | `ErrorKey` | A machine-readable error identifier | | `Field` | The field or resource related to the error | | `Message` | A human-readable description of the error | ## Status codes | Code | Description | | ----- | ------------------------------------------------------------------------------------------------------------- | | `400` | The request did not pass validation. This may include issues with the request model, body, or parameters. | | `401` | Authorization for the request has failed. | | `403` | The action is forbidden for the user. This may include restrictions on the URL or the HTTP method being used. | | `404` | The requested entity could not be found. | | `429` | Too many requests; the rate limit has been exceeded. | | `500` | An internal server error occurred. | # Core API Reference Source: https://bunny.net/docs/api-reference/core/index Manage your bunny.net account, pull zones, storage zones, DNS, and more. The Core Platform API provides a RESTful interface for managing your bunny.net account and all associated resources. Create and configure pull zones, manage storage, set up DNS, and access billing and statistics. ## Base URL ``` https://api.bunny.net ``` ## Authentication Authenticate using the `AccessKey` header with your account API key: ```bash theme={null} curl --request GET \ --url https://api.bunny.net/pullzone \ --header 'AccessKey: YOUR_API_KEY' ``` Find your API key in the [account settings](https://dash.bunny.net/account/settings) under **API Keys**. ## Resources The Core Platform API provides access to the following resources: | Resource | Description | | ---------------------- | -------------------------------------------------------- | | Pull Zones | Create and configure CDN pull zones for content delivery | | Storage Zones | Manage edge storage zones for file hosting | | DNS Zones | Configure DNS records and zone settings | | Stream Video Libraries | Create and manage video libraries | | Statistics | Access bandwidth, requests, and cache statistics | | Billing | View billing summaries and usage details | | Purge | Purge cached content from the CDN | | API Keys | Manage API keys for programmatic access | | Countries | Retrieve country lists for geo-blocking | | Regions | List available edge regions | ## Product-Specific APIs For managing content within specific products, use the dedicated APIs: Upload and manage videos in your libraries Upload and download files in storage zones Configure WAF rules and security settings Deploy and manage edge scripts # Add Allowed Referer Source: https://bunny.net/docs/api-reference/core/pull-zone/add-allowed-referer https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id}/addAllowedReferrer # Add Blocked IP Source: https://bunny.net/docs/api-reference/core/pull-zone/add-blocked-ip https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id}/addBlockedIp # Add Blocked Referer Source: https://bunny.net/docs/api-reference/core/pull-zone/add-blocked-referer https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id}/addBlockedReferrer # Add Custom Certificate Source: https://bunny.net/docs/api-reference/core/pull-zone/add-custom-certificate https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id}/addCertificate # Add Custom Hostname Source: https://bunny.net/docs/api-reference/core/pull-zone/add-custom-hostname https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id}/addHostname # Add Pull Zone Source: https://bunny.net/docs/api-reference/core/pull-zone/add-pull-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone # Add/Update Edge Rule Source: https://bunny.net/docs/api-reference/core/pull-zone/addupdate-edge-rule https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{pullZoneId}/edgerules/addOrUpdate # Change hostname private key type Source: https://bunny.net/docs/api-reference/core/pull-zone/change-hostname-private-key-type https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id}/updatePrivateKeyType # Check the pull zone availability Source: https://bunny.net/docs/api-reference/core/pull-zone/check-the-pull-zone-availability https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/checkavailability # Complete External DNS Certificate Source: https://bunny.net/docs/api-reference/core/pull-zone/complete-external-dns-certificate https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/completeExternalDnsCertificate # Complete External HTTP Certificate Source: https://bunny.net/docs/api-reference/core/pull-zone/complete-external-http-certificate https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/completeExternalHttpCertificate # Count Pull Zones Source: https://bunny.net/docs/api-reference/core/pull-zone/count-pull-zones https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /pullzone/count # Delete Edge Rule Source: https://bunny.net/docs/api-reference/core/pull-zone/delete-edge-rule https://core-api-public-docs.b-cdn.net/docs/v3/public.json delete /pullzone/{pullZoneId}/edgerules/{edgeRuleId} # Delete Pull Zone Source: https://bunny.net/docs/api-reference/core/pull-zone/delete-pull-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json delete /pullzone/{id} # Get Pull Zone Source: https://bunny.net/docs/api-reference/core/pull-zone/get-pull-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /pullzone/{id} # List Pull Zones Source: https://bunny.net/docs/api-reference/core/pull-zone/list-pull-zones https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /pullzone # Load Free Certificate Source: https://bunny.net/docs/api-reference/core/pull-zone/load-free-certificate https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /pullzone/loadFreeCertificate # Purge Cache Source: https://bunny.net/docs/api-reference/core/pull-zone/purge-cache https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id}/purgeCache # Remove Allowed Referer Source: https://bunny.net/docs/api-reference/core/pull-zone/remove-allowed-referer https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id}/removeAllowedReferrer # Remove Blocked IP Source: https://bunny.net/docs/api-reference/core/pull-zone/remove-blocked-ip https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id}/removeBlockedIp # Remove Blocked Referer Source: https://bunny.net/docs/api-reference/core/pull-zone/remove-blocked-referer https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id}/removeBlockedReferrer # Remove Certificate Source: https://bunny.net/docs/api-reference/core/pull-zone/remove-certificate https://core-api-public-docs.b-cdn.net/docs/v3/public.json delete /pullzone/{id}/removeCertificate # Remove Custom Hostname Source: https://bunny.net/docs/api-reference/core/pull-zone/remove-custom-hostname https://core-api-public-docs.b-cdn.net/docs/v3/public.json delete /pullzone/{id}/removeHostname # Request External DNS Certificate Source: https://bunny.net/docs/api-reference/core/pull-zone/request-external-dns-certificate https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/requestExternalDnsCertificate # Request External HTTP Certificate Source: https://bunny.net/docs/api-reference/core/pull-zone/request-external-http-certificate https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/requestExternalHttpCertificate # Reset Token Key Source: https://bunny.net/docs/api-reference/core/pull-zone/reset-token-key https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id}/resetSecurityKey # Set Edge Rule Enabled Source: https://bunny.net/docs/api-reference/core/pull-zone/set-edge-rule-enabled https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{pullZoneId}/edgerules/{edgeRuleId}/setEdgeRuleEnabled # Set Force SSL Source: https://bunny.net/docs/api-reference/core/pull-zone/set-force-ssl https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id}/setForceSSL # Update Pull Zone Source: https://bunny.net/docs/api-reference/core/pull-zone/update-pull-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id} # Purge URL Source: https://bunny.net/docs/api-reference/core/purge/purge-url https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /purge # Region list Source: https://bunny.net/docs/api-reference/core/region/region-list https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /region # Global Search Source: https://bunny.net/docs/api-reference/core/search/global-search https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /search # Get optimizer statistics Source: https://bunny.net/docs/api-reference/core/statistics/get-optimizer-statistics https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /pullzone/{pullZoneId}/optimizer/statistics # Get Origin Shield Queue Statistics Source: https://bunny.net/docs/api-reference/core/statistics/get-origin-shield-queue-statistics https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /pullzone/{pullZoneId}/originshield/queuestatistics # Get SafeHop Statistics Source: https://bunny.net/docs/api-reference/core/statistics/get-safehop-statistics https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /pullzone/{pullZoneId}/safehop/statistics # Get Statistics Source: https://bunny.net/docs/api-reference/core/statistics/get-statistics https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /statistics # Add Storage Zone Source: https://bunny.net/docs/api-reference/core/storage-zone/add-storage-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /storagezone # Check the storage zone availability Source: https://bunny.net/docs/api-reference/core/storage-zone/check-the-storage-zone-availability https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /storagezone/checkavailability # Delete Storage Zone Source: https://bunny.net/docs/api-reference/core/storage-zone/delete-storage-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json delete /storagezone/{id} # Get Storage Zone Source: https://bunny.net/docs/api-reference/core/storage-zone/get-storage-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /storagezone/{id} # Get Storage Zone Egress Statistics Source: https://bunny.net/docs/api-reference/core/storage-zone/get-storage-zone-egress-statistics https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /storagezone/{id}/statistics/egress # Get Storage Zone Regions Source: https://bunny.net/docs/api-reference/core/storage-zone/get-storage-zone-regions https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /storagezone/regions # Get Storage Zone Statistics Source: https://bunny.net/docs/api-reference/core/storage-zone/get-storage-zone-statistics https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /storagezone/{id}/statistics # List Storage Zones Source: https://bunny.net/docs/api-reference/core/storage-zone/list-storage-zones https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /storagezone # Reset Password Source: https://bunny.net/docs/api-reference/core/storage-zone/reset-password https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /storagezone/{id}/resetPassword # Reset Read-Only Password Source: https://bunny.net/docs/api-reference/core/storage-zone/reset-read-only-password https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /storagezone/resetReadOnlyPassword # Update Storage Zone Source: https://bunny.net/docs/api-reference/core/storage-zone/update-storage-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /storagezone/{id} # Add Allowed Referer Source: https://bunny.net/docs/api-reference/core/stream-video-library/add-allowed-referer https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /videolibrary/{id}/addAllowedReferrer # Add Blocked Referer Source: https://bunny.net/docs/api-reference/core/stream-video-library/add-blocked-referer https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /videolibrary/{id}/addBlockedReferrer # Add Video Library Source: https://bunny.net/docs/api-reference/core/stream-video-library/add-video-library https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /videolibrary # Add Watermark Source: https://bunny.net/docs/api-reference/core/stream-video-library/add-watermark https://core-api-public-docs.b-cdn.net/docs/v3/public.json put /videolibrary/{id}/watermark # Delete Video Library Source: https://bunny.net/docs/api-reference/core/stream-video-library/delete-video-library https://core-api-public-docs.b-cdn.net/docs/v3/public.json delete /videolibrary/{id} # Delete Watermark Source: https://bunny.net/docs/api-reference/core/stream-video-library/delete-watermark https://core-api-public-docs.b-cdn.net/docs/v3/public.json delete /videolibrary/{id}/watermark # Get Languages Source: https://bunny.net/docs/api-reference/core/stream-video-library/get-languages https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /videolibrary/languages # Get Video Library Source: https://bunny.net/docs/api-reference/core/stream-video-library/get-video-library https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /videolibrary/{id} # Get Video Library DRM Statistics Source: https://bunny.net/docs/api-reference/core/stream-video-library/get-video-library-drm-statistics https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /videolibrary/{id}/drm/statistics # Get Video Library Transcribing Statistics Source: https://bunny.net/docs/api-reference/core/stream-video-library/get-video-library-transcribing-statistics https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /videolibrary/{id}/transcribing/statistics # List Video Libraries Source: https://bunny.net/docs/api-reference/core/stream-video-library/list-video-libraries https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /videolibrary # Remove Allowed Referer Source: https://bunny.net/docs/api-reference/core/stream-video-library/remove-allowed-referer https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /videolibrary/{id}/removeAllowedReferrer # Remove Blocked Referer Source: https://bunny.net/docs/api-reference/core/stream-video-library/remove-blocked-referer https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /videolibrary/{id}/removeBlockedReferrer # Reset API Key Source: https://bunny.net/docs/api-reference/core/stream-video-library/reset-api-key https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /videolibrary/{id}/resetApiKey # Reset Read Only API Key Source: https://bunny.net/docs/api-reference/core/stream-video-library/reset-read-only-api-key https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /videolibrary/{id}/resetReadOnlyApiKey # Update Video Library Source: https://bunny.net/docs/api-reference/core/stream-video-library/update-video-library https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /videolibrary/{id} # Close the account Source: https://bunny.net/docs/api-reference/core/user/close-the-account https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /user/closeaccount Close the current user account # Introduction Source: https://bunny.net/docs/api-reference/index Learn about the bunny.net APIs and how to integrate with the platform. bunny.net provides a comprehensive set of APIs that enable you to programmatically manage your resources and integrate with our services. Get started and create your first API request ## Core Platform API The [Core Platform API](/docs/api-reference/core) provides access to manage your account and resources: * Pull Zones (CDN) * Storage Zones * DNS Zones * Stream Video Libraries * Statistics and billing ### Base URL ```bash theme={null} https://api.bunny.net ``` ## Product APIs In addition to the Core Platform API, each product has its own dedicated API for managing content and product-specific operations: | Product | Description | Base URL | | --------------------------------------------------- | ------------------------------------------------------- | --------------------------------------- | | [Stream](/docs/api-reference/stream) | Manage video libraries, collections, and videos | `https://video.bunnycdn.com` | | [Edge Storage](/docs/api-reference/storage) | Upload, download, and manage files in storage zones | `https://{region}.storage.bunnycdn.com` | | [Shield](/docs/api-reference/shield) | Configure WAF rules, rate limiting, and DDoS protection | `https://api.bunny.net` | | [Scripting](/docs/api-reference/scripting) | Deploy and manage edge scripts | `https://api.bunny.net` | | [Magic Containers](/docs/api-reference/magic-containers) | Manage container applications and deployments | `https://api.bunny.net/mc` | | [Origin Errors](/docs/api-reference/origin-errors) | Retrieve origin error logs for pull zones | `https://cdn-origin-logging.bunny.net` | | [CDN Logging](/docs/api-reference/cdn-logging) | Retrieve raw request logs for pull zones | `https://logging.bunnycdn.com` | # Add Application Source: https://bunny.net/docs/api-reference/magic-containers/applications/add-application https://api-mc.opsbunny.net/docs/public/swagger.json post /apps Creates a new application with the specified configuration including containers, volumes, region settings, and autoscaling. # Delete Application Source: https://bunny.net/docs/api-reference/magic-containers/applications/delete-application https://api-mc.opsbunny.net/docs/public/swagger.json delete /apps/{appId} Marks the application for deletion and enqueues cleanup of all associated resources. Returns immediately; deletion is processed asynchronously. # Deploy Application Source: https://bunny.net/docs/api-reference/magic-containers/applications/deploy-application https://api-mc.opsbunny.net/docs/public/swagger.json post /apps/{appId}/deploy Deploys an application, making it active and running. # Get application Source: https://bunny.net/docs/api-reference/magic-containers/applications/get-application https://api-mc.opsbunny.net/docs/public/swagger.json get /apps/{appId} # Get Application Overview Source: https://bunny.net/docs/api-reference/magic-containers/applications/get-application-overview https://api-mc.opsbunny.net/docs/public/swagger.json get /apps/{appId}/overview Retrieves comprehensive status overview for an application including latency, CPU/RAM usage, active instances, regions, and cost information. # Get Application Statistics Source: https://bunny.net/docs/api-reference/magic-containers/applications/get-application-statistics https://api-mc.opsbunny.net/docs/public/swagger.json get /apps/{appId}/statistics Retrieves historical statistics for an application including CPU, RAM, traffic, latency, and volume usage over a specified time period. # Get Application Usage Summary Source: https://bunny.net/docs/api-reference/magic-containers/applications/get-application-usage-summary https://api-mc.opsbunny.net/docs/public/swagger.json get /apps/{appId}/summary Retrieves usage summary for an application including latency, volume size, monthly cost, and status information. # List Applications Source: https://bunny.net/docs/api-reference/magic-containers/applications/list-applications https://api-mc.opsbunny.net/docs/public/swagger.json get /apps Lists all applications for the authenticated user with their current status. # Patch Application Source: https://bunny.net/docs/api-reference/magic-containers/applications/patch-application https://api-mc.opsbunny.net/docs/public/swagger.json patch /apps/{appId} Partially updates an existing application using JSON Merge Patch semantics. Only provided fields will be updated; existing fields not included in the request will remain unchanged. For arrays (containers, volumes, endpoints), items with matching IDs will be updated, items without IDs will be added as new, and items not included will be deleted. # Restart Application Source: https://bunny.net/docs/api-reference/magic-containers/applications/restart-application https://api-mc.opsbunny.net/docs/public/swagger.json post /apps/{appId}/restart Triggers a restart of all pods for the specified application. # Undeploy Application Source: https://bunny.net/docs/api-reference/magic-containers/applications/undeploy-application https://api-mc.opsbunny.net/docs/public/swagger.json post /apps/{appId}/undeploy Undeploys an application, stopping all running instances. # Update Application Source: https://bunny.net/docs/api-reference/magic-containers/applications/update-application https://api-mc.opsbunny.net/docs/public/swagger.json put /apps/{appId} Updates an existing application with full replacement of all configuration fields. # Get Application Autoscaling Source: https://bunny.net/docs/api-reference/magic-containers/autoscalingsettings/get-application-autoscaling https://api-mc.opsbunny.net/docs/public/swagger.json get /apps/{appId}/autoscaling Retrieves the current autoscaling settings for an application, including minimum and maximum replica counts. # Update Application Autoscaling Source: https://bunny.net/docs/api-reference/magic-containers/autoscalingsettings/update-application-autoscaling https://api-mc.opsbunny.net/docs/public/swagger.json put /apps/{appId}/autoscaling Updates the autoscaling settings for an application, including minimum and maximum replica counts. # Add container registry Source: https://bunny.net/docs/api-reference/magic-containers/containerregistries/add-container-registry https://api-mc.opsbunny.net/docs/public/swagger.json post /registries Add a container registry for user. # Delete Container Registry Source: https://bunny.net/docs/api-reference/magic-containers/containerregistries/delete-container-registry https://api-mc.opsbunny.net/docs/public/swagger.json delete /registries/{registryId} Deletes a container registry. Returns an error if the registry is currently in use by any applications. # Get Container Config Suggestions Source: https://bunny.net/docs/api-reference/magic-containers/containerregistries/get-container-config-suggestions https://api-mc.opsbunny.net/docs/public/swagger.json post /registries/config-suggestions Gets recommended configuration for a container image including endpoint configurations and environment variables. # Get Container Image Digest Source: https://bunny.net/docs/api-reference/magic-containers/containerregistries/get-container-image-digest https://api-mc.opsbunny.net/docs/public/swagger.json post /registries/digest Retrieves the digest information for a specific container image tag. # Get Container Registry Source: https://bunny.net/docs/api-reference/magic-containers/containerregistries/get-container-registry https://api-mc.opsbunny.net/docs/public/swagger.json get /registries/{registryId} Retrieves a specific container registry by its ID. # Get Image Config Source: https://bunny.net/docs/api-reference/magic-containers/containerregistries/get-image-config https://api-mc.opsbunny.net/docs/public/swagger.json post /registries/image-config Retrieves endpoint and volume suggestions for a container image from a registry. # List Container Image Tags Source: https://bunny.net/docs/api-reference/magic-containers/containerregistries/list-container-image-tags https://api-mc.opsbunny.net/docs/public/swagger.json post /registries/tags Lists all available tags for a specific container image. # List Container Images Source: https://bunny.net/docs/api-reference/magic-containers/containerregistries/list-container-images https://api-mc.opsbunny.net/docs/public/swagger.json post /registries/images Lists all container images available in a private registry. # List Container Registries Source: https://bunny.net/docs/api-reference/magic-containers/containerregistries/list-container-registries https://api-mc.opsbunny.net/docs/public/swagger.json get /registries Lists all container registries configured for the authenticated user. # Search Public Container Images Source: https://bunny.net/docs/api-reference/magic-containers/containerregistries/search-public-container-images https://api-mc.opsbunny.net/docs/public/swagger.json post /registries/public-images/search Searches for public container images in a registry by prefix. # Update Container Registry Source: https://bunny.net/docs/api-reference/magic-containers/containerregistries/update-container-registry https://api-mc.opsbunny.net/docs/public/swagger.json put /registries/{registryId} Updates an existing container registry configuration including credentials. # Add Container Template Source: https://bunny.net/docs/api-reference/magic-containers/containers/add-container-template https://api-mc.opsbunny.net/docs/public/swagger.json post /apps/{appId}/containers Adds a new container template to an application. # Delete Container Template Source: https://bunny.net/docs/api-reference/magic-containers/containers/delete-container-template https://api-mc.opsbunny.net/docs/public/swagger.json delete /apps/{appId}/containers/{containerId} Deletes a container template from an application. # Get Container Template Source: https://bunny.net/docs/api-reference/magic-containers/containers/get-container-template https://api-mc.opsbunny.net/docs/public/swagger.json get /apps/{appId}/containers/{containerId} Gets a container template within an application. # Patch Container Template Source: https://bunny.net/docs/api-reference/magic-containers/containers/patch-container-template https://api-mc.opsbunny.net/docs/public/swagger.json patch /apps/{appId}/containers/{containerId} Partially updates a container template within an application. Only provided fields will be updated; existing fields not included in the request will remain unchanged. # Set Container Environment Variables Source: https://bunny.net/docs/api-reference/magic-containers/containers/set-container-environment-variables https://api-mc.opsbunny.net/docs/public/swagger.json put /apps/{appId}/containers/{containerId}/env Replaces all environment variables for a container template. All existing environment variables will be removed and replaced with the provided set. # Add application endpoint Source: https://bunny.net/docs/api-reference/magic-containers/endpoints/add-application-endpoint https://api-mc.opsbunny.net/docs/public/swagger.json post /apps/{appId}/containers/{containerId}/endpoints Add CDN or Anycast endpoint to a container of given application. # Delete application endpoint Source: https://bunny.net/docs/api-reference/magic-containers/endpoints/delete-application-endpoint https://api-mc.opsbunny.net/docs/public/swagger.json delete /apps/{appId}/endpoints/{endpointId} Delete endpoint of a container for given application. # List application endpoints Source: https://bunny.net/docs/api-reference/magic-containers/endpoints/list-application-endpoints https://api-mc.opsbunny.net/docs/public/swagger.json get /apps/{appId}/endpoints List endpoints from all containers for given application # Update Application Endpoint Source: https://bunny.net/docs/api-reference/magic-containers/endpoints/update-application-endpoint https://api-mc.opsbunny.net/docs/public/swagger.json put /apps/{appId}/endpoints/{endpointId} Update an existing endpoint for given application # Get User Limits Source: https://bunny.net/docs/api-reference/magic-containers/limits/get-user-limits https://api-mc.opsbunny.net/docs/public/swagger.json get /limits Retrieves the current resource limits and usage for the authenticated user, including application counts and instance limits. # Create log forwarding configuration Source: https://bunny.net/docs/api-reference/magic-containers/log-forwarding/create-log-forwarding-configuration https://api-mc.opsbunny.net/docs/public/swagger.json post /log/forwarding Create a new log forwarding configuration. # Delete log-forwarding configuration Source: https://bunny.net/docs/api-reference/magic-containers/log-forwarding/delete-log-forwarding-configuration https://api-mc.opsbunny.net/docs/public/swagger.json delete /log/forwarding/{appId} Delete a log-forwarding configuration. # Get log-forwarding configuration Source: https://bunny.net/docs/api-reference/magic-containers/log-forwarding/get-log-forwarding-configuration https://api-mc.opsbunny.net/docs/public/swagger.json get /log/forwarding/{appId} Get a specific log-forwarding configuration by ID. # List log-forwarding configurations Source: https://bunny.net/docs/api-reference/magic-containers/log-forwarding/list-log-forwarding-configurations https://api-mc.opsbunny.net/docs/public/swagger.json get /log/forwarding Get a list of all log-forwarding configurations for the authenticated user. # Update log-forwarding configuration Source: https://bunny.net/docs/api-reference/magic-containers/log-forwarding/update-log-forwarding-configuration https://api-mc.opsbunny.net/docs/public/swagger.json put /log/forwarding/{appId} Update an existing log-forwarding configuration. # List Node IPs (Plain) Source: https://bunny.net/docs/api-reference/magic-containers/nodes/list-node-ips-plain https://api-mc.opsbunny.net/docs/public/swagger.json get /nodes/plain Lists all node IP addresses in the Magic Containers network as a flat list. # List Nodes Source: https://bunny.net/docs/api-reference/magic-containers/nodes/list-nodes https://api-mc.opsbunny.net/docs/public/swagger.json get /nodes Lists all node IP addresses in the Magic Containers network. # Magic Containers API Reference Source: https://bunny.net/docs/api-reference/magic-containers/overview Create, modify or delete your Magic Containers applications configuration. The Magic Containers API provides a RESTful interface for managing your magic containers applications. ## Base URL ``` https://api.bunny.net/mc ``` ## Authentication Authenticate using the `AccessKey` header with your Bunny Net Access Key: ```bash theme={null} curl --request GET \ --url https://api.bunny.net/mc/apps \ --header 'AccessKey: YOUR_API_KEY' ``` Find your API key in the [account API Keys](https://dash.bunny.net/account/api-key). # Recreate Pod Source: https://bunny.net/docs/api-reference/magic-containers/pods/recreate-pod https://api-mc.opsbunny.net/docs/public/swagger.json post /apps/{appId}/pods/{podId}/recreate Recreate a pod, deleting previous one. # Get Optimal Base Region Source: https://bunny.net/docs/api-reference/magic-containers/regions/get-optimal-base-region https://api-mc.opsbunny.net/docs/public/swagger.json get /regions/optimal Returns the optimal base region for deployment based on the user's CDN server token location. # List Regions Source: https://bunny.net/docs/api-reference/magic-containers/regions/list-regions https://api-mc.opsbunny.net/docs/public/swagger.json get /regions Lists all available regions where applications can be deployed, including their anycast support and capacity status. # Get Application Region Settings Source: https://bunny.net/docs/api-reference/magic-containers/regionsettings/get-application-region-settings https://api-mc.opsbunny.net/docs/public/swagger.json get /apps/{appId}/region-settings Retrieves the current region settings for an application, including allowed regions, required regions, and maximum allowed regions. # Update Application Region Settings Source: https://bunny.net/docs/api-reference/magic-containers/regionsettings/update-application-region-settings https://api-mc.opsbunny.net/docs/public/swagger.json put /apps/{appId}/region-settings Updates the region settings for an application, including allowed regions, required regions, and maximum allowed regions. # Delete All Volume Instances Source: https://bunny.net/docs/api-reference/magic-containers/volumes/delete-all-volume-instances https://api-mc.opsbunny.net/docs/public/swagger.json delete /apps/{appId}/volumes/{volumeId} Deletes all volume instances for a volume template. All instances must be detached before deletion. # Delete Volume Instance Source: https://bunny.net/docs/api-reference/magic-containers/volumes/delete-volume-instance https://api-mc.opsbunny.net/docs/public/swagger.json delete /apps/{appId}/volumes/{volumeId}/instances/{instanceId} Deletes a specific volume instance. The volume must be detached before deletion. # Detach Volume Source: https://bunny.net/docs/api-reference/magic-containers/volumes/detach-volume https://api-mc.opsbunny.net/docs/public/swagger.json post /apps/{appId}/volumes/{volumeId}/detach Detaches a volume template from all application containers. # List Volumes Source: https://bunny.net/docs/api-reference/magic-containers/volumes/list-volumes https://api-mc.opsbunny.net/docs/public/swagger.json get /apps/{appId}/volumes Lists all volume templates and their instances for an application, including usage statistics. # Update Volume Source: https://bunny.net/docs/api-reference/magic-containers/volumes/update-volume https://api-mc.opsbunny.net/docs/public/swagger.json patch /apps/{appId}/volumes/{volumeId} Partially updates a volume template's configuration including name and size. Only provided fields will be updated. # Get origin error logs for a specific Pull Zone and date Source: https://bunny.net/docs/api-reference/origin-errors/get-origin-error-logs-for-a-specific-pull-zone-and-date /api-reference/origin-errors/openapi.json get /{pullZoneId}/{dateTime} Retrieves origin error logs for the given Pull Zone and date. # Origin Errors API Reference Source: https://bunny.net/docs/api-reference/origin-errors/index Retrieve origin error logs for your pull zones via HTTP. The Origin Errors API provides visibility into failed origin requests. Query error logs by pull zone and date to identify DNS failures, timeouts, and other origin issues. ## Base URL ``` https://cdn-origin-logging.bunny.net ``` ## Authentication Authenticate using the `AccessKey` header with your account API key: ```bash theme={null} curl --request GET \ --url https://cdn-origin-logging.bunny.net/{pullZoneId}/{dateTime} \ --header 'AccessKey: YOUR_API_KEY' ``` Find your API key in the [account settings](https://dash.bunny.net/account/settings) under **API Keys**. ## Error codes The API returns logs with the following error codes: | Error Code | Description | | -------------------------- | ------------------------------- | | `dns_lookup` | Origin DNS lookup failed | | `http_timeout` | Request to origin timed out | | `http_request_exception` | HTTP request exception occurred | | `http_request_failure` | HTTP request failed | | `http_invalid_range` | Invalid range request | | `http_loop_detected` | Request loop detected | | `http_invalid_compression` | Invalid compression in response | | `network_socket_exception` | Network socket exception | | `network_io_error` | Network I/O error | | `notfound_localdb` | Not found in local database | # Quickstart Source: https://bunny.net/docs/api-reference/quickstart Make your first API request to bunny.net in minutes. This guide walks you through making your first API request to bunny.net. 1. Log in to the [bunny.net dashboard](https://dash.bunny.net) 2. Navigate to **Account** > **API** 3. Copy your **Account API Key** Keep your API key secure and never share it publicly. Use the following curl command to list your pull zones: ```bash theme={null} curl --request GET \ --url https://api.bunny.net/pullzone \ --header 'AccessKey: YOUR_API_KEY' \ --header 'Content-Type: application/json' ``` Replace `YOUR_API_KEY` with the API key you copied from the dashboard. A successful response returns a list of your pull zones: ```json theme={null} { "Items": [ { "Id": 1234, "Name": "my-pull-zone", "OriginUrl": "https://example.com", "Enabled": true, "Hostnames": [ { "Id": 5678, "Value": "my-pull-zone.b-cdn.net" } ] } ], "CurrentPage": 0, "TotalItems": 1, "HasMoreItems": false } ``` If you don't have any pull zones yet, the `Items` array will be empty. Explore the full range of bunny.net APIs: # Get Code Source: https://bunny.net/docs/api-reference/scripting/code/get-code https://core-api-public-docs.b-cdn.net/docs/v3/compute.json get /compute/script/{id}/code # Set Code Source: https://bunny.net/docs/api-reference/scripting/code/set-code https://core-api-public-docs.b-cdn.net/docs/v3/compute.json post /compute/script/{id}/code # Add Edge Script Source: https://bunny.net/docs/api-reference/scripting/edge-script/add-edge-script https://core-api-public-docs.b-cdn.net/docs/v3/compute.json post /compute/script # Delete Edge Script Source: https://bunny.net/docs/api-reference/scripting/edge-script/delete-edge-script https://core-api-public-docs.b-cdn.net/docs/v3/compute.json delete /compute/script/{id} # Get Edge Script Source: https://bunny.net/docs/api-reference/scripting/edge-script/get-edge-script https://core-api-public-docs.b-cdn.net/docs/v3/compute.json get /compute/script/{id} # Get Edge Script Statistics Source: https://bunny.net/docs/api-reference/scripting/edge-script/get-edge-script-statistics https://core-api-public-docs.b-cdn.net/docs/v3/compute.json get /compute/script/{id}/statistics # List Edge Scripts Source: https://bunny.net/docs/api-reference/scripting/edge-script/list-edge-scripts https://core-api-public-docs.b-cdn.net/docs/v3/compute.json get /compute/script # Rotate Deployment Key Source: https://bunny.net/docs/api-reference/scripting/edge-script/rotate-deployment-key https://core-api-public-docs.b-cdn.net/docs/v3/compute.json post /compute/script/{id}/deploymentKey/rotate # Update Edge Script Source: https://bunny.net/docs/api-reference/scripting/edge-script/update-edge-script https://core-api-public-docs.b-cdn.net/docs/v3/compute.json post /compute/script/{id} # Edge Scripting API Reference Source: https://bunny.net/docs/api-reference/scripting/index Deploy, manage, and monitor edge scripts via HTTP. The Scripting API lets you manage edge scripts programmatically. Create scripts, configure deployments, and monitor execution via HTTP. ## Base URL ``` https://api.bunny.net ``` ## Authentication Authenticate using the `AccessKey` header with your account API key: ```bash theme={null} curl --request GET \ --url https://api.bunny.net/compute/script \ --header 'AccessKey: YOUR_API_KEY' ``` Find your API key in the [account settings](https://dash.bunny.net/account/settings) under **API Keys**. ## Script Types Edge Scripting supports two script types: * **Standalone** — Handle requests directly without an origin server * **Middleware** — Intercept and modify requests/responses flowing through a pull zone See the [Edge Scripting documentation](/docs/scripting) for details on each type. ## SDKs # Get Active Release Source: https://bunny.net/docs/api-reference/scripting/release/get-active-release https://core-api-public-docs.b-cdn.net/docs/v3/compute.json get /compute/script/{id}/releases/active # Get Releases Source: https://bunny.net/docs/api-reference/scripting/release/get-releases https://core-api-public-docs.b-cdn.net/docs/v3/compute.json get /compute/script/{id}/releases # Publish Release Source: https://bunny.net/docs/api-reference/scripting/release/publish-release https://core-api-public-docs.b-cdn.net/docs/v3/compute.json post /compute/script/{id}/publish # Publish Release Source: https://bunny.net/docs/api-reference/scripting/release/publish-release-1 https://core-api-public-docs.b-cdn.net/docs/v3/compute.json post /compute/script/{id}/publish/{uuid} # Add Secret Source: https://bunny.net/docs/api-reference/scripting/secret/add-secret https://core-api-public-docs.b-cdn.net/docs/v3/compute.json post /compute/script/{id}/secrets # Delete Secret Source: https://bunny.net/docs/api-reference/scripting/secret/delete-secret https://core-api-public-docs.b-cdn.net/docs/v3/compute.json delete /compute/script/{id}/secrets/{secretId} # List Secrets Source: https://bunny.net/docs/api-reference/scripting/secret/list-secrets https://core-api-public-docs.b-cdn.net/docs/v3/compute.json get /compute/script/{id}/secrets # Update Secret Source: https://bunny.net/docs/api-reference/scripting/secret/update-secret https://core-api-public-docs.b-cdn.net/docs/v3/compute.json post /compute/script/{id}/secrets/{secretId} # Upsert Secret Source: https://bunny.net/docs/api-reference/scripting/secret/upsert-secret https://core-api-public-docs.b-cdn.net/docs/v3/compute.json put /compute/script/{id}/secrets # Add Variable Source: https://bunny.net/docs/api-reference/scripting/variable/add-variable https://core-api-public-docs.b-cdn.net/docs/v3/compute.json post /compute/script/{id}/variables/add # Delete Variable Source: https://bunny.net/docs/api-reference/scripting/variable/delete-variable https://core-api-public-docs.b-cdn.net/docs/v3/compute.json delete /compute/script/{id}/variables/{variableId} # Get Variable Source: https://bunny.net/docs/api-reference/scripting/variable/get-variable https://core-api-public-docs.b-cdn.net/docs/v3/compute.json get /compute/script/{id}/variables/{variableId} # Update Variable Source: https://bunny.net/docs/api-reference/scripting/variable/update-variable https://core-api-public-docs.b-cdn.net/docs/v3/compute.json post /compute/script/{id}/variables/{variableId} # Upsert Variable Source: https://bunny.net/docs/api-reference/scripting/variable/upsert-variable https://core-api-public-docs.b-cdn.net/docs/v3/compute.json put /compute/script/{id}/variables # Create a new Custom Access List associated with a Shield Zone Source: https://bunny.net/docs/api-reference/shield/access-lists/create-a-new-custom-access-list-associated-with-a-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json post /shield/shield-zone/{shieldZoneId}/access-lists # Delete the specified Custom Access List associated with a Shield Zone Source: https://bunny.net/docs/api-reference/shield/access-lists/delete-the-specified-custom-access-list-associated-with-a-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json delete /shield/shield-zone/{shieldZoneId}/access-lists/{id} # Get all Access Lists API enumeration types and their values Source: https://bunny.net/docs/api-reference/shield/access-lists/get-all-access-lists-api-enumeration-types-and-their-values https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zone/{shieldZoneId}/access-lists/enums # Get all Access Lists available for a Shield Zone Source: https://bunny.net/docs/api-reference/shield/access-lists/get-all-access-lists-available-for-a-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zone/{shieldZoneId}/access-lists # Get the specified Custom Access List associated with a Shield Zone Source: https://bunny.net/docs/api-reference/shield/access-lists/get-the-specified-custom-access-list-associated-with-a-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zone/{shieldZoneId}/access-lists/{id} # Update Access List Configuration for a Shield Zone Source: https://bunny.net/docs/api-reference/shield/access-lists/update-access-list-configuration-for-a-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json patch /shield/shield-zone/{shieldZoneId}/access-lists/configurations/{id} # Update the specified Custom Access List associated with a Shield Zone Source: https://bunny.net/docs/api-reference/shield/access-lists/update-the-specified-custom-access-list-associated-with-a-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json patch /shield/shield-zone/{shieldZoneId}/access-lists/{id} # Get all API Guardian enumeration types and their values Source: https://bunny.net/docs/api-reference/shield/api-guardian/get-all-api-guardian-enumeration-types-and-their-values https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zone/{shieldZoneId}/api-guardian/enums # Get the API Guardian configuration and endpoints. Source: https://bunny.net/docs/api-reference/shield/api-guardian/get-the-api-guardian-configuration-and-endpoints https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zone/{shieldZoneId}/api-guardian # Update the API Guardian configuration (enabled, execution mode, body limit action) Source: https://bunny.net/docs/api-reference/shield/api-guardian/update-the-api-guardian-configuration-enabled-execution-mode-body-limit-action https://api.bunny.net/shield/docs/v1/swagger.json patch /shield/shield-zone/{shieldZoneId}/api-guardian # Update your API Guardian Endpoint configuration Source: https://bunny.net/docs/api-reference/shield/api-guardian/update-your-api-guardian-endpoint-configuration https://api.bunny.net/shield/docs/v1/swagger.json patch /shield/shield-zone/{shieldZoneId}/api-guardian/endpoint/{endpointId} # Update your OpenAPI specification Source: https://bunny.net/docs/api-reference/shield/api-guardian/update-your-openapi-specification https://api.bunny.net/shield/docs/v1/swagger.json patch /shield/shield-zone/{shieldZoneId}/api-guardian/spec # Upload your OpenAPI specification Source: https://bunny.net/docs/api-reference/shield/api-guardian/upload-your-openapi-specification https://api.bunny.net/shield/docs/v1/swagger.json post /shield/shield-zone/{shieldZoneId}/api-guardian/spec # List bots available for explicit allow/block configuration on this Shield Zone, grouped by category. Source: https://bunny.net/docs/api-reference/shield/bot-categorization/list-bots-available-for-explicit-allowblock-configuration-on-this-shield-zone-grouped-by-category https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zone/{shieldZoneId}/bot-categorization # Set or clear the action applied to a categorised bot for this Shield Zone. Source: https://bunny.net/docs/api-reference/shield/bot-categorization/set-or-clear-the-action-applied-to-a-categorised-bot-for-this-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json put /shield/shield-zone/{shieldZoneId}/bot-categorization/bots/{botId} # Set or clear the action applied to every bot in a category for this Shield Zone. Source: https://bunny.net/docs/api-reference/shield/bot-categorization/set-or-clear-the-action-applied-to-every-bot-in-a-category-for-this-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json put /shield/shield-zone/{shieldZoneId}/bot-categorization/categories/{category} # Update your current Bot Detection configuration Source: https://bunny.net/docs/api-reference/shield/bot-detection/update-your-current-bot-detection-configuration https://api.bunny.net/shield/docs/v1/swagger.json patch /shield/shield-zone/{shieldZoneId}/bot-detection # Your current Bot Detection configuration Source: https://bunny.net/docs/api-reference/shield/bot-detection/your-current-bot-detection-configuration https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zone/{shieldZoneId}/bot-detection # Delete a custom HTML response page for a Shield Zone Source: https://bunny.net/docs/api-reference/shield/custom-response-pages/delete-a-custom-html-response-page-for-a-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json delete /shield/shield-zone/{shieldZoneId}/custom-page/{pageType} # Get a custom HTML response page for a Shield Zone Source: https://bunny.net/docs/api-reference/shield/custom-response-pages/get-a-custom-html-response-page-for-a-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zone/{shieldZoneId}/custom-page/{pageType} # Upload a custom HTML response page for a Shield Zone Source: https://bunny.net/docs/api-reference/shield/custom-response-pages/upload-a-custom-html-response-page-for-a-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json put /shield/shield-zone/{shieldZoneId}/custom-page/{pageType} # List of all DDoS Enum Mappings Source: https://bunny.net/docs/api-reference/shield/ddos/list-of-all-ddos-enum-mappings https://api.bunny.net/shield/docs/v1/swagger.json get /shield/ddos/enums # Export the full filtered Event Logs set for a Shield Zone as CSV Source: https://bunny.net/docs/api-reference/shield/event-logs/export-the-full-filtered-event-logs-set-for-a-shield-zone-as-csv https://api.bunny.net/shield/docs/v1/swagger.json post /shield/event-logs/{shieldZoneId}/export # Get Event Logs for Shield Zone Source: https://bunny.net/docs/api-reference/shield/event-logs/get-event-logs-for-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/event-logs/{shieldZoneId}/{date}/{continuationToken} # Search, filter and group Event Logs for a Shield Zone Source: https://bunny.net/docs/api-reference/shield/event-logs/search-filter-and-group-event-logs-for-a-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json post /shield/event-logs/{shieldZoneId}/search # Shield API Reference Source: https://bunny.net/docs/api-reference/shield/index Configure WAF rules, rate limiting, and security settings via HTTP. The Shield API provides a RESTful interface for managing security settings on your pull zones. Configure WAF rules, rate limiting, bot detection, and access controls programmatically. ## Base URL ``` https://api.bunny.net ``` ## Authentication Authenticate using the `AccessKey` header with your account API key: ```bash theme={null} curl --request GET \ --url https://api.bunny.net/shield/waf/{shieldZoneId} \ --header 'AccessKey: YOUR_API_KEY' ``` Find your API key in the [account settings](https://dash.bunny.net/account/settings) under **API Keys**. ## Resources Block exploits and OWASP Top 10 vulnerabilities Control request rates per IP, user, or path Detect and block malicious bots Manage IP allowlists and blocklists # Get a detailed metrics overview for the specified Shield Zone within the selected time range and resolution Source: https://bunny.net/docs/api-reference/shield/metrics/get-a-detailed-metrics-overview-for-the-specified-shield-zone-within-the-selected-time-range-and-resolution https://api.bunny.net/shield/docs/v1/swagger.json get /shield/metrics/overview/{shieldZoneId}/detailed # Get aggregated rate limit metrics for the specified Shield Zone Source: https://bunny.net/docs/api-reference/shield/metrics/get-aggregated-rate-limit-metrics-for-the-specified-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/metrics/rate-limits/{shieldZoneId} # Get an overview of metrics for the specified Shield Zone Source: https://bunny.net/docs/api-reference/shield/metrics/get-an-overview-of-metrics-for-the-specified-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/metrics/overview/{shieldZoneId} # Get API Guardian metrics for the specified Shield Zone Source: https://bunny.net/docs/api-reference/shield/metrics/get-api-guardian-metrics-for-the-specified-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/metrics/shield-zone/{shieldZoneId}/api-guardian # Get bot detection metrics for the specified Shield Zone Source: https://bunny.net/docs/api-reference/shield/metrics/get-bot-detection-metrics-for-the-specified-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/metrics/shield-zone/{shieldZoneId}/bot-detection # Get detailed metrics for the specified Rate Limit Source: https://bunny.net/docs/api-reference/shield/metrics/get-detailed-metrics-for-the-specified-rate-limit https://api.bunny.net/shield/docs/v1/swagger.json get /shield/metrics/rate-limit/{id} # Get metrics for a specific API Guardian endpoint within the specified Shield Zone Source: https://bunny.net/docs/api-reference/shield/metrics/get-metrics-for-a-specific-api-guardian-endpoint-within-the-specified-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/metrics/shield-zone/{shieldZoneId}/api-guardian/endpoint/{endpointId} # Get metrics for a specific WAF Rule within the specified Shield Zone Source: https://bunny.net/docs/api-reference/shield/metrics/get-metrics-for-a-specific-waf-rule-within-the-specified-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/metrics/shield-zone/{shieldZoneId}/waf-rule/{ruleId} # Get the overage breakdown for the specified Shield Zone for a given month, segmented by billing plan changes Source: https://bunny.net/docs/api-reference/shield/metrics/get-the-overage-breakdown-for-the-specified-shield-zone-for-a-given-month-segmented-by-billing-plan-changes https://api.bunny.net/shield/docs/v1/swagger.json get /shield/metrics/overages/{shieldZoneId} # Get upload scanning metrics for the specified Shield Zone Source: https://bunny.net/docs/api-reference/shield/metrics/get-upload-scanning-metrics-for-the-specified-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/metrics/shield-zone/{shieldZoneId}/upload-scanning # Get the current Promotion State for your Account Source: https://bunny.net/docs/api-reference/shield/promotions/get-the-current-promotion-state-for-your-account https://api.bunny.net/shield/docs/v1/swagger.json get /shield/promo/state # Create a Rate Limit for your Shield Zone Source: https://bunny.net/docs/api-reference/shield/rate-limiting/create-a-rate-limit-for-your-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json post /shield/rate-limit # Delete a Rate Limit on your Shield Zone Source: https://bunny.net/docs/api-reference/shield/rate-limiting/delete-a-rate-limit-on-your-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json delete /shield/rate-limit/{id} # Get Individual Rate Limit for your Shield Zone Source: https://bunny.net/docs/api-reference/shield/rate-limiting/get-individual-rate-limit-for-your-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/rate-limit/{id} # Get Rate Limits for your Shield Zone Source: https://bunny.net/docs/api-reference/shield/rate-limiting/get-rate-limits-for-your-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/rate-limits/{shieldZoneId} # Update a Rate Limit configuration on your Shield Zone Source: https://bunny.net/docs/api-reference/shield/rate-limiting/update-a-rate-limit-configuration-on-your-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json patch /shield/rate-limit/{id} # Create a Shield Zone for your PullZone Source: https://bunny.net/docs/api-reference/shield/shield-zone/create-a-shield-zone-for-your-pullzone https://api.bunny.net/shield/docs/v1/swagger.json post /shield/shield-zone # Create a Shield Zone in under-attack mode for your PullZone Source: https://bunny.net/docs/api-reference/shield/shield-zone/create-a-shield-zone-in-under-attack-mode-for-your-pullzone https://api.bunny.net/shield/docs/v1/swagger.json post /shield/shield-zone/under-attack # Get Active Shield Zones for Pullzone Mapping Source: https://bunny.net/docs/api-reference/shield/shield-zone/get-active-shield-zones-for-pullzone-mapping https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zones/pullzone-mapping # Get all of your Shield Zone Configurations Source: https://bunny.net/docs/api-reference/shield/shield-zone/get-all-of-your-shield-zone-configurations https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zones # Get Singular Shield Zone Configuration Source: https://bunny.net/docs/api-reference/shield/shield-zone/get-singular-shield-zone-configuration https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zone/{shieldZoneId} # Get Singular Shield Zone Configuration for PullZone Source: https://bunny.net/docs/api-reference/shield/shield-zone/get-singular-shield-zone-configuration-for-pullzone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zone/get-by-pullzone/{pullZoneId} # Get the recommended defaults for creating a Shield Zone Source: https://bunny.net/docs/api-reference/shield/shield-zone/get-the-recommended-defaults-for-creating-a-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zone/defaults # Update your Shield Zone configuration Source: https://bunny.net/docs/api-reference/shield/shield-zone/update-your-shield-zone-configuration https://api.bunny.net/shield/docs/v1/swagger.json patch /shield/shield-zone # Get your Current Upload Scanning Configuration Source: https://bunny.net/docs/api-reference/shield/upload-scanning/get-your-current-upload-scanning-configuration https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zone/{shieldZoneId}/upload-scanning # Update your Upload Scanning Configuration Source: https://bunny.net/docs/api-reference/shield/upload-scanning/update-your-upload-scanning-configuration https://api.bunny.net/shield/docs/v1/swagger.json patch /shield/shield-zone/{shieldZoneId}/upload-scanning # Create a new custom WAF rule Source: https://bunny.net/docs/api-reference/shield/waf/create-a-new-custom-waf-rule https://api.bunny.net/shield/docs/v1/swagger.json post /shield/waf/custom-rule # Delete a custom WAF rule Source: https://bunny.net/docs/api-reference/shield/waf/delete-a-custom-waf-rule https://api.bunny.net/shield/docs/v1/swagger.json delete /shield/waf/custom-rule/{id} # Retrieve a specific custom WAF rule Source: https://bunny.net/docs/api-reference/shield/waf/retrieve-a-specific-custom-waf-rule https://api.bunny.net/shield/docs/v1/swagger.json get /shield/waf/custom-rule/{id} # Retrieve all available WAF enum mappings Source: https://bunny.net/docs/api-reference/shield/waf/retrieve-all-available-waf-enum-mappings https://api.bunny.net/shield/docs/v1/swagger.json get /shield/waf/enums # Retrieve all available WAF profiles Source: https://bunny.net/docs/api-reference/shield/waf/retrieve-all-available-waf-profiles https://api.bunny.net/shield/docs/v1/swagger.json get /shield/waf/profiles # Retrieve all available WAF rules for a Shield Zone Source: https://bunny.net/docs/api-reference/shield/waf/retrieve-all-available-waf-rules-for-a-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/waf/rules/{shieldZoneId} # Retrieve an AI recommendation for a triggered WAF rule Source: https://bunny.net/docs/api-reference/shield/waf/retrieve-an-ai-recommendation-for-a-triggered-waf-rule https://api.bunny.net/shield/docs/v1/swagger.json get /shield/waf/rules/review-triggered/ai-recommendation/{shieldZoneId}/{ruleId} # Retrieve custom WAF rules configured for the specified Shield Zone Source: https://bunny.net/docs/api-reference/shield/waf/retrieve-custom-waf-rules-configured-for-the-specified-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/waf/custom-rules/{shieldZoneId} # Retrieve the default WAF engine configuration Source: https://bunny.net/docs/api-reference/shield/waf/retrieve-the-default-waf-engine-configuration https://api.bunny.net/shield/docs/v1/swagger.json get /shield/waf/engine-config # Retrieve WAF rules segmented by subscription plan Source: https://bunny.net/docs/api-reference/shield/waf/retrieve-waf-rules-segmented-by-subscription-plan https://api.bunny.net/shield/docs/v1/swagger.json get /shield/waf/rules/plan-segmentation # Review all triggered WAF rules for the specified Shield Zone Source: https://bunny.net/docs/api-reference/shield/waf/review-all-triggered-waf-rules-for-the-specified-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/waf/rules/review-triggered/{shieldZoneId} # Review and update the action of a triggered WAF rule Source: https://bunny.net/docs/api-reference/shield/waf/review-and-update-the-action-of-a-triggered-waf-rule https://api.bunny.net/shield/docs/v1/swagger.json post /shield/waf/rules/review-triggered/{shieldZoneId} # Update an existing custom WAF rule Source: https://bunny.net/docs/api-reference/shield/waf/update-an-existing-custom-waf-rule https://api.bunny.net/shield/docs/v1/swagger.json put /shield/waf/custom-rule/{id} # Update an existing custom WAF rule Source: https://bunny.net/docs/api-reference/shield/waf/update-an-existing-custom-waf-rule-1 https://api.bunny.net/shield/docs/v1/swagger.json patch /shield/waf/custom-rule/{id} # List Files Source: https://bunny.net/docs/api-reference/storage/browse-files/list-files /api-reference/storage/openapi.json get /{storageZoneName}/{path}/ Retrieve a list of files and directories located in the given directory. # Storage API Reference Source: https://bunny.net/docs/api-reference/storage/index Upload, download, and manage files in your storage zones via HTTP. The Edge Storage API provides a simple RESTful interface for managing files in your storage zones. ## Base URL ``` https://{region}.storage.bunnycdn.com ``` The endpoint depends on your storage zone's primary region. See [storage endpoints](/docs/storage/http#storage-endpoints) for all available regions. ## Authentication Authenticate using the `AccessKey` header with your storage zone password: ```bash theme={null} curl --request GET \ --url https://storage.bunnycdn.com/{storageZoneName}/ \ --header 'AccessKey: YOUR_STORAGE_ZONE_PASSWORD' ``` Use your storage zone password, not your account API key. Find it in the **FTP & API Access** tab of your storage zone. ## SDKs # Delete File Source: https://bunny.net/docs/api-reference/storage/manage-files/delete-file /api-reference/storage/openapi.json delete /{storageZoneName}/{path}/{fileName} Delete an object from the storage zone. In case the object is a directory all the data in it will be recursively deleted as well. # Download File Source: https://bunny.net/docs/api-reference/storage/manage-files/download-file /api-reference/storage/openapi.json get /{storageZoneName}/{path}/{fileName} Returns the stored file at the given path. If the file does not exist, a 404 response will be returned. # Upload File Source: https://bunny.net/docs/api-reference/storage/manage-files/upload-file /api-reference/storage/openapi.json put /{storageZoneName}/{path}/{fileName} Upload a file to a storage zone based on the URL path. If the directory tree does not exist, it will be created automatically. **The file content should be sent as the body of the request without any type of encoding.** # Stream API Reference Source: https://bunny.net/docs/api-reference/stream/index Upload, manage, and deliver videos with the Stream API. The Stream API provides a RESTful interface for managing video libraries, uploading videos, and controlling playback settings. ## Base URL ``` https://video.bunnycdn.com ``` ## Authentication Authenticate using the `AccessKey` header with your Stream API key: ```bash theme={null} curl --request GET \ --url https://video.bunnycdn.com/library/{libraryId}/videos \ --header 'AccessKey: YOUR_STREAM_API_KEY' ``` Find your Stream API key in the **API** section of your video library settings. See [Authentication](/docs/stream/authentication) for details. ## Resources # Create Collection Source: https://bunny.net/docs/api-reference/stream/manage-collections/create-collection https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json post /library/{libraryId}/collections # Delete Collection Source: https://bunny.net/docs/api-reference/stream/manage-collections/delete-collection https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json delete /library/{libraryId}/collections/{collectionId} # Get Collection Source: https://bunny.net/docs/api-reference/stream/manage-collections/get-collection https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json get /library/{libraryId}/collections/{collectionId} # Get Collection List Source: https://bunny.net/docs/api-reference/stream/manage-collections/get-collection-list https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json get /library/{libraryId}/collections # Update Collection Source: https://bunny.net/docs/api-reference/stream/manage-collections/update-collection https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json post /library/{libraryId}/collections/{collectionId} # Add Caption Source: https://bunny.net/docs/api-reference/stream/manage-videos/add-caption https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json post /library/{libraryId}/videos/{videoId}/captions/{srclang} # Add output codec to video Source: https://bunny.net/docs/api-reference/stream/manage-videos/add-output-codec-to-video https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json put /library/{libraryId}/videos/{videoId}/outputs/{outputCodecId} # Cleanup unconfigured resolutions Source: https://bunny.net/docs/api-reference/stream/manage-videos/cleanup-unconfigured-resolutions https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json post /library/{libraryId}/videos/{videoId}/resolutions/cleanup # Create Video Source: https://bunny.net/docs/api-reference/stream/manage-videos/create-video https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json post /library/{libraryId}/videos # Delete Caption Source: https://bunny.net/docs/api-reference/stream/manage-videos/delete-caption https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json delete /library/{libraryId}/videos/{videoId}/captions/{srclang} # Delete Video Source: https://bunny.net/docs/api-reference/stream/manage-videos/delete-video https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json delete /library/{libraryId}/videos/{videoId} # Fetch Video Source: https://bunny.net/docs/api-reference/stream/manage-videos/fetch-video https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json post /library/{libraryId}/videos/fetch # Get Video Source: https://bunny.net/docs/api-reference/stream/manage-videos/get-video https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json get /library/{libraryId}/videos/{videoId} # Get Video Heatmap Source: https://bunny.net/docs/api-reference/stream/manage-videos/get-video-heatmap https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json get /library/{libraryId}/videos/{videoId}/heatmap Returns the attention heatmap for a specific video, showing relative viewer interest across the timeline. May be unavailable if the feature is disabled or there isn't enough viewing data. # Get Video heatmap data Source: https://bunny.net/docs/api-reference/stream/manage-videos/get-video-heatmap-data https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json get /library/{libraryId}/videos/{videoId}/play/heatmap # Get Video play data Source: https://bunny.net/docs/api-reference/stream/manage-videos/get-video-play-data https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json get /library/{libraryId}/videos/{videoId}/play # Get Video Statistics Source: https://bunny.net/docs/api-reference/stream/manage-videos/get-video-statistics https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json get /library/{libraryId}/statistics Returns time-series views and watch time, plus country-level aggregates, at the library level or for a specific video. Control the time window with dateFrom/dateTo and the granularity with hourly. Basic safeguards prevent spam and bot inflation by de-duplicating sessions and ignoring obviously invalid events. # Get video storage size info Source: https://bunny.net/docs/api-reference/stream/manage-videos/get-video-storage-size-info https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json get /library/{libraryId}/videos/{videoId}/storage # List Videos Source: https://bunny.net/docs/api-reference/stream/manage-videos/list-videos https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json get /library/{libraryId}/videos # Reencode Video Source: https://bunny.net/docs/api-reference/stream/manage-videos/reencode-video https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json post /library/{libraryId}/videos/{videoId}/reencode # Repackage Video Source: https://bunny.net/docs/api-reference/stream/manage-videos/repackage-video https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json post /library/{libraryId}/videos/{videoId}/repackage # Set Thumbnail Source: https://bunny.net/docs/api-reference/stream/manage-videos/set-thumbnail https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json post /library/{libraryId}/videos/{videoId}/thumbnail # Transcribe video Source: https://bunny.net/docs/api-reference/stream/manage-videos/transcribe-video https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json post /library/{libraryId}/videos/{videoId}/transcribe # Trigger Smart actions Source: https://bunny.net/docs/api-reference/stream/manage-videos/trigger-smart-actions https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json post /library/{libraryId}/videos/{videoId}/smart # Update Video Source: https://bunny.net/docs/api-reference/stream/manage-videos/update-video https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json post /library/{libraryId}/videos/{videoId} # Upload Video Source: https://bunny.net/docs/api-reference/stream/manage-videos/upload-video https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json put /library/{libraryId}/videos/{videoId} # Video resolutions info Source: https://bunny.net/docs/api-reference/stream/manage-videos/video-resolutions-info https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json get /library/{libraryId}/videos/{videoId}/resolutions # Get oembed Source: https://bunny.net/docs/api-reference/stream/oembed/get-oembed https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json get /OEmbed # Add Funds Source: https://bunny.net/docs/billing/add-funds Add credit to your bunny.net account balance. bunny.net operates on a Pay As You Go model. You add credit to your account, and charges are deducted as you use services. You only pay for what you actually use. You can add funds from the [Billing](https://dash.bunny.net/account/billing) page by clicking **Recharge Account**. Add funds ## Adding funds Navigate to [Billing](https://dash.bunny.net/account/billing) in your dashboard. Click the **Recharge Account** button. Select an amount: \$10, \$25, \$50, \$100, \$250, \$500, \$1000, \$2000, or enter a custom amount. Choose **Card or PayPal** or **Crypto**, then select or add a payment method. Click **Pay** to add the funds to your account. ## Accepted payment methods We accept the following payment methods with no extra fees: * PayPal * Visa * Mastercard * American Express * Discover * JCB * Diner's Club * Bitcoin * Apple Pay Your credit card information is never sent to bunny.net. Payment details are securely stored by Braintree. ## Applying a promo code If you have a promo code, click **Enter Promo Code** on the Billing page, enter your code, and click **Apply Code**. Enter Promo Code ## How billing works ### Traffic billing You are charged for each outbound byte transferred from the bunny.net network. Incoming traffic from your server to our network is free. If a 1GB video is viewed 100 times, you are charged for 100GB. Partial downloads are only charged for the bytes transferred. ### Storage billing Storage is charged based on usage in the past period, usually less than 24 hours. Data that is uploaded and deleted within this period is only charged for that time, not the whole month. ### Minimum monthly usage Your credits never expire. Note that trial credits are an exception: they are removed at the end of your trial period and any unused balance is not carried over. However, there is a \$1 minimum monthly usage while you have active zones on your account. If your usage is below \$1, it will be rounded up to \$1 for that month. This applies to the entire account, not per zone. ### Minimum account balance You need to maintain a positive balance to keep your services online. If your balance drops low, you'll receive warning emails. If your account is not recharged, your account will be suspended and your pull zones disabled. Data in storage zones will be deleted after a few days without backup. To avoid interruptions, consider enabling [Auto-Recharge](/docs/billing/auto-recharge) to automatically add funds when your balance gets low. # Best Practices Manual Source: https://bunny.net/docs/billing/affiliate-best-practices A step-by-step guide to getting started with the Bunny Affiliate Program. # Getting started with the Bunny Affiliate Program: Best practices You love bunny.net and want to share it with the world? Awesome! The Bunny Affiliate Program is here to help you do just that *and* earn some sweet rewards while you're at it. Whether you're new to affiliate marketing or a seasoned pro, here's a simple, step-by-step guide to get you hopping in the right direction. ## Step 1: Know your audience Before you start spreading the hop, take a breath and ask yourself who you're talking to. The better you understand your audience, the easier it is to show how [bunny.net](https://bunny.net/) can help them hop ahead. And the more carrots you'll collect along the way. Here are a few common examples: * **Game devs** facing dragging updates? Suggest Bunny CDN that delivers patches and updates at lightning speed. * **Bloggers or small businesses** frustrated with slow-loading sites? Show them how Bunny CDN speeds things up globally. * **Developers** building web apps or media-heavy sites? Recommend Bunny Storage + CDN for simple, blazing-fast delivery. **Tip**: Shape your message to fit the moment. A quick tweet for devs isn't the same as a YouTube deep dive for content creators. Meet people where they are, and speak to what they care about. ### Example scenarios (Steal these!) Here's how you can turn everyday conversations into affiliate wins: * **Online course creators / EdTech platforms:** "My course videos used to buffer like crazy for some students. Since switching to [bunny.net](https://bunny.net/), everything streams smoothly even halfway across the world." * **Tech blog post:** "I recently switched my blog to Bunny CDN and load times dropped by xy%. Here's how you can do it too (affiliate link)." * **Developer forum reply:** "If you're looking for a fast and reliable way to serve static files or video, I've had great results using bunny.net." *** ### Quick tips for maximum impact * **Be specific**: Don't just say it's great. Share what changed and how that impact showed up. * **Speak in your own voice**: Authentic beats polished. Your real experience goes further than any sales pitch. * **Show the results**: Speed tests, before-and-afters, real numbers. That's what builds trust and gets people's attention. *** ### Ready to hop in? Start small! One audience. One use case. One post. Share your story, add your link, and watch what happens. You might be surprised how fast the Bunny word gets around. *** ## Step 2: Pick your channels Now that you know *who* you're talking to, let's talk about *where*. There are tons of great places to share your bunny.net affiliate link. Just pick the ones that fit your style. ### 1. Blog about Bunny Got a blog? Sweet! That's the perfect place to share how bunny.net helped you and how it can help your readers hop ahead too. * **How:** Write a post like "5 Ways I Made My Website Blazing Fast" or a step-by-step tutorial. * **Pro tip:** Add your affiliate link where it fits naturally. No need to hard-sell. Just focus on telling your story. * **Example:** *"I shaved 5 seconds off my site's load time with bunny.net. Visitors don't even have time to blink! Want to try it too? Here's what I used: \[your link]."* ### 2. Create a YouTube video or vlog Prefer talking to typing? Video is gold, especially when you can *show* how bunny.net performs. * **How:** Record a tutorial, review, or even a casual "how I fixed my slow site" story. * **Pro tip:** Say your affiliate link out loud during the video *and* include it in the description. * **Example:** *"My site used to crawl. Seriously. But then I found bunny.net. It's been a total game changer. Want to see what I mean? Link's below!"* ### 3. Share on social media Whether you're into Twitter (X), Instagram, TikTok, or LinkedIn, your followers might love a speed boost too. * **How:** Post your experience, tips, or even funny "before and after" stories. * **Pro tip:** Use visuals. Think memes, GIFs, or screenshots. You can grab ready-to-go creatives from our affiliate assets library! * **Example post:** *"Why does my site load in the blink of an eye? Meet @BunnyCDN. They're behind the magic. Here's the link I used: \[your link]"* Don't forget to tag us! We'd love to reshare your content. * [Facebook](https://www.facebook.com/bunnycdn/) * [Twitter (X)](https://twitter.com/BunnyCDN) * [LinkedIn](https://www.linkedin.com/company/bunnynet) * [BlueSky](https://bsky.app/profile/bunny.net) * [Discord](https://discord.gg/bunnynet) ### 4. Email your friends or mailing list Got a newsletter or some tech-savvy friends? Send a friendly heads-up about your favorite new tool. * **How:** Keep it personal and helpful, like a tip you'd give your best friend. * **Pro tip:** Include a quick story or example of how bunny.net helped you. * **Example email snippet:** *"Hey! I found a service that made my site way faster. It's called bunny.net, super easy to use, and crazy fast. Thought you might want to try it too: \[your link]."* ### 5. Join the conversation in online communities Reddit, Discord, Facebook groups, and forums are places where people talk about tech, streaming, or websites. Wherever that happens, there's an opportunity. * **How:** Jump into threads where someone's struggling with slow load times or video issues. Share what worked for you. * **Pro tip:** Be genuinely helpful. That builds trust (and clicks). * **Example comment:** *"Totally had the same issue. bunny.net helped me drop my load time from 6s to under 2s. Might be worth a try: \[your link]."* **Mix and match!** You don't have to be everywhere. Just pick 1-2 channels where you feel comfortable and start there. Track what works and scale from there. *** ## Step 3: Nail your message No matter where you're sharing, blog, video, tweet, or TikTok, it's not just *what* you say, it's *how* you say it. That's what makes people click. Here's how to make your message land: ### Be real People can spot a sales pitch from a mile away, but they'll listen to a real story. * **Share your journey:** What problem did you face, and how did bunny.net help? * **Keep it casual:** Write like you're texting a friend, not writing an ad. **Example:** *"My videos used to buffer all the time. Switched to [bunny.net](https://bunny.net/) and haven't had issues since!"* ### Make the value shine Make it clear what's in it for them. Highlight real benefits, like: * Speeding up websites * Buffer-free video streaming * Reliable, low-latency file storage * Tools developers actually enjoy using **Try this:** *"Need a faster site without the tech headache? bunny.net is made for that."* ### Add a natural call-to-action Guide your audience toward action, but do it like a helpful friend, not a pushy salesperson. Swap this: *"Click this amazing link now!"* For this: *"Want to speed up your site too? Check out what worked for me." or this "Give it a try. I think you'll love how smooth your site gets."* ### Use numbers (they're powerful) People love results. If you have real improvements, share them! * *"Cut my load time by 3 seconds."* * *"Pages now load 60% faster."* * *"My bounce rate dropped by 25% after switching to bunny.net."* **Example Post:** *"It's faster, costs less, and it's quick and easy to set up. If you're interested, here's my link."* *** ## Step 4: Track your progress You've put your bunny.net link out into the world, nice work! Now it's time to see how it's doing. Just hop over to the **Affiliate** section in your bunny.net dashboard, and you'll find all the juicy stats you need: * **Referrals:** See how many people clicked your link and signed up. * **Rewards:** Track how much carrot power you've racked up! * **Payouts:** Request a payout or add your affiliate carrots to your Bunny balance. *** ## Step 5: Experiment, test, and have fun! The best part? You don't need to get it perfect on the first try. In fact, trying new things is half the fun and the secret to long-term success. Here's how to keep things fresh and effective: * **A/B test** Try two versions of a tweet, video title, or blog headline. See which one gets more clicks, and learn what your audience loves! * **Engage with your people** Got questions or comments? Drop a reply, say thanks, or ask for feedback. Building trust = more conversions. * **Stay consistent** Share often! A quick tip today, a story next week, a follow-up video later, it all adds up. **Example ideas to test:** * A side-by-side "before and after" speed test video * A tweet thread breaking down "How I fixed my slow website in 3 steps" * An Instagram Reel showing your setup with Bunny Stream in action *** ## That's a wrap! With these steps under your belt, you're ready to hop into the Bunny Affiliate Program with confidence. Remember, it's not about being perfect, it's about being *genuine*, helpful, and a little creative. So go ahead. Share your story, track your progress, try new things, and have fun while you earn! **P.S. Need help?** We've got your back! Just reach out to us anytime at [affiliate@bunny.net](mailto:affiliate@bunny.net), and we'll be happy to help. Happy hopping! # Affiliate Guidelines Source: https://bunny.net/docs/billing/affiliate-guidelines Guidelines for promoting bunny.net through the Bunny Affiliate Program. # Welcome to the Bunny Affiliate Program! We're thrilled to have you hop on board, spread the Bunny word (the right way!), and help more people discover a faster, smarter, and simpler way to deliver content at the edge. ## 1. Keep it real People relate to real stories, not sales talk, so share what your experience with bunny.net has actually been like. What made you hop on? What surprised you? What convinced you to stick with us? Share that. Keep it honest, personal, and grounded in what actually makes Bunny awesome for you. No need to overpromise. Your genuine take is what will resonate with everyone most. **Example:** "bunny.net will make your site the fastest in the world!" "What I love most about [bunny.net](https://bunny.net/) is that it just works. I don't think about it, which is exactly what I want from my infrastructure." "I'm not a CDN expert, but setup with [bunny.net](https://bunny.net/) took me maybe 10 minutes. It was really easy." ## 2. Share with care, not clutter Nobody likes a random link drop. Don't spam forums or stuff comment sections with your affiliate code just to chase clicks. That's not how we hop around here. Instead, look for moments where someone's stuck: a slow site, scaling issues, or delivery pains, and share how Bunny helped you solve something similar. Real help, real stories, real value. That's what builds trust. And that's what gets your link the right kind of attention. **Example:** Dropping your link where it doesn't belong? That's a fast track to losing trust. Someone asks how to speed up their website? That's a perfect moment to mention bunny.net and share your link! ## 3. Keep it in kind corners of the internet Bunny is built to make the internet hop faster, safer, and more fun. So let's keep that spirit in where and how you share. Please don't promote your affiliate link on sites that spread harm, hate, or anything illegal. That's not what we're about. Stick to spaces that reflect the same values we share: honesty, creativity, and helping others hop ahead. **Example:** No links on sites promoting hate speech, violence, or illegal activities. Share your link where it can truly help, like on your blog, in dev communities, or your social media channels. ## 4. Play by the platform's rules Every platform has its own playbook, and part of earning trust is respecting it. If Instagram wants a #ad, add it. If a forum says no affiliate links, don't try to sneak one in. Clear, honest, and upfront is how Bunny rolls. Transparency builds credibility and makes your recommendations go even further. **Example:** Disclosing your affiliate links builds trust and shows you play by the rules, which is exactly how our Bunnies hop. ## 5. Show off the Bunny brand (the right way) Our brand is part of what makes Bunny special. So let's keep it consistent and unmistakably Bunny. Feel free to use [official bunny.net logos and visuals](https://www.figma.com/design/hXVOrM9Jg4IlVw3femLqXr/Bunny-Promo-Assets?node-id=0-1\&t=7ZeexKH5n0nUH4dm-1), or create your own assets that follow our brand guidelines. Just don't copy content from other affiliates or slap together stuff that doesn't feel like Bunny. We want your creativity to shine within the lines that keep our brand strong. **Example:** Don't copy a competitor's logo and slap it on your post. [Grab Bunny-approved banners from the library.](https://www.figma.com/design/hXVOrM9Jg4IlVw3femLqXr/Bunny-Promo-Assets?node-id=0-1\&t=7ZeexKH5n0nUH4dm-1) ## 6. Be upfront: trust starts there People appreciate honesty, especially online! So when you're sharing an affiliate link, make it crystal clear. Not just because it's often legally required (which it is), but because it shows respect for your audience. **Example:** "This is an affiliate link, which means I may earn a commission if you sign up." ## 7. Share the Bunny story. No need to throw shade. We're proud of what we've built, and there's no need to tear others down to lift Bunny up. Talk about what Bunny does well, not what others don't. Focus on what makes Bunny awesome for you and let that speak for itself. **Example:** "Other CDNs are slow and expensive. Use bunny.net instead!" "I switched to bunny.net because it's fast, affordable, and super easy to use!" ### Questions? Not sure about something? Just ask us at **[affiliate@bunny.net](mailto:affiliate@bunny.net).** We're here to help! *** ### By joining, you agree to: * Share bunny.net responsibly * Use your affiliate link with honesty and care * Follow these simple guidelines * Promote us in ways that are ethical, kind, and professional We're super excited to have you as part of the Bunny Affiliate crew! By following these simple guidelines, you'll not only **earn rewards** but also help us keep Bunny's reputation strong and trustworthy. If you have any questions, we're just an email away! # Affiliate Program Source: https://bunny.net/docs/billing/affiliate-program Now you can earn carrots for helping others hop faster too. You know Bunny. You love Bunny. Now you can earn carrots for helping others hop faster too. Sign up, get your unique link, and share it with your friends, followers, or fellow devs. When they hop on and become paying customers, you get rewarded. No complicated rules. Just a simple way to turn your Bunny experience into real wins (for them *and* for you). ### Who is eligible for the Bunny Affiliate Program? If you're a paying [bunny.net](https://bunny.net/) customer, you're in! The program is open to anyone already hopping with us. ### How do I join the Affiliate Program? Easy! Just log in to your [bunny.net](https://bunny.net/) account, click your profile icon (top left), and hop to the Affiliate Program page. Enable it with one click, grab your unique link, and you're ready to start sharing. image.png ### How much do I get for every person I refer? You earn **\$20** for every new paying customer who signs up through your link. Simple as that. Your rewards and referrals live in the Affiliate tab. Easy to find and easy to follow. ### Is there a limit on how much commission I can earn? Not at all. There's no cap on how much you can earn. Every new paying customer who signs up through your link means more rewards in your warren. Keep sharing, keep earning, and watch the carrots roll in. ### When can I expect my payout? Anytime you're ready. Just hop into your Affiliate page, request a payout, and choose the method that works in your region. Easy. Want even more bounce for your buck? You can turn your rewards into Bunny credits and get a **25% bonus!** Perfect for powering up your next project. Claim \$20, get \$25. Just like that. ### Can I create my own marketing materials? Yes please! We love seeing your creativity hop to life! You're free to design your own assets, write your own copy, or build your own banners. Just make sure everything follows our brand [guidelines](/docs/billing/affiliate-guidelines) so it still feels unmistakably Bunny. Need a head start? We've got [a stash of official logos, graphics, and goodies](https://www.figma.com/design/hXVOrM9Jg4IlVw3femLqXr/Bunny-Promo-Assets?node-id=0-1\&t=7ZeexKH5n0nUH4dm-1) ready for you to grab and go. ### How do I track my referrals and earnings? Curious how your link's performing? Hop into your dashboard and head to the Affiliate page. You'll see every click, signup, and successful referral, plus a clear view of your earnings and past payouts. All the good stuff, in one place. ### What is considered a valid referral? It starts with your link. Someone clicks it, signs up for [bunny.net](https://bunny.net/), and gives it a go. Once they become a paying customer, boom. That's a valid referral. And just like that, your reward is on the way. ### What are the rules for promoting my affiliate link? We're all for creativity. In fact, we love seeing the unique ways you share Bunny. Just keep it kind, clear, and honest. No spam. No shady shortcuts. And no aggressive ads. Here's the rule of thumb: share the way *you'd* want someone to share with you. For more tips on what's cool (and what's not), check out the full [Affiliate Guidelines](/docs/billing/affiliate-guidelines). ### How do payouts work? Once you've earned rewards, you've got two ways to collect your carrots. **Want instant credit?** You can convert your affiliate balance into Bunny credits anytime. No minimum required. We'll even boost it with a **25% bonus**. Add \$20 and we'll drop \$25 into your account. Just like that. **Prefer a monetary payout?** You'll need at least **\$100** in rewards before requesting one. Once you hit the threshold, just hop over to your Affiliate Program page to request your payout. We send monetary payouts through our partner [Tremendous](https://www.tremendous.com/). They offer a range of methods, such as PayPal or a virtual VISA card, and the ones you can pick depend on where you are. So does the currency you get paid in. Tremendous checks your location when you redeem your reward, then shows the options it supports there. To see what your region gets, read [Sending international rewards](https://help.tremendous.com/hc/en-us/sections/41475572352275-Sending-international-rewards). Tremendous cannot deliver a reward to every recipient. Certain limitations apply on their side, so check [Tremendous recipient restrictions](https://help.tremendous.com/hc/en-us/articles/41472341152019-Understand-recipient-restrictions) before you count on a monetary payout. If a monetary payout isn't available to you, you can still convert your balance into Bunny credits. image.png ### Can I participate from any location? Yes! Whether you're coding from São Paulo, designing in Tokyo, or building dreams in the Swiss Alps, you're welcome to join. If [bunny.net](https://bunny.net/) works in your country, you're good to go. The Affiliate Program is open worldwide, so no matter where you hop in from, you can share, earn, and be part of the Fluffle. Collecting a monetary payout is a slightly different story. Those go through our partner Tremendous, so the options you get depend on your region, and certain limitations apply on their side. Converting your balance into Bunny credits works everywhere, so there's always a way to spend what you earn. See [How do payouts work?](#how-do-payouts-work) before you count on a monetary payout. # Auto-Recharge Source: https://bunny.net/docs/billing/auto-recharge Automatically add funds when your balance runs low. Auto-Recharge automatically tops up your account when your balance gets low or is about to expire. Once enabled, you never have to worry about manually adding funds. You can enable Auto-Recharge from the [Billing](https://dash.bunny.net/account/billing) page. Auto-Recharge ## How it works bunny.net operates on a pre-paid model where credits are deducted from your balance as you use services. Auto-Recharge triggers when your balance drops below \$5.00, automatically charging your saved payment method for the amount you configure. ## Setting up Auto-Recharge Navigate to [Billing](https://dash.bunny.net/account/billing) in your dashboard. Click the **Enable Auto-Recharge** button. Select a recharge amount: \$10, \$25, \$50, \$100, \$250, \$500, \$1000, \$2000, or enter a custom amount. Choose from your saved payment methods. To add a new payment method, use it first to manually recharge your account. Click **Set up Auto-Recharge** to enable. ## Supported payment methods Auto-Recharge supports Mastercard, Visa, American Express, and PayPal. ## Troubleshooting ### My Auto-Recharge payment didn't trigger To prevent excessive retries, Auto-Recharge only triggers every six hours. Your balance might temporarily appear below the threshold without a payment triggering. Wait a couple of hours to see if the payment executes. If your balance continues to drop after 24 hours with no payment, the Auto-Recharge payments are likely failing. This could be due to: * Credit card authorization failure * Insufficient balance on the card * Rejected payments * Expired credit card We only retry a card up to 5 times. After consecutive failures, you'll need to trigger a payment manually. ### Fixing failed Auto-Recharge Try removing and re-adding your saved payment method. This re-authorizes the card for automatic charges. If you're still unable to make a payment, [contact support](https://bunny.net/contact). # Billing History & Invoices Source: https://bunny.net/docs/billing/billing-history View past transactions and download invoices and usage summaries. The [Billing History](https://dash.bunny.net/account/billing-history) page shows all your account transactions, including monthly usage, payments, and applied promo codes. Billing history ## What you'll see The billing history displays: * **Date** of the transaction * **Description** of the charge or credit * **Amount** * **Type** (Monthly usage, PayPal, Coupon code, etc.) A chart at the top shows your account balance over time. ## Downloading invoices For payments made via PayPal or card, click **Download invoice** to get a PDF invoice for your records. ## Downloading usage summaries For monthly usage entries, click **Download summary** to get a detailed breakdown of your charges for that period. The usage summary includes: * Your billing details * Starting and ending balance * Total activity, credits, payments, and refunds * Itemized usage by service (traffic by region, storage, scripting, etc.) Usage summaries are not invoices. They are a detailed breakdown of billing activity for the stated period. # Sales Tax Source: https://bunny.net/docs/billing/sales-tax Information about U.S. sales tax on bunny.net services. All prices in our dashboard and on our website are shown without any tax. We apply U.S. sales tax on digital services when required by the customer's state and local laws. This affects only customers in states or localities that tax digital services. U.S. sales tax on digital services is not consistent across the country. Some states tax digital services, others do not, and some apply tax only in specific cases. Rules and rates also change over time. ## What this means for your invoices * **Only the tax line may change.** Your service prices stay the same. * **Tax depends on your billing address.** If your billing address is in a state or locality that taxes digital services, sales tax will appear on the invoice. If not, you will not be charged sales tax. * **Rates can change.** States update digital service tax rules and rates regularly. We apply the latest available rules and rates at the time of invoicing. ## How we determine your tax We calculate sales tax based on: * **Your billing address state and locality.** U.S. sales tax is mainly state based, and many states allow local add-ons. * **How that state taxes digital services.** Every state defines digital service taxability differently. * **Your usage for the billing period.** Tax is applied to the taxable part of your invoice based on what you used. Because rules vary, two customers with the same usage can see different tax results if they are in different states. ## Changing your billing address You can [change your billing address](/docs/account#billing-address) once per month. If you update your address, the new tax jurisdiction and rate take effect on the first day of the next month. We do not change tax rates mid-month because invoices must reflect one jurisdiction per billing period. This is a common compliance requirement. ## Tax exemptions If your organization is tax exempt, send us a valid state exemption certificate. After verification, we will stop charging sales tax on future invoices as required by that state. Exemptions are state specific. The certificate must match the state on your billing address. We cannot remove tax retroactively unless state rules allow it and your certificate covered that period. # CDN Acceleration Source: https://bunny.net/docs/cdn/cdn-acceleration Connect your domain to a Pull Zone using Bunny DNS for automatic CDN routing and simplified SSL. CDN Acceleration routes your domain's traffic through the Bunny CDN automatically when using Bunny DNS. Static assets (images, CSS, JS, videos) are served from Bunny's global network without manually configuring Pull Zones for each subdomain. ## Benefits * Connect your domain to the CDN with a few clicks * Automatic SSL via Let's Encrypt (no manual certificate setup) * Reduced load on your origin server * Works with root domains (no CNAME limitation) ## Prerequisites Your domain must use Bunny DNS. Update your domain's nameservers at your registrar to: * `coco.bunny.net` * `kiki.bunny.net` ## Setup 1. Go to **DNS** in your [bunny.net dashboard](https://dash.bunny.net) 2. Click on the domain you want to accelerate 1. Find the **CDN Acceleration** button (or CDN Proxy column) 2. Click **Enable** Bunny automatically creates a Pull Zone and configures SSL for your domain. If you have an existing Pull Zone with the same hostname, delete that hostname from the Pull Zone first. Hostnames can only be mapped to one Pull Zone at a time. Visit your site. Static assets should now be served through bunny.net's CDN. The green **CDN Proxy** icon next to your DNS record confirms successful setup. You can also check **Monitoring** > **Logs** and filter by your accelerated pull zone to confirm requests are being logged. Open your browser's Developer Tools (**F12** or right-click > **Inspect**), go to the **Network** tab, and refresh the page. Click on a static file (`.jpg`, `.css`, `.js`) and check for these headers: | Header | Description | | ------------------------ | ----------------------------------------- | | `cdn-pullzone` | The Pull Zone handling the request | | `cdn-cache` | Cache status (`HIT`, `MISS`, or `BYPASS`) | | `cdn-requestcountrycode` | Country from which the request was served | If these headers are present, your content is being served through bunny.net's CDN. ## SSL certificate timing Bunny issues SSL certificates via Let's Encrypt within seconds of enabling acceleration. If HTTPS doesn't work immediately, wait a few minutes for the certificate to propagate. Ensure your nameservers point to Bunny DNS before enabling acceleration. If the domain isn't resolving through Bunny yet, SSL certificate verification will fail. # Changelog Source: https://bunny.net/docs/cdn/changelog Latest updates and improvements to the CDN. ## IP Family Policy for Pull Zones Pull Zones now support an IP Family Policy that controls which address families (IPv4, IPv6, or both) are advertised and which edge servers are eligible to serve the zone. Four policies are available: IPv4 Only, Dual Stack (default), Dual Stack Prefer IPv6, and IPv6 Only. Token authentication IP locking now supports IPv6 addresses, binding at the client's `/64` prefix. Enabling Token IP Validation pins the zone to a single address family automatically. [Learn more](/docs/cdn/ip-family-policy) ## HTTP validation for Seamless Domain Migration Seamless Domain Migration now supports HTTP (file-based) validation alongside the existing DNS (TXT record) method for issuing SSL certificates before pointing your domain to bunny.net. Choose DNS if you control your domain's DNS records, or HTTP if you can place a challenge file on the server your domain currently points to. [Learn more](/docs/cdn/seamless-migration) ## CDN Connectivity documentation New documentation covering IPv4 and IPv6 connectivity across the bunny.net CDN, including network tier support, origin connectivity, CDN Acceleration with dual-stack origins, and Origin Shield for guaranteed IPv6 connectivity. [Learn more](/docs/cdn/connectivity) ## Vary Cache by Request Headers You can now vary cached content based on the value of one or more request headers, such as `Accept-Language`. Each unique header value combination creates a separate cache entry, enabling localized content, feature flags, and device-specific experiences without sacrificing CDN caching. [Learn more](/docs/cdn/vary-cache) ## WebSocket pay-as-you-go pricing WebSocket billing has moved to pay-as-you-go pricing based on connection-minutes, replacing the previous tiered monthly subscription model. Connection limits can now be configured from 100 to 25,000 concurrent connections per zone. [Learn more](/docs/cdn/websockets) ## CDN Logging API enhancements The CDN Logging API now supports exact request ID lookup and includes additional fields such as JA4 TLS client fingerprint, ASN, and ASN organization in the response. [Learn more](/docs/cdn/logging/index) ## CDN Logging API v2 The CDN Logging API v2 now returns structured JSON with filtering by status, cache status, country, IP, edge location, URL, User-Agent, and Referer. [Learn more](/docs/cdn/logging) ## Per-request control for Request Coalescing You can now enable or disable Request Coalescing per request using Edge Rules, allowing more granular control over caching behavior. [Learn more](/docs/cdn/request-coalescing) ## Seamless Domain Migration Introducing seamless domain migration with SSL certificate issuance via DNS verification, allowing zero-downtime transitions to bunny.net. [Learn more](/docs/cdn/seamless-migration) ## JA4 fingerprinting JA4 TLS fingerprinting is now available on all pull zones. Identify clients based on their TLS handshake characteristics via the `CDN-JA4` request header. [Learn more](/docs/cdn/security/ja4-fingerprinting) ## Edge Rule pattern matching Use Lua-based pattern matching in Edge Rule conditions to match structured request values such as URLs, headers, cookies, and query strings. [Learn more](/docs/cdn/edge-rules/pattern-matching) ## Edge Rule pattern matching You can now use Lua-based pattern matching in Edge Rule conditions to match structured request values such as URLs, headers, cookies, and query strings. [Learn more](/docs/cdn/edge-rules/pattern-matching) ## JA4 fingerprinting JA4 TLS fingerprinting is now available on all pull zones. Identify clients based on their TLS handshake characteristics, and access the fingerprint via the `CDN-JA4` request header. [Learn more](/docs/cdn/security/ja4-fingerprinting) ## Logging and Permanent Log Storage updates Logging documentation has been updated with improved details on log retention, and Permanent Log Storage now includes specifics on part rotation, file size limits, and GZip compression. [Learn more](/docs/cdn/logging/permanent-storage) # Connectivity Source: https://bunny.net/docs/cdn/connectivity Learn how bunny.net supports IPv4 and IPv6 connectivity across our global network. bunny.net supports both IPv4 and IPv6 connectivity across our global edge network. Our IPv6 support continues to expand as we upgrade connectivity throughout our infrastructure and partner networks. ## Network Support bunny.net operates two network tiers with different IPv6 support levels: | Network | Coverage | IPv6 Support | | ---------------- | ----------------------- | ----------------- | | Standard Network | 119+ Points of Presence | Best effort | | Volume Network | 10 Points of Presence | Native dual-stack | The **Volume Network** is fully dual-stack, providing native IPv4 and IPv6 connectivity throughout the entire delivery path. The **Standard Network** offers IPv6 support on a best-effort basis. While many locations already support dual-stack connectivity, some edge locations remain IPv4-only due to upstream network limitations or regional infrastructure constraints. ## CDN Ingress Connectivity When users connect to bunny.net, DNS responses are generated based on the capabilities of the serving location. * **A records** are always available for IPv4 connectivity. * **AAAA records** are returned when the serving Point of Presence supports IPv6 connectivity. * Clients with IPv6 connectivity will automatically connect over IPv6 whenever possible. This approach ensures optimal compatibility while allowing IPv6-capable locations to serve traffic natively over IPv6. Each Pull Zone can override this default behavior with an [IP Family Policy](/docs/cdn/ip-family-policy), which controls whether we advertise IPv4, IPv6, or both, and whether routing prefers IPv6-capable locations. ## Origin Connectivity We support both IPv4 and IPv6 connections to your origin servers. You can configure origins using: * Hostnames that resolve to **A records** * Hostnames that resolve to **AAAA records** * Dual-stack hostnames that resolve to both **A and AAAA records** * Direct IPv6 addresses enclosed in square brackets (`[]`) according to RFC standards Examples: ```text theme={null} https://origin.example.com https://[2001:db8::1] ``` When a hostname resolves to both IPv4 and IPv6 addresses, we will attempt to establish connectivity using the best available path based on the capabilities of the serving edge location. ### Volume Network Origins The Volume Network provides native dual-stack connectivity to origins. Edge servers within the Volume Network can establish connections to both IPv4 and IPv6 origin infrastructure. ### Standard Network Origins On the Standard Network, IPv6 origin connectivity is provided on a best-effort basis. If the serving edge location has IPv6 connectivity to the origin, we can connect to IPv6-enabled origins. Otherwise, IPv4 connectivity will be used. If guaranteed IPv6 connectivity to your origin is required, enabling Origin Shield routes all origin traffic through a Volume Network location with native dual-stack support. ## 502 and 504 errors A `502 Bad Gateway` means an edge server received an invalid response from your origin. A `504 Gateway Timeout` means the origin didn't respond in time. When the origin is unreachable, edge servers serve from cache where possible and only return the error when the file isn't cached. Common causes: * **Origin offline:** The most common cause. Edge servers can't connect to your origin and the file isn't in cache. * **Firewall blocking edge servers:** Because the CDN proxies all your traffic, origin firewalls or security software can mistake the volume of edge requests for an attack and block them. Whitelist the [edge server IP list](https://api.bunny.net/system/edgeserverlist) ([IPv6](https://api.bunny.net/system/edgeserverlist/ipv6)) and update it periodically as the network changes. * **Network congestion:** A rare, transient routing problem between an edge location and a distant origin. * **Timeouts:** By default Bunny waits 10 seconds for a TCP connection and 60 seconds for the origin to start sending data. Immediate 502s usually mean an origin firewall or WAF is rejecting the connection. These timeouts are configurable via Safehop. Use the [bunny.net diagnostic tools](https://tools.bunny.net/) to test latency, traceroutes, and HTTP requests from over 120 locations and see whether specific regions are blocked or the origin is slow. If you've ruled out the causes above, contact [support@bunny.net](mailto:support@bunny.net). ## Origin redirect loops If your CDN URLs return a 301 redirect back to your own domain, the origin is issuing that redirect and the CDN is forwarding it. Bunny CDN never returns a 301 on its own, except for hotlink protection. Check the following: * **Origin URL host:** Make sure the Pull Zone Origin URL matches the exact domain your site serves from, including the `www` or non-`www` version. A mismatch triggers your origin's canonical redirect. * **Origin URL protocol:** If your server forces HTTPS, set the Origin URL to `https://`. An `http://` Origin URL against an HTTPS-only origin produces a redirect. * **Origin redirect or rewrite rules:** Check your server configuration for redirect or rewrite rules affecting your assets. * **Hotlink protection:** If enabled, whitelist every domain the CDN serves (both `www` and non-`www`), with no protocol or slashes in the hostname field. After any change, purge the full Pull Zone cache and wait for it to sync, since cached 301s persist on the edge. ## CDN Acceleration and IPv6 Origins bunny.net's CDN Acceleration feature supports dual-stack origin connectivity when used with Bunny DNS. When accelerating DNS records, we automatically detect matching IPv4 and IPv6 records for the same hostname and use both when establishing connectivity to your origin. For example, if your DNS zone contains: ```dns theme={null} www A 203.0.113.10 www AAAA 2001:db8::10 ``` and the **A record** is configured for CDN Acceleration, we will automatically discover the matching **AAAA record** and attempt to use both addresses when connecting to your origin. This enables: * Dual-stack origin connectivity through CDN Acceleration * Automatic IPv6 support without additional configuration * Improved resiliency by allowing both IPv4 and IPv6 paths to be used when available As with standard origin connectivity, IPv6 connectivity depends on the capabilities of the serving edge location. Volume Network locations provide native dual-stack connectivity, while Standard Network locations provide IPv6 connectivity on a best-effort basis. When accelerating a hostname through Bunny DNS, matching A and AAAA records for the same hostname are automatically considered for origin connectivity. You do not need to separately accelerate both record types. ## Origin Shield and IPv6 Origins For customers using the Standard Network who require guaranteed IPv6 connectivity to their origin infrastructure, we recommend enabling Origin Shield. Both Origin Shield locations: * **Chicago, IL** * **Paris, France** are part of the Volume Network and provide native dual-stack connectivity. When Origin Shield is enabled, all origin fetches are routed through the selected shield location rather than directly from the serving edge location. This ensures that communication between bunny.net and your origin always originates from a Volume Network Point of Presence with native IPv4 and IPv6 support. This configuration is particularly useful when: * Your origin is reachable only over IPv6 * You want to guarantee IPv6 connectivity regardless of which Standard Network edge location serves the request * You want to combine the global coverage of the Standard Network with reliable dual-stack origin connectivity Origin Shield can be used to guarantee IPv6 connectivity to your origin even when requests are served from Standard Network locations that do not currently have IPv6 connectivity to the origin. ## IPv6 Expansion Expanding IPv6 support remains an ongoing priority across our network. A significant portion of our infrastructure already operates in dual-stack mode, to achieve full IPv6 coverage across more than 119 Points of Presence we are actively coordinating with upstream providers and local network operators worldwide. Some locations currently operate in environments where native IPv6 connectivity is not yet available. As connectivity improves, IPv6 support will continue to expand across additional edge locations without requiring any changes to your CDN configuration. IPv6 support on the Volume Network is available across all locations. On the Standard Network, IPv6 availability depends on the capabilities of the serving Point of Presence and its upstream connectivity. # Custom 404 Page Source: https://bunny.net/docs/cdn/custom-404-page Display a branded error page when files are not found in your storage zone. When using a Storage Zone as your Pull Zone origin, you can configure a custom 404 page to display when requested files don't exist. ## Setup In the root of your storage zone, create a folder named `bunnycdn_errors`. Create a file named `404.html` with your custom HTML content and upload it to the `bunnycdn_errors` folder. To return an image instead, upload a file named `404.png` and remove any `404.html` file. Navigate to a non-existent URL on your Pull Zone to confirm your custom 404 page appears. ## Folder structure ``` /your-storage-zone/ └── bunnycdn_errors/ └── 404.html ``` This feature only works when your Pull Zone origin is a Bunny Storage Zone. For external origins, configure 404 handling on your origin server. # Custom Hostname Source: https://bunny.net/docs/cdn/custom-hostname Serve content from your own branded domain like cdn.yourdomain.com. Use a custom hostname to serve CDN content from your own domain instead of the default bunny.net URL. 1. Open your pull zone in the [dashboard](https://dash.bunny.net) 2. Locate the **Hostnames** panel in the General section 3. Enter your custom domain (e.g., `cdn.yourdomain.com`) 4. Click **Add hostname** Add custom hostname The hostname appears in the Linked Hostnames list. Copy the CNAME record value shown below the hostname field. You'll need this for the next step. Copy custom hostname CNAME Add a CNAME record at your DNS provider: 1. Log in to your DNS provider (Cloudflare, Route 53, or similar) 2. Create a new **CNAME** record: * **Name/Host**: Your subdomain (e.g., `cdn`) * **Type**: CNAME * **Value**: The CNAME from step 2 (e.g., `yourzone.b-cdn.net`) * **TTL**: 3600 seconds or lower On Cloudflare, disable the proxy option (orange cloud icon). Proxying interferes with DNS resolution and prevents your custom domain from working with bunny.net. Save the DNS record. Your domain connects to bunny.net once DNS propagates, typically within a few minutes to an hour. Click **Verify & Activate SSL** to get a free Let's Encrypt certificate for your custom hostname. Verify and activate SSL If you skip SSL verification initially, find your hostname in the **Linked Hostnames** section and click **Enable** to activate a free certificate or upload your own. Enable custom hostname SSL ## Root domains CNAME records cannot be created at the apex level (`yourdomain.com`) per the DNS specification. Most DNS providers don't support non-standard record types like ALIAS or ANAME that work around this limitation. Pull Zones do not support direct IP address connections. There is no stable anycast IP that can be used with A records to point a domain to a Pull Zone. You can use a direct IP as the origin with a custom Host header, but pointing an A record to a specific Bunny IP is not supported. See [How to set up a direct IP origin URL](/docs/cdn/edge-rules/ip-origin) for more information. To use a root domain with your Pull Zone, you have two options: 1. **Use Bunny DNS (recommended)**: Bunny DNS supports CNAME flattening on apex domains, allowing you to point your root domain directly to a Pull Zone. See [CDN Acceleration](/docs/cdn/cdn-acceleration) for setup instructions. 2. **Redirect apex to www**: Create a CNAME for `www.yourdomain.com` pointing to your Pull Zone, then set up an HTTP redirect from your root domain to `www`. # Custom Cache Time for File Extensions Source: https://bunny.net/docs/cdn/edge-rules/custom-cache-time Use Edge Rules to set custom cache durations for specific file types. Edge Rules make it easy to customize cache times based on file extensions. This is useful when different content types need different caching strategies—for example, caching video files longer than HTML pages. ## Use cases * Cache static assets (images, CSS, JS) for longer periods * Set shorter cache times for dynamic content * Cache video files for extended periods to reduce origin load * Override default cache behavior for specific file types ## Configuration Navigate to your pull zone and select **Edge Rules** from the side menu. Click **Add Edge Rule** to create a new rule. Choose **Override Cache Time** as the action. Enter the desired cache time in seconds: | Duration | Seconds | | -------- | --------- | | 1 hour | `3600` | | 1 day | `86400` | | 1 week | `604800` | | 1 month | `2592000` | Click **Add Condition** and select **File Extension** as the condition type. Enter the extension you want to match (without the dot). For example: * `mp4` for video files * `jpg` for JPEG images * `css` for stylesheets Click **Save Edge Rule** to activate. ## Examples ### Cache video files for 30 days | Setting | Value | | -------------- | ------------------- | | **Action** | Override Cache Time | | **Cache Time** | `2592000` (30 days) | | **Condition** | File Extension | | **Extension** | `mp4` | ### Cache images for 1 week | Setting | Value | | -------------- | ------------------- | | **Action** | Override Cache Time | | **Cache Time** | `604800` (7 days) | | **Condition** | File Extension | | **Extension** | `jpg` | Create separate rules for each file extension, or use multiple conditions with **Match Any** to apply the same cache time to multiple extensions. ### Bypass cache for HTML files To prevent caching of HTML files entirely: | Setting | Value | | -------------- | ------------------- | | **Action** | Override Cache Time | | **Cache Time** | `0` | | **Condition** | File Extension | | **Extension** | `html` | Setting cache time to `0` will bypass the edge cache if the content is not already cached. This increases origin load but ensures fresh content. ## Multiple extensions To apply the same cache time to multiple file types, you have two options: ### Option 1: Multiple conditions (Match Any) Add multiple **File Extension** conditions to a single rule and set the match type to **Match Any**: * Condition 1: File Extension = `jpg` * Condition 2: File Extension = `png` * Condition 3: File Extension = `gif` ### Option 2: Separate rules Create individual rules for each extension. This gives you more flexibility if you later need different cache times per extension. ## Cache time vs Browser cache time Edge Rules offer two cache-related actions: | Action | Description | | ------------------------------- | ------------------------------------------------------------- | | **Override Cache Time** | Controls how long content is cached on bunny.net edge servers | | **Override Browser Cache Time** | Controls the `Cache-Control` header sent to browsers | You can use both together to have different cache durations at the edge vs in the browser. ## Related * [Smart Cache](/docs/cdn/smart-cache) - Default cache settings for your pull zone * [Trigger Path Setup](/docs/cdn/edge-rules/trigger-path) - Alternative way to match files using wildcards * [Rule Ordering](/docs/cdn/edge-rules/ordering) - Understand how multiple cache rules interact # Dynamic Variables Source: https://bunny.net/docs/cdn/edge-rules/dynamic-variables Complete reference for dynamic variables available in Edge Rule actions like redirects, header modifications, and origin changes. Edge Rules support dynamic variables that automatically adapt based on request data. These variables allow you to create flexible rules for redirects, header modifications, and origin changes. ## Supported actions Variable expansion works in the following Edge Rule actions: * **Redirect to URL** * **Change Origin URL** * **Set Request Header** * **Set Response Header** ## Variable syntax There are two variable syntaxes available: | Syntax | Format | Example | | -------- | ------------------- | --------------------- | | Basic | `{{variable}}` | `{{path}}` | | Advanced | `%{Collection.Key}` | `%{User.CountryCode}` | The `{{ variable }}` syntax automatically includes slashes where needed in URLs. When using `%{Collection.Key}` syntax, you need to manually include slashes in your URLs. ## Basic variables These simple variables are commonly used for general redirection and header logic: | Variable | Description | Example Output | | -------------------- | ------------------------------------------- | ------------------------- | | `{{path}}` | Full request path including query string | `/videos/test.mp4?user=1` | | `{{hostname}}` | The hostname from the request | `test.b-cdn.net` | | `{{country_code}}` | Two-letter country code of the user's IP | `US` | | `{{query_string}}` | Query string only (without the `?`) | `user=1` | | `{{request_method}}` | HTTP method used | `GET`, `POST` | | `{{file_name}}` | Filename from the URL (last part after `/`) | `file.jpg`, `test.mp4` | ### Example: Redirect preserving path To redirect all requests from one domain to another while preserving the path: | Setting | Value | | ---------------- | --------------------------------- | | **Action** | Redirect to URL | | **Redirect URL** | `https://www.example.com{{path}}` | | **Status Code** | 301 | *** ## Advanced variable collections The advanced variable system provides detailed access to request data using the `%{Collection.Key}` syntax. ### RequestHeaders Access headers from the incoming HTTP request. | Example | Result | | ------------------------------ | ---------------------- | | `%{RequestHeaders.Host}` | `videos.example.com` | | `%{RequestHeaders.User-Agent}` | Browser or client info | *** ### Query Access specific query parameters from the URL. For a URL like `/video.mp4?user=123`: | Variable | Result | | --------------- | ------ | | `%{Query.user}` | `123` | Replace `KeyName` with your actual query parameter name. For example, if your query is `?Myhash=123`, use `%{Query.Myhash}` to get `123`. *** ### Path Extract specific segments of the URL path. This is useful for complex routing or origin overrides. For the example path `/hello/world/bunny/eat/carrot.jpg`: | Variable | Output | Description | | ------------- | ---------------------------- | -------------------------------------------- | | `%{Path.0}` | `hello` | First segment | | `%{Path.1}` | `world` | Second segment | | `%{Path.0-2}` | `hello/world/bunny` | Range from index 0 to 2 | | `%{Path.1-3}` | `world/bunny/eat` | Range from index 1 to 3 | | `%{Path.3-}` | `eat/carrot.jpg` | From index 3 to end | | `%{Path.1-}` | `world/bunny/eat/carrot.jpg` | From index 1 to end | | `%{Path.-1}` | `hello/world` | From start up to (but not including) index 1 | If the URL includes a query string and the path you extract doesn't alter the filename, the query string remains intact. For example, with `/hello/world/bunny/eat/carrot.jpg?query=something`, using `%{Path.1-}` returns `world/bunny/eat/carrot.jpg?query=something`. *** ### Url Access parsed components of the full URL. | Variable | Description | Example | | ------------------ | ------------------------------------ | ----------------------- | | `%{Url.Filename}` | Last part of URL (empty for folders) | `video.mp4` | | `%{Url.Extension}` | File extension | `mp4` | | `%{Url.Directory}` | Folder path only | `/videos/2024/` | | `%{Url.Hostname}` | Hostname from request | `cdn.mysite.com` | | `%{Url.Path}` | Full path and query | `/videos/video.mp4?x=1` | *** ### User Information about the user making the request. | Variable | Description | Example | | --------------------- | ---------------------- | -------------- | | `%{User.IP}` | End-user IP address | `203.0.113.10` | | `%{User.CountryCode}` | Country of the request | `DE` | *** ### Server Internal information about the server processing the request. | Variable | Description | Example | | -------------------- | ------------------------ | ------- | | `%{Server.ZoneCode}` | Code of the serving zone | `NY` | | `%{Server.ID}` | Server ID | `9482` | *** ### Request Information about the HTTP request. | Variable | Description | Example | | ------------------------ | ----------------------- | ----------------------- | | `%{Request.Method}` | HTTP method | `GET`, `POST` | | `%{Request.Path}` | Full URL path and query | `/videos/video.mp4?x=1` | | `%{Request.QueryString}` | Query string only | `x=1` | *** ## Practical examples ### Redirect non-www to www Redirect all requests from `domain.com` to `www.domain.com` while preserving the path: | Setting | Value | | ------------------- | -------------------------------- | | **Action** | Redirect to URL | | **Redirect URL** | `https://www.domain.com{{path}}` | | **Status Code** | 301 | | **Condition** | Request URL | | **Condition Value** | `*` | This rule must be added to the pull zone where `domain.com` is declared, not where `www.domain.com` is declared. If both hostnames are in the same pull zone, narrow the condition to `*://domain.com*` to only match the non-www version. ### Route requests based on path segment Route requests to different origins based on the first path segment: | Setting | Value | | ------------------- | -------------------------------------------------- | | **Action** | Change Origin URL | | **Origin URL** | `https://%{Path.0}.example-backend.com/%{Path.1-}` | | **Condition** | Request URL | | **Condition Value** | `*/api/*` | This would route `/api/users/123` to `https://api.example-backend.com/users/123`. ### Add country code to request header Pass the user's country to your origin for geo-based logic: | Setting | Value | | ------------------- | --------------------- | | **Action** | Set Request Header | | **Header Name** | `X-User-Country` | | **Header Value** | `%{User.CountryCode}` | | **Condition** | Request URL | | **Condition Value** | `*` | ## Related * [Variable Expansion](/docs/cdn/edge-rules/variable-expansion) - Detailed syntax reference for variable collections * [Trigger Path Setup](/docs/cdn/edge-rules/trigger-path) - Configure trigger conditions correctly * [Middleware Scripts](/docs/scripting/middleware/overview) - For complex transformations beyond Edge Rules # Edge Rules Source: https://bunny.net/docs/cdn/edge-rules/index Create powerful custom behaviors for your CDN using Edge Rules to control redirects, headers, caching, and origin routing. Edge Rules allow you to create powerful custom behaviors for your pull zone, such as redirects, header modifications, cache time overrides, or origin changes based on incoming request data. ## How to access Edge Rules Go to **CDN** in the dashboard and select the pull zone you want to configure. Click on the **Edge Rules** tab in the pull zone settings. Click **Add Edge Rule** to create a new rule with your desired action and trigger conditions. ## What can Edge Rules do? Edge Rules support a variety of actions that execute at different layers of the CDN: | Action | Description | | --------------------------------------- | ----------------------------------------------- | | **Force SSL** | Redirect HTTP requests to HTTPS | | **Redirect to URL** | Redirect requests to a different URL | | **Change Origin URL** | Route requests to a different origin server | | **Override Cache Time** | Set custom cache duration for matching requests | | **Block Request** | Block requests that match certain conditions | | **Set Response Header** | Add or modify response headers | | **Set Request Header** | Add or modify headers sent to the origin | | **Force Download** | Force the browser to download the file | | **Disable/Enable Token Authentication** | Control token authentication per request | | **Override Browser Cache Time** | Set custom browser cache duration | | **Set Network Rate Limit** | Limit bandwidth for matching requests | | **Enable/Disable Request Coalescing** | Control request coalescing per request | ## Guides Learn how to correctly configure trigger paths with wildcards and pattern matching. Set custom cache durations for specific file extensions. Redirect your b-cdn.net hostname to a custom domain. Connect directly to an origin IP with a custom hostname. ## Reference Complete list of variables available in Edge Rule actions. Use variable expansion syntax to create dynamic rule values. Control execution order and understand rule priority. ## Need more power? For complex request and response transformations that go beyond what Edge Rules can offer, consider using [Middleware Scripts](/docs/scripting/middleware/overview). Middleware scripts allow you to write custom code that executes at the edge, giving you full programmatic control over request handling, response modification, and routing logic. # Direct IP Origin with Custom Hostname Source: https://bunny.net/docs/cdn/edge-rules/ip-origin Connect your pull zone directly to a server IP address while serving content from a custom hostname using Edge Rules. This guide explains how to connect your pull zone to your server IP directly, bypassing intermediary services (such as firewalls or proxies) that might block CDN access to your origin. This is useful when: * Your origin is behind a firewall that blocks CDN requests * You need to bypass proxy services like Cloudflare * You want to connect directly to a specific server IP ## Prerequisites * A pull zone configured in the bunny.net dashboard * Your origin server's IP address * A custom hostname you want to use for serving content ## Configuration Navigate to your pull zone and go to **General** → **Origin**. Set the origin URL to point directly to your server's IP address: * Select the correct scheme (`HTTP` or `HTTPS`) to prevent redirects * If you have **Forward Host Header** enabled, disable it for this configuration At this point, your pull zone will connect to the server directly. However, the server will return the default website configured for that IP. Step 2 configures bunny.net to send the correct hostname to your origin. Go to **General** → **Hostnames** and add your custom hostname: 1. Enter your domain name in the **Add a custom hostname** field 2. Click **Add Hostname** Update your DNS provider to point your custom hostname to the bunny.net pull zone: - Create a **CNAME** record pointing to your pull zone hostname (e.g., `your-zone.b-cdn.net`) After DNS propagation, you can enable SSL for your custom hostname: 1. Click **Verify & Activate SSL** to get a free Let's Encrypt certificate 2. Alternatively, scroll to **Linked Hostnames** and click **Enable** to activate SSL later or upload your own certificate Navigate to **Edge Rules** and create a new rule to send the correct hostname to your origin: | Setting | Value | | ------------------- | ------------------------------------------ | | **Action** | Set Request Header | | **Header Name** | `Host` | | **Header Value** | Your origin hostname (e.g., `example.com`) | | **Condition** | Request URL | | **Match Type** | Match Any | | **Condition Value** | `*` | If you had cached content from a previous configuration, purge the cache to ensure fresh content is fetched from the new origin. ## How it works When a request comes in: 1. The client connects to your custom hostname (served by bunny.net) 2. bunny.net connects directly to your origin server IP 3. The Edge Rule sets the `Host` header so your origin knows which website to serve 4. Your origin responds with the correct content This bypasses any intermediate proxies while maintaining proper hostname routing. ## Related * [Variable Expansion](/docs/cdn/edge-rules/variable-expansion) - Use dynamic values in your Edge Rules * [Rule Ordering](/docs/cdn/edge-rules/ordering) - Understand how multiple rules interact # Edge Rules Ordering Source: https://bunny.net/docs/cdn/edge-rules/ordering Control the sequence of your Edge Rules to fine-tune behavior and execution priority. ## How Edge Rules Are Ordered Edge Rules execute in the order defined by the **OrderIndex**. Rules are always processed from the smallest to the largest **OrderIndex** value: * A rule with `OrderIndex = 1` will execute **before** a rule with `OrderIndex = 2`. * All rules are executed sequentially based on their index. This allows you to control which rules should apply first, which is especially important when multiple rules affect the same headers or behaviors. ## Default Behavior: What Happens if No `OrderIndex` Is Set? If you do not specify an `OrderIndex` for your Edge Rules: * Rules will execute in **the order they were created**. Because of this, **we strongly recommend setting an explicit OrderIndex for all rules** to ensure predictable and consistent execution order. ## Short-Circuiting vs. Multi-Action Execution Edge rules process actions differently depending on the action type. Some actions follow a **short-circuiting model**, where **only the first matching action of that type is executed**, and further actions of that same type are ignored. This behavior applies when executing multiple actions of the same kind would cause conflicts or unintended side effects. Other actions support **multi-action execution**, meaning **all matching actions of that type are processed**, in sequence. This is typical for additive behaviors like setting response headers or appending cookies, where multiple values can coexist. We will add either the 🛑(`short-circuiting model`) or 🔁(`multi-action execution`) icon to the action list below to indicate how each action is processed. ### Example Behaviors If multiple rules modify the same request or response header, all matching actions will be executed, allowing cumulative changes. If multiple rules attempt to rewrite the URL or redirect, only the first matching action is executed, and subsequent conflicting actions are skipped. **Example:** | OrderIndex | Condition | Action | | ---------- | -------------------------------------------------------- | --------------------------------------- | | 1 | If `Request URL` equals `/images` | `Override Browser Cache Time` -> `3600` | | 2 | if `Request URL` equals `/images` | `SetResponseHeader` -> `hello=world` | | 3 | If `Request Header` -> `User-Agent` contains `Googlebot` | `Override Browser Cache Time` -> `5` | | 4 | if `Request Header` -> `User-Agent` contains `Google` | `SetResponseHeader` -> `hello2=world2` | If a request matches both conditions: * The `Cache-Control` value will be `3600`, because `Override Browser Cache Time` is a 🛑 short-circuiting action and the first match (OrderIndex = 1) wins. * Both `SetResponseHeader` actions will execute, resulting in `hello` and `hello2` headers being added. ## Edge Rule Execution Layers (Cache vs Origin) Edge Rules execute in two distinct stages of the request lifecycle: * Cache Layer - evaluated on every request, before cache lookup and cache decision are finalized * Proxy (Origin) Layer - evaluated only if the request goes to origin (CACHE MISS or BYPASS) The Cache Layer is always executed, regardless of whether the request results in a CACHE HIT or MISS. The Proxy (Origin) Layer is only executed when the request cannot be served from cache. ### Request Flow Overview A request is processed in the following order: 1. Edge Rules (Cache Layer) 2. Cache Lookup (HIT or MISS) 3. Edge Rules (Proxy/Origin Layer, only on MISS/BYPASS) 4. Origin Request (if needed) ### Cache Layer Execution (Always Runs): The following Edge Rules are evaluated at the Cache Layer, meaning they run on every request before cache resolution, regardless of whether the final result is a HIT or MISS. * `ForceSSL` (Enum Value: `0`) 🛑 * `Redirect` (Enum Value: `1`) 🛑 * `OverrideCacheTime` (Enum Value: `3`) 🛑 *Note: if set to`0` it will bypass edge cache, if we don't already have the content cached on our edge.* * `BlockRequest` (Enum Value: `4`) 🛑 * `SetResponseHeader` (Enum Value: `5`) 🔁 * `ForceDownload` (Enum Value: `7`) 🛑 * `DisableTokenAuthentication` (Enum Value: `8`) 🛑 * `EnableTokenAuthentication` ( Enum Value: `9`) 🛑 * `OverrideCacheTimePublic` (Enum Value: `10`) 🛑 * `IgnoreQueryString` (Enum Value: `11`) 🛑 * `DisableOptimizer` (Enum Value: `12`) 🛑 * `ForceCompression` (Enum Value: `13`) 🛑 * `SetStatusCode` (Enum Value: `14`) 🛑 * `OverrideBrowserCacheTime` (Enum Value: `16`) 🛑 * `SetNetworkRateLimit` (Enum Value: `18`) 🛑 * `SetConnectionLimit` (Enum Value: `19`) 🛑 * `SetRequestsPerSecondLimit` (Enum Value: `20`) 🛑 * `OverrideBrowserCacheResponseHeader` (Enum Value: `25`) 🛑 * `RemoveBrowserCacheResponseHeader` (Enum Value: `26`) 🛑 * `DisableShieldChallenge` (Enum Value: `27`) 🛑 ### Proxy (Origin) Layer Execution (MISS/BYPASS Only): The following Edge Rules are evaluated only when the request reaches the origin, meaning: * Cache MISS * Cache BYPASS (e.g. due to rules or headers) These rules are not executed on cache hits, as no origin request is made. * `OriginUrl` (Enum Value: `2`) 🛑 * `OverrideCacheTime` (Enum Value: `3`) 🛑 * `SetResponseHeader` (Enum Value: `5`) 🔁 * `SetRequestHeader` (Enum Value: `6`) 🔁 * `BypassPermaCache` (Enum Value: `15`) 🛑 * `OriginStorage` (Enum Value: `17`) 🛑 * `RunEdgeScript` (Enum Value: `21`) 🛑 * `OriginMagicContainers` (Enum Value: `22`) 🛑 * `DisableWAF` (Enum Value: `23`) 🛑 * `RetryOrigin` (Enum Value: `24`) 🛑 * `DisableShieldChallenge` (Enum Value: `27`) 🛑 * `DisableShieldBotDetection` (Enum Value: `29`) 🛑 * `BypassAwsS3Authentication` (Enum Value: `30`) 🛑 ## Setting the Execution Order ### Method 1: Dashboard (Drag & Drop) You can reorder your Edge Rules visually: 1. Go to your **Pull Zone** > **Edge Rules** tab. 2. Click "Reorder" badge at the top of the page. 3. Drag rules to change their order. 4. The order you set will automatically adjust the **OrderIndex** behind the scenes. ### Method 2: API (Set OrderIndex Directly) If you manage rules programmatically via our API, you can set the execution order by adjusting the `OrderIndex` value when creating or updating an edge rule. ## Best Practices * Plan rule priorities carefully, especially when multiple rules affect the same property or header. * Keep related rules grouped and ordered for easier maintenance. * Use the dashboard for visual management and the API for bulk or automated updates. # Pattern Matching in Edge Rules Source: https://bunny.net/docs/cdn/edge-rules/pattern-matching Use Lua-based pattern matching in Edge Rule conditions to match structured request values such as URLs, headers, cookies, and query strings. Edge Rules allow you to define logic that runs directly on bunny.net's edge servers before a request reaches your origin. Rules can modify caching behavior, redirect traffic, route requests to different origins, apply security actions, and more based on request conditions. Edge Rule conditions often evaluate values such as: * Cookie Value * Country Code (2 letter) * Country State Code * File Extension * Origin Retry Attempt Count * Query String * Random Chance (%) * Remote IP * Request Header * Request Method * Request URL * Response Header * Response Status Code In many cases, simple string or wildcard matching is sufficient. However, modern applications frequently generate URLs and request values that follow predictable patterns rather than fixed strings. Examples include: * Streaming manifests * Versioned build assets * Dynamically generated API routes To make these scenarios easier to handle, Edge Rules support **pattern matching using Lua patterns**. *** # Using Pattern Matching Pattern matching can be used in any Edge Rule condition that evaluates a request value. To enable pattern matching, prefix the value with: ```text theme={null} pattern:^...$ ``` Example: ```text theme={null} pattern:^.*/video_chunk%-[^%-]+%-[^%-]+%.dash$ ``` The `pattern:` prefix instructs the Edge Rule engine to evaluate the value using **Lua pattern matching** instead of a standard string comparison. Lua patterns are similar to regular expressions but intentionally simpler and optimized for performance. ## Anchoring the Pattern Patterns should normally be wrapped with: ```text theme={null} ^ $ ``` This ensures the **entire value is matched**, rather than matching a substring. If the `pattern:` prefix is not used, the condition behaves as a normal string comparison. *** # Example Pattern: ```text theme={null} pattern:^.*/video_chunk%-[^%-]+%-[^%-]+%.dash$ ``` Matches URLs such as: ```text theme={null} /live/video_chunk-us-12345.dash /live/video_chunk-es-54321.dash ``` Internally, Edge Rules evaluate patterns using Lua's `string.find` function. This provides efficient pattern matching without requiring a full regular expression engine. *** # Example Use Cases ## Matching Streaming Manifests Streaming platforms often generate manifest files containing region identifiers or session identifiers in the filename. Example URLs: ```text theme={null} /live/video_chunk-us-12345.dash /live/video_chunk-es-54321.dash ``` A pattern condition can match these files and apply actions such as: * Custom cache control * Edge redirects * Request routing *** ## Targeting Versioned Assets Many build systems generate versioned assets: ```text theme={null} /assets/app-1.4.7.js /assets/app-1.4.8.js /assets/app-1.5.0.js ``` A pattern condition allows matching all versions using a single rule. Common use cases include: * Cache rules * Rate limiting * Header modification *** ## Security Filtering Pattern matching can also be used in security rules. Example scenarios: * Blocking administrative endpoints * Detecting suspicious request patterns * Filtering specific URL structures These rules can be combined with conditions such as cookie presence or header values. *** # Lua Pattern Cheat Sheet Edge Rules use **Lua patterns**, which are simpler than full regular expressions. ### Format ```text theme={null} pattern:^...$ ``` Edge Rules evaluate patterns using Lua's `string.find`. ### Anchors | Symbol | Meaning | | ------ | --------------- | | ^ | Start of string | | \$ | End of string | ### Character Classes | Pattern | Meaning | | ------- | ------------------------------------- | | %d | Digit | | %a | Letter | | %w | Alphanumeric + \_ | | . | Any character | | \[abc] | Match a, b, or c | | \[^abc] | Match any character except a, b, or c | ### Repeaters | Pattern | Meaning | | ------- | ---------------------- | | + | 1 or more | | \* | 0 or more (greedy) | | - | 0 or more (non-greedy) | ### Escaping Characters Use `%` to escape special characters. Examples: ```text theme={null} %. # literal dot %- # literal hyphen ``` ### Limitations Lua patterns are intentionally simpler than full regular expressions. Unsupported features include: * Alternation (`|`) * Lookaheads / lookbehinds * Full PCRE syntax These limitations help ensure pattern evaluation remains fast and predictable across the edge network. # Redirect b-cdn.net to Custom Hostname Source: https://bunny.net/docs/cdn/edge-rules/redirect-hostname Use Edge Rules to redirect your default b-cdn.net hostname to your custom CDN hostname. Edge Rules allow you to redirect your default `b-cdn.net` hostname to your custom hostname, ensuring your content is only accessible through your own domain. This is useful for: * SEO purposes (avoiding duplicate content) * Brand consistency * Ensuring all traffic goes through your custom domain ## Prerequisites * A pull zone with a [custom hostname](/docs/cdn/custom-hostname) already configured * The custom hostname should have SSL enabled ## Configuration Navigate to your pull zone and select **Edge Rules** from the side menu. Click **Add Edge Rule** to open the rule configuration page. Select **Redirect To URL** as the action. In the **Redirect URL** field, enter your custom URL with the `{{path}}` variable appended: ``` https://cdn.yourdomain.com{{path}} ``` The `{{path}}` variable automatically includes the full request path and query string, ensuring all URLs redirect correctly. Set the status code to **301** for a permanent redirect, or **302** for a temporary redirect. Click **Add Condition** and configure: | Setting | Value | | ------------------- | -------------------------- | | **Condition Type** | Request URL | | **Match Type** | Match Any | | **Condition Value** | `*://yourzone.b-cdn.net/*` | Replace `yourzone` with the name of your pull zone. Make sure to include both the scheme wildcard (`*://`) and the trailing wildcard (`/*`) to match all requests to your b-cdn.net hostname. Click **Save Edge Rule** to activate the redirect. ## Example configuration For a pull zone named `mycdn` with a custom hostname `cdn.example.com`: | Setting | Value | | ------------------- | --------------------------------- | | **Action** | Redirect To URL | | **Redirect URL** | `https://cdn.example.com{{path}}` | | **Status Code** | 301 | | **Condition Type** | Request URL | | **Condition Value** | `*://mycdn.b-cdn.net/*` | ### Before and after | Original URL | Redirected URL | | ------------------------------------------------ | ------------------------------------------------ | | `https://mycdn.b-cdn.net/images/logo.png` | `https://cdn.example.com/images/logo.png` | | `https://mycdn.b-cdn.net/video.mp4?quality=high` | `https://cdn.example.com/video.mp4?quality=high` | | `https://mycdn.b-cdn.net/` | `https://cdn.example.com/` | ## HTTP vs HTTPS Make sure to use the correct scheme in your redirect URL: * Use `https://` if your custom hostname has SSL enabled (recommended) * Use `http://` only if SSL is not configured Using the wrong scheme may cause redirect loops or connection errors. ## Related * [Custom Hostname](/docs/cdn/custom-hostname) - Set up a custom hostname for your pull zone * [SSL Setup](/docs/cdn/ssl-setup) - Configure SSL for your custom hostname * [Trigger Path Setup](/docs/cdn/edge-rules/trigger-path) - Learn more about configuring trigger conditions * [Dynamic Variables](/docs/cdn/edge-rules/dynamic-variables) - Full list of variables available in Edge Rules # Trigger Path Setup Source: https://bunny.net/docs/cdn/edge-rules/trigger-path Learn how to correctly configure Edge Rule trigger paths using wildcards and pattern matching. Edge Rules use a powerful wildcard matching system for trigger paths. Understanding how to properly configure these patterns helps you create precise rules that trigger only when needed. ## Wildcard basics The `*` wildcard matches any sequence of characters. You can use it anywhere in the trigger path: | Pattern | Matches | Does Not Match | | ------------------- | ---------------------------------------------------------- | ------------------------------ | | `*.jpg` | `/image.jpg`, `/photos/sunset.jpg` | `/image.png`, `/image.jpg.bak` | | `/images/*` | `/images/logo.png`, `/images/photos/cat.jpg` | `/img/logo.png` | | `*://example.com/*` | `https://example.com/page`, `https://example.com/file.css` | `https://sub.example.com/page` | ## Common patterns ### Matching by file extension To match all files with a specific extension, use `*.extension`: ``` *.mp4 ``` This matches any `.mp4` file regardless of path depth. ### Matching subdomains Use wildcards to match all subdomains or exclude them: | Pattern | Description | | --------------------- | ------------------------------------------- | | `*://*.example.com/*` | Matches all subdomains | | `*://example.com/*` | Matches only the root domain (no subdomain) | | `*://*example.com/*` | Matches root domain and all subdomains | ### Matching specific paths | Pattern | Matches | | ------------- | ----------------------------------------------- | | `/api/*` | All paths starting with `/api/` | | `*/admin/*` | Any URL containing `/admin/` | | `/v1/*/users` | Paths like `/v1/api/users`, `/v1/service/users` | ## Important considerations ### Query parameters are not matched Trigger paths do not match query strings or any data after the filename. Only the absolute URL path is evaluated. For example, if your trigger path is `*.jpg`, it will match: * `/image.jpg` * `/image.jpg?width=100` Both URLs match because the query string (`?width=100`) is ignored during pattern matching. ### HTTP vs HTTPS scheme matters The trigger path is compared as a complete string, including the scheme. Make sure to account for both protocols: | Pattern | Matches | | ----------------------- | ---------------------------- | | `http://example.com/*` | Only HTTP requests | | `https://example.com/*` | Only HTTPS requests | | `*://example.com/*` | Both HTTP and HTTPS requests | Use `*://` to match both HTTP and HTTPS if your rule should apply regardless of protocol. ### Trailing slashes are required for root paths Due to HTTP protocol design, the trigger path always contains a trailing slash when accessing a root path. A trigger path of `http://example.com` will **never** match because requests to the root always include a trailing slash. Use `http://example.com/` instead. | Trigger Path | Will Match Root? | | --------------------- | ---------------- | | `http://example.com` | No | | `http://example.com/` | Yes | | `*://example.com/*` | Yes | ## Examples **Trigger path:** `*/admin/*` This blocks access to any URL containing `/admin/` in the path, such as: * `https://example.com/admin/dashboard` * `https://example.com/app/admin/users` **Trigger path:** `*.jpg` (repeat for each extension) Or use the **File Extension** condition type instead, which allows you to specify extensions like `jpg`, `png`, `gif` without wildcards. **Trigger path:** `*://cdn.example.com/*` This matches all requests to `cdn.example.com` regardless of protocol, path, or query string. You'll need two rules: 1. First rule (higher priority): Allow `/api/public/*` - use **Disable** action or skip 2. Second rule (lower priority): Apply your action to `/api/*` See [Rule Ordering](/docs/cdn/edge-rules/ordering) for more on rule priority. ## Related * [Rule Ordering](/docs/cdn/edge-rules/ordering) - Control which rules execute first * [Dynamic Variables](/docs/cdn/edge-rules/dynamic-variables) - Use variables in your rule actions * [Custom Cache Time](/docs/cdn/edge-rules/custom-cache-time) - Example using file extension matching # Variable Expansion Source: https://bunny.net/docs/cdn/edge-rules/variable-expansion Use variable expansion syntax to dynamically configure Edge Rule values based on request parameters. Variable expansion allows Edge Rules to dynamically configure values based on specific request parameters. Instead of hardcoding URLs or header values, you can use variables that are replaced at runtime with actual request data. ## Supported actions Variable expansion works with the following Edge Rule actions: * **Change Origin URL** * **Redirect to URL** * **Set Request Header** * **Set Response Header** ## Syntax Variables are accessed using the following syntax: ``` %{Collection.Key} ``` Where `Collection` is a group of related variables, and `Key` is the specific value you want to access. For a complete list of all available variables with examples, see [Dynamic Variables](/docs/cdn/edge-rules/dynamic-variables). *** ## Variable collections ### RequestHeaders The `RequestHeaders` collection contains HTTP headers from the incoming request. Headers are automatically mapped based on their name. | Example | Description | | ----------------------------------- | --------------------------------------- | | `%{RequestHeaders.Host}` | The `Host` header sent with the request | | `%{RequestHeaders.User-Agent}` | The `User-Agent` header | | `%{RequestHeaders.Accept-Language}` | The `Accept-Language` header | ### Query The `Query` collection contains URL query parameters. Parameters are automatically mapped based on their key. For a URL like `?token=abc123&user=42`: | Example | Result | | ---------------- | -------- | | `%{Query.token}` | `abc123` | | `%{Query.user}` | `42` | ### Request The `Request` collection contains information about the HTTP request: | Key | Description | Example | | ------------- | ----------------------- | ----------------------------- | | `Method` | The HTTP method | `GET`, `POST` | | `Path` | Full URL path and query | `/videos/test.mp4?quality=hd` | | `QueryString` | Query string only | `quality=hd` | ### Path The `Path` collection allows you to extract specific segments from the URL path using index-based access. For the path `/hello/world/bunny/eat/carrot.jpg`: | Syntax | Result | Description | | ------------- | ---------------------------- | ------------------------ | | `%{Path.0}` | `hello` | Single segment at index | | `%{Path.1}` | `world` | | | `%{Path.0-2}` | `hello/world/bunny` | Range from index 0 to 2 | | `%{Path.1-3}` | `world/bunny/eat` | Range from index 1 to 3 | | `%{Path.3-}` | `eat/carrot.jpg` | From index 3 to end | | `%{Path.1-}` | `world/bunny/eat/carrot.jpg` | From index 1 to end | | `%{Path.-1}` | `hello` | From start up to index 1 | | `%{Path.-3}` | `hello/world/bunny` | From start up to index 3 | ### Url The `Url` collection provides parsed components of the request URL: | Key | Description | Example | | ----------- | ----------------------------------------- | ----------------------- | | `FileName` | Filename from the URL (empty for folders) | `video.mp4` | | `Path` | Full URL path and query | `/videos/video.mp4?x=1` | | `Extension` | File extension | `mp4` | | `Directory` | Directory path without filename | `/videos/2024/` | | `Hostname` | Request hostname | `cdn.example.com` | ### User The `User` collection contains information about the requesting user: | Key | Description | Example | | ------------- | ----------------------- | ---------------- | | `IP` | End-user IP address | `203.0.113.10` | | `CountryCode` | Two-letter country code | `US`, `DE`, `JP` | ### Server The `Server` collection contains information about the serving edge server: | Key | Description | Example | | ---------- | ------------------------------- | ---------------- | | `ZoneCode` | Zone code of the serving server | `NY`, `LA`, `DE` | | `ID` | Unique server ID | `9482` | *** ## Examples ### Dynamic origin routing Route requests to different origins based on the first path segment: **Origin URL:** `https://%{Path.0}.backend.example.com/%{Path.1-}` For a request to `/api/users/123`, this becomes `https://api.backend.example.com/users/123`. ### Pass query parameter as header Forward a specific query parameter to your origin as a header: **Header Name:** `X-Auth-Token`\ **Header Value:** `%{Query.token}` ### Geo-based routing Add the user's country to the origin request: **Header Name:** `X-Country`\ **Header Value:** `%{User.CountryCode}` *** ## Related * [Dynamic Variables](/docs/cdn/edge-rules/dynamic-variables) - Complete variable reference with all examples * [Rule Ordering](/docs/cdn/edge-rules/ordering) - Control execution order for complex rule sets * [Middleware Scripts](/docs/scripting/middleware/overview) - For advanced request/response transformations # Do you support cache warming or preloading content onto the CDN? Source: https://bunny.net/docs/cdn/frequently-asked-questions/cache-warming To ensure fair cache management and file distribution, we do not currently allow preloading files into cache. We hae a 'Smart Preloader' feature on Optimizer that monitors dynamic HTML requests going through bunny.net. If the system detects a request is slow, and might be negatively impacting the user experience, it will respond to the user with a special preloader screen HTML response. [Learn more](/docs/cdn/performance/smart-preloader). # Is there a domain limit on pull zone configurations? Source: https://bunny.net/docs/cdn/frequently-asked-questions/domain-limit Currently we have a limit of 10 hostnames per pull zone. If more are required, please reach out to [support@bunny.net](mailto:support@bunny.net). # Do you support ETag? Source: https://bunny.net/docs/cdn/frequently-asked-questions/etag-support ## What is an ETag? The `ETag` (Entity Tag) header is an HTTP response header used to uniquely identify a specific version of a file. It allows web browsers and CDNs to quickly determine whether a cached copy of a file is still identical to the one on the origin server. If a file changes, its ETag changes as well, making it a highly effective tool for validation, maximizing cache efficiency, and minimizing origin bandwidth usage. *** ## How Bunny.net Handles ETag Headers Bunny.net fully supports the `ETag` header out of the box, with a few important behavior considerations depending on your configuration: ### Origin Passthrough If your origin server includes an `ETag` header in its response and the **Optimize for large media delivery** setting is **disabled** in your Pull Zone, Bunny.net will pass the ETag through to the client and cache the resource accordingly. ### Smart Compression Adjustments When Bunny.net applies edge compression to a file (such as Brotli or Gzip), the original strong ETag will automatically be converted into a **weak ETag**. When this happens, an upstream `W/` prefix is added to the start of the header string: ```http theme={null} # Original Strong ETag from Origin ETag: "123456789" # Modified Weak ETag after Edge Compression ETag: W/"123456789" ``` # MIME Type Compression Support Source: https://bunny.net/docs/cdn/frequently-asked-questions/mime-compression Learn how BunnyCDN automatically compresses content using gzip, Brotli, and Zstandard, and view supported MIME types. BunnyCDN automatically compresses content using **gzip**, **Brotli** (`br`), or **Zstandard** (`zstd`), depending on the incoming `Accept-Encoding` header. Compression is guaranteed for a predefined set of MIME types listed below, as well as for files with the following extensions—even if the `Content-Type` header is missing or does not exactly match the list: * `.css` * `.js` * `.json` * `.xml` * `.svg` * `.html` BunnyCDN provides compression for these types, but it may also apply compression to other content types based on compatibility. If you don't see a specific MIME type listed, feel free to contact our support team, and we’ll be happy to review it for possible inclusion. ## Supported MIME Types | Type | MIME Content Type | | :-------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Application** | `application/atf`
`application/atom+xml`
`application/dash+xml`
`application/ecmascript`
`application/eot`
`application/font`
`application/font-sfnt`
`application/javascript`
`application/json`
`application/ld+json`
`application/manifest+json`
`application/opentype`
`application/otf`
`application/pkcs7-mime`
`application/rss+xml`
`application/truetype`
`application/ttf`
`application/vnd.apple.mpegurl`
`application/vnd.geo+json`
`application/vnd.ms-fontobject`
`application/wasm`
`application/x-font-opentype`
`application/x-font-truetype`
`application/x-font-ttf`
`application/x-httpd-cgi`
`application/x-javascript`
`application/x-mpegurl`
`application/x-opentype`
`application/x-otf`
`application/x-perl`
`application/x-ttf`
`application/x-web-app-manifest+json`
`application/xhtml+xml`
`application/xml`
`application/xml+rss` | | **Font** | `font/eot`
`font/opentype`
`font/otf`
`font/truetype`
`font/ttf` | | **Image** | `image/svg+xml`
`image/vnd.microsoft.icon`
`image/x-icon` | | **Model** | `model/gltf-binary` | | **Text** | `text/cache-manifest`
`text/css`
`text/csv`
`text/html`
`text/javascript`
`text/js`
`text/plain`
`text/richtext`
`text/tab-separated-values`
`text/x-component`
`text/x-java-source`
`text/x-script`
`text/xml` | # Which PoP you are being routed to Source: https://bunny.net/docs/cdn/frequently-asked-questions/pop-routing Learn how to identify which CDN edge server is handling your requests and how to check your routing details. BunnyCDN returns the server location and ID code with every request, which can help you easily determine which PoP and server you are being connected to. This guide explains how to get this information in three easy steps or by using automated diagnostic tools. Which PoP am I connected to ### 1. Open your browser debug console First, open your web browser developer tools by pressing `Ctrl + Shift + I` (Windows/Linux) or `Cmd + Option + I` (Mac) on your keyboard, and switch to the **Network** tab in the newly opened window. *Note: In our example we are using Google Chrome, but the procedure is nearly identical across all modern web browsers.* ### 2. Find a bunny.net request in the network list Next, enter the hostname of your Pull Zone into your browser address bar (such as `https://mytestzone.b-cdn.net`) and navigate to it. After the page finishes loading, you will see a list of individual assets and network requests appear in the table. Select one of the requests going directly to BunnyCDN—this is typically the very first HTML document or initial request in the list. ### 3. Find the Server header in the request details After selecting the request, a detail panel will open displaying the headers returned by the edge node you connected to. Look closely under the **Response Headers** section for the `Server` header, which will look something like `BunnyCDN-{PoP Code}-{Server-ID}`. When debugging routing issues, you can use this to determine which location you are being routed to and if requested, forward this to our support team. ## Alternative option: using our Tools Report In addition to the above, you can use our new diagnostic tools here: [Bunny Diagnostic Tools](https://tools.bunny.net/diagnostic-report) which will show you some metrics about your connection: Diagnostic Example # Do you support range requests? Source: https://bunny.net/docs/cdn/frequently-asked-questions/range-requests BunnyCDN fully supports range requests for both cached and uncached content. By default, BunnyCDN enables range byte requests for any request involving cached content. Ensure that you activate 'Optimize for large object delivery' in your pullzone's caching settings to enable this for uncached content. # Do you support RTMP streaming? Source: https://bunny.net/docs/cdn/frequently-asked-questions/rtmp-streaming RTMP streams are currently not supported on our platform, but we are exploring what we can offer in the near future. # Does BunnyCDN automatically detect when a file is changed? Source: https://bunny.net/docs/cdn/frequently-asked-questions/updating-files BunnyCDN does not monitor the files on your origin server for changes, this means that if a file is already cached on our servers, it will remain cached until the Cache-Control expires or it gets deleted to make space for more popular content. If you make a change to a file and need to see it reflected immediately, please make sure to purge the cache using the purge tool available at [https://dash.bunny.net/purge](https://dash.bunny.net/purge) # Bunny CDN Source: https://bunny.net/docs/cdn/index Deliver content globally with smart caching, edge rules, and real-time analytics. bunny.net CDN Bunny CDN accelerates your websites, applications, and media by caching and delivering content from edge locations closest to your users. With servers spanning six continents, your content reaches audiences faster while reducing load on your origin server. ## Key features * **Global edge network**: Over 100 points of presence deliver content from the nearest location to each user * **Smart caching**: Intelligent cache management with configurable durations, query string handling, and vary headers * **Origin Shield**: Protect your origin server by consolidating requests through a single shield location * **Edge Rules**: Customize request handling, set headers, rewrite URLs, and implement redirects at the edge * **Token authentication**: Secure your content with signed URLs and time-limited access tokens * **WebSocket support**: Enable real-time bidirectional communication for interactive applications * **Perma-Cache**: Store content permanently at the edge for maximum cache hit rates * **Logging and analytics**: Access detailed logs via API, forward to storage, or enable permanent log retention ## How it works A Pull Zone connects your origin server to the Bunny CDN network. When a user requests content: 1. The request routes to the nearest edge location 2. If cached, the content serves immediately from the edge 3. If not cached, the edge fetches from your origin, caches the response, and delivers it This reduces round-trip time and offloads traffic from your infrastructure. # Discourse Source: https://bunny.net/docs/cdn/integrations/cms/discourse Speed up your Discourse forum with bunny.net CDN for faster page loads and improved performance. Discourse has built-in CDN support. This guide walks you through pointing it at Bunny CDN. Log in to your [bunny.net dashboard](https://dash.bunny.net) and open the **Add Pull Zone** page. Choose a name for your zone, then set the origin URL to your Discourse forum (for example, `discourse.example.com`). Creating a Pull Zone for Discourse Select a pricing tier and click **Add Pull Zone**. For more detail, see [How to create your first Pull Zone](/docs/cdn/quickstart). Open your `app.yml` configuration file and find the following lines: ```yaml theme={null} ## the origin pull CDN address for this Discourse instance DISCOURSE_CDN_URL: https://discourse-cdn.example.com ``` If they aren't present, add them below the other `DISCOURSE_` variables. Replace the `DISCOURSE_CDN_URL` value with your Pull Zone hostname. Setting DISCOURSE_CDN_URL in app.yml Apply the new configuration by rebuilding the container: ```bash theme={null} ./launcher rebuild app ``` Once the rebuild finishes, your forum is served through Bunny CDN. To confirm everything is working, see [Verify your configuration](/docs/cdn/verify-configuration). # Drupal Source: https://bunny.net/docs/cdn/integrations/cms/drupal Integrate bunny.net CDN with your Drupal website for faster content delivery and improved performance. Drupal supports CDN delivery through the [CDN module](https://www.drupal.org/project/cdn), which rewrites your asset URLs to serve them through Bunny CDN. This guide walks you through installing and configuring it. Log in to your [bunny.net dashboard](https://dash.bunny.net) and create a new Pull Zone with your Drupal site as the origin URL. For details, see [How to create your first Pull Zone](/docs/cdn/quickstart). Match the protocol of your site exactly. If your Drupal site runs on HTTPS, enable HTTPS on the Pull Zone; if it runs on HTTP only, leave HTTPS disabled. A mismatch will cause errors. You can set a long Cache-Control time for Drupal. The CDN module serves updated elements from new URLs, so changes appear immediately without waiting for the cache to expire. Download the latest `.tar.gz` release of the [Drupal CDN module](https://www.drupal.org/project/cdn) and copy its download URL. You'll need SSH or FTP access to your site. In the Drupal admin, select **Manage**, then **Extend**, then **Install new module**. Drupal Extend page with the Install new module button Paste the `.tar.gz` URL, click **Install**, and provide your SSH/FTP credentials when prompted. When the installer finishes, click **Enable newly added modules**. Scroll to the **Web Services** section, select the **CDN** and **CDN UI** modules, then click **Install**. Enabling the CDN and CDN UI modules Go to **Manage → Configuration**, scroll to the bottom, and click **CDN Integration**. On the **Status** tab, make sure **Serve files from a CDN** is enabled. Enabling Serve files from a CDN Open the **Mapping** tab, select **Simple** mapping, choose **Serve all files**, and enter your Pull Zone hostname in the field on the right. Click **Save configuration**. Configuring the CDN mapping Your Drupal site now serves static assets through Bunny CDN. # ExpressionEngine Source: https://bunny.net/docs/cdn/integrations/cms/expressionengine Configure bunny.net CDN with ExpressionEngine to accelerate your website and optimize asset delivery. Setting up Bunny CDN with ExpressionEngine takes only a single line of configuration plus a template change. Log in to your [bunny.net dashboard](https://dash.bunny.net) and create a new Pull Zone with your ExpressionEngine site as the origin URL. For details, see [How to create your first Pull Zone](/docs/cdn/quickstart). You'll need SSH or FTP access to your web server. Open the `index.php` file in your site's root and find this line: ```php theme={null} // $assign_to_config['global_vars'] = array(); ``` Uncomment it and define a global variable for your CDN URL, replacing the hostname with your Pull Zone: ```php theme={null} $assign_to_config['global_vars'] = array('cdn_url' => 'https://YOURZONE.b-cdn.net/'); ``` Update your templates to reference the new `{cdn_url}` variable when linking to static resources. For example: ```text theme={null} {cdn_url}uploads/image.jpg ``` Load your site and confirm that the URLs for your static content now include your Pull Zone hostname. Once they do, ExpressionEngine is serving assets through Bunny CDN. # Joomla Source: https://bunny.net/docs/cdn/integrations/cms/joomla Speed up your Joomla website with bunny.net CDN using the CDN for Joomla plugin. Joomla can serve assets through Bunny CDN using the [CDN for Joomla plugin](https://www.regularlabs.com/extensions/cdnforjoomla) by Regular Labs, which rewrites your content URLs to serve them through the CDN. The free version of the CDN for Joomla plugin only supports HTTP URLs. To use HTTPS URLs, you'll need the paid version. Download the plugin from the [Regular Labs site](https://www.regularlabs.com/extensions/cdnforjoomla). In your Joomla admin, go to **Extensions → Manage → Install**, then upload the ZIP file you downloaded. Installing the CDN for Joomla plugin Go to **Extensions → Manage → Manage**, use the search to find the CDN plugin, then click it to open its configuration page and switch to the **Setup** tab. Finding the CDN plugin In your [bunny.net dashboard](https://dash.bunny.net), create a new Pull Zone with a name, set the origin URL to your website, choose your tier, and click **Add Pull Zone**. For details, see [How to create your first Pull Zone](/docs/cdn/quickstart). Creating a Pull Zone Back on the plugin's **Setup** tab, enter your Pull Zone hostname (the `b-cdn.net` hostname) in the **CDN Domain** field, then click **Save**. Setting the CDN Domain Your Joomla site now serves content through Bunny CDN. # Magento Source: https://bunny.net/docs/cdn/integrations/cms/magento Integrate bunny.net CDN with your Magento store for faster page loads and optimized e-commerce performance. This guide walks you through serving your Magento store's static and media files through Bunny CDN in four steps. Log in to your [bunny.net dashboard](https://dash.bunny.net) and create a new Pull Zone with your Magento store as the origin URL. Match the protocol (HTTP or HTTPS) of your site exactly. For details, see [How to create your first Pull Zone](/docs/cdn/quickstart). Creating a Pull Zone for Magento Open your Pull Zone's **Headers** settings and add the `html` and `json` extensions. This is required, otherwise your Magento admin interface won't work correctly. Adding CORS headers to the Pull Zone Log in to your Magento admin panel and go to **Stores → Configuration → General → Web**. Magento Web configuration Open the **Base URLs** tab and enter your Pull Zone hostname in both **Base URL for Static View Files** and **Base URL for User Media Files**, including the `/static/` and `/media/` subfolders respectively. Setting the Base URLs Repeat the same values in the **Base URLs (Secure)** section, using the `https://` version of the URL to avoid connectivity issues. Click **Save Config**. Flush your Magento cache as prompted by the settings dashboard. Your Magento store is now serving static and media files through Bunny CDN. # PrestaShop Source: https://bunny.net/docs/cdn/integrations/cms/prestashop Speed up your PrestaShop store with bunny.net CDN for faster product pages and improved checkout performance. PrestaShop includes built-in CDN settings, so connecting it to Bunny CDN is straightforward. Log in to your [bunny.net dashboard](https://dash.bunny.net) and create a new Pull Zone with your PrestaShop store as the origin URL. Match the protocol (HTTP or HTTPS) of your site exactly. For details, see [How to create your first Pull Zone](/docs/cdn/quickstart). Log in to your PrestaShop admin panel and go to **Advanced Parameters → Performance**. PrestaShop Advanced Parameters menu In the **CCC (Combine, Compress and Cache)** section, set **Smart cache for CSS** and **Smart cache for JavaScript** to **Yes**. Configuring the CCC section On older versions of PrestaShop, this section shows checkboxes instead of dropdowns. In that case, select **Use CCC for CSS** and **Use CCC for JavaScript**. Click **Save**. Scroll down to the **Media Servers** section and enter your Pull Zone hostname in the **Media server #1** field. Click **Save**. Configuring Media server #1 If caching is enabled for your store, click **Clear cache** at the top of the page. Your pages will now be served through Bunny CDN. # Shopware Source: https://bunny.net/docs/cdn/integrations/cms/shopware Configure bunny.net CDN with Shopware to accelerate your e-commerce store and improve customer experience. Shopware can serve media through Bunny CDN using a community-built adapter that uploads your content to a Bunny Storage zone and delivers it through a connected Pull Zone. The [Shopware adapter](https://github.com/tinect/TinectMediaBunnycdn) is built and maintained by the community, not by bunny.net. The adapter uploads your media to a Bunny [Storage Zone](/docs/storage), which is then delivered through a Pull Zone that uses the Storage Zone as its origin. Set up both before configuring Shopware. For details, see [How to create your first Pull Zone](/docs/cdn/quickstart). Download the latest `TinectMediaBunnycdn.zip` from the adapter's [GitHub releases page](https://github.com/tinect/TinectMediaBunnycdn/releases). In Shopware, open **Configuration → Plugin Manager**. Opening the Shopware Plugin Manager Go to **Installed**, click **Upload Plugin**, and select the ZIP file you downloaded. Uploading the plugin ZIP file The plugin appears under **Uninstalled**. Click the green **+** to install it. Installing the uploaded plugin Once installed, the plugin is **Inactive**. Click the pencil icon next to its name, then click **Activate**. If prompted to clear caches, confirm. Activating the plugin You'll need SSH or FTP access to edit a PHP file. In your Shopware install directory, open `config.php` and append the following to the configuration array: ```php theme={null} 'cdn' => [ 'backend' => 'bunnycdn', 'adapters' => [ 'bunnycdn' => [ 'type' => 'bunnycdn', 'mediaUrl' => 'https://PULLZONE.b-cdn.net/', 'apiUrl' => 'https://storage.bunnycdn.com/STORAGEZONENAME/', 'apiKey' => 'secret-api-key', ], ], ], ``` Replace `PULLZONE` with your Pull Zone hostname, `STORAGEZONENAME` with your Storage Zone name, and `apiKey` with the password from your Storage Zone's **FTP & API Access** page. Finally, upload your existing local media to the Storage Zone: ```bash theme={null} bin/console sw:media:migrate --from=local --to=bunnycdn ``` Your Shopware store now serves media through Bunny CDN. # Typo3 Source: https://bunny.net/docs/cdn/integrations/cms/typo3 Integrate bunny.net CDN with your TYPO3 website for faster content delivery and improved performance. TYPO3 can serve assets through Bunny CDN using the [Content Replacer extension](https://extensions.typo3.org/extension/replacer/), which rewrites the content URLs on your site. This guide walks you through the setup. Log in to your [bunny.net dashboard](https://dash.bunny.net) and create a new Pull Zone with your TYPO3 site as the origin URL. For details, see [How to create your first Pull Zone](/docs/cdn/quickstart). Match the protocol of your site exactly. If your TYPO3 site runs on HTTPS, enable HTTPS on the Pull Zone; if it runs on HTTP only, leave HTTPS disabled. A mismatch will cause errors. If your TYPO3 install uses Composer mode, install the extension from the command line: ```bash theme={null} composer req jweiland/replacer ``` Installing the Content Replacer extension with Composer Otherwise, install it through the **Extension Manager** in the TYPO3 web interface. Go to **Template**, select the root page, enable **Modify**, and edit the entire template record. Editing the TYPO3 template record Scroll to the **Setup** box and paste the following, replacing `YOURZONE.b-cdn.net` with your Pull Zone hostname: ```typoscript theme={null} config.tx_ja_replacer { search { 1 = typo3temp/ 2 = fileadmin/ 3 = typo3conf/ } replace { 1 = https://YOURZONE.b-cdn.net/typo3temp/ 2 = https://YOURZONE.b-cdn.net/fileadmin/ 3 = https://YOURZONE.b-cdn.net/typo3conf/ } } ``` Click **Save**, then select the lightning bolt and click **Flush all caches**. Flushing all caches in TYPO3 Your TYPO3 site now serves static assets through Bunny CDN. # WordPress Source: https://bunny.net/docs/cdn/integrations/cms/wordpress Speed up your WordPress site with bunny.net CDN using our official plugin for easy integration and optimization. # Amazon S3 Source: https://bunny.net/docs/cdn/integrations/storage/amazon-s3 Use bunny.net CDN as a caching layer for Amazon S3 to accelerate file delivery and reduce origin costs. Bunny CDN caches files from your Amazon S3 bucket and delivers them from a global edge network, speeding up delivery and reducing egress costs. This guide walks you through the setup in a few steps. Prefer to keep your files on bunny.net? [Bunny Storage](/docs/storage) is globally replicated object storage with tight CDN integration, and it offers an [S3-compatible API](/docs/storage/s3) (currently in beta). If you don't already have a bucket, sign in to the [AWS Management Console](https://console.aws.amazon.com/s3/), click **Create bucket**, and follow the prompts (see [Amazon's guide](https://docs.aws.amazon.com/AmazonS3/latest/userguide/creating-bucket.html) for details). Upload a file and give it public-read permissions. Creating an Amazon S3 bucket Click the uploaded file to open its details, which include the public link. Copy only the first part of the link, the hostname and bucket path, for example `https://s3-eu-west-1.amazonaws.com/your-bucket/`. Don't include the file name. Getting the S3 object URL Log in to your [bunny.net dashboard](https://dash.bunny.net) and create a new Pull Zone. Give it a name (this becomes your CDN hostname) and paste the URL from the previous step into the **Origin URL** field, then choose your pricing tiers and click **Add Pull Zone**. For details, see [How to create your first Pull Zone](/docs/cdn/quickstart). Adding a Pull Zone with the S3 bucket as origin Once the configuration has synced to the edge network, request a file through your Pull Zone hostname, for example: ``` https://mys3zone.b-cdn.net/bunny.jpg ``` If the file is served, Bunny CDN is caching content from your bucket. Replace your S3 URLs with the Bunny CDN URLs in your application to start serving cached content. # Microsoft Azure Storage Blobs Source: https://bunny.net/docs/cdn/integrations/storage/azure-blob Accelerate Azure Blob Storage delivery with bunny.net CDN for faster file access and reduced egress costs. Bunny CDN caches files from your Microsoft Azure Blob Storage container and delivers them from a global edge network, speeding up delivery and reducing egress costs. This guide walks you through the setup in three steps. Prefer to keep your files on bunny.net? [Bunny Storage](/docs/storage) is globally replicated object storage with tight CDN integration, and it offers an [S3-compatible API](/docs/storage/s3) (currently in beta). Set up an Azure Storage container and upload some data, following [Microsoft's quickstart](https://learn.microsoft.com/en-us/azure/storage/blobs/storage-quickstart-blobs-portal). Click the three dots to the right of your file, then select **Blob Properties**. Opening Blob Properties Copy the blob URL. It looks like this: ``` https://harrytest1.blob.core.windows.net/cdn01/bunnycdn.png ``` The blob URL in Blob Properties For the Pull Zone origin you only need the hostname and container path, so remove the file name: ``` https://harrytest1.blob.core.windows.net/cdn01/ ``` Log in to your [bunny.net dashboard](https://dash.bunny.net) and create a new Pull Zone. Give it a name (this becomes your CDN hostname), paste the trimmed URL into the **Origin URL** field, choose your pricing tiers, and click **Add Pull Zone**. For details, see [How to create your first Pull Zone](/docs/cdn/quickstart). Adding a Pull Zone with the Azure container as origin Once the configuration has synced to the edge network, request a file through your Pull Zone hostname, for example: ``` https://azuretest.b-cdn.net/bunnycdn.png ``` If the file is served, Bunny CDN is caching content from your container. Replace your container URLs with the Bunny CDN URLs in your application to start serving cached content. # Backblaze Source: https://bunny.net/docs/cdn/integrations/storage/backblaze Speed up Backblaze B2 file delivery with bunny.net CDN for global caching and reduced bandwidth costs. Bunny CDN caches files from your Backblaze B2 bucket and delivers them from a global edge network, speeding up delivery and saving on bandwidth costs. This guide uses S3-compatible authentication to securely connect a private B2 bucket. If you don't have a Backblaze B2 account yet, you can [create one for free](https://www.backblaze.com/b2/sign-up.html). Prefer to keep your files on bunny.net? [Bunny Storage](/docs/storage) is globally replicated object storage with tight CDN integration, and it offers an [S3-compatible API](/docs/storage/s3) (currently in beta). Log in to Backblaze and click **Create a Bucket**. Set the bucket to **Private** mode to keep your content secure while it's served to users. Encryption and Object Lock can be left at their defaults. Creating a Backblaze B2 bucket Setting the bucket to Private mode To authenticate securely, Bunny CDN connects to your bucket using S3 authentication. Select **App Keys** in the Backblaze dashboard. The App Keys page Click **Add a New Application Key**. Adding a new application key Select the private bucket you created and ensure **Allow List All Bucket Names** is checked, then click **Create New Key**. Configuring the application key On the confirmation page, save the **keyID** and **applicationKey** somewhere safe. You'll need them in a later step. The created application key details Click **Browse Files** and upload a test file if you don't have one. Click the **(i)** info tooltip next to the file. Browsing files and opening the info tooltip Find the **S3 URL** and save it, excluding the file name. For example, for a bucket named `bunnytestbucket`: ``` https://bunnytestbucket.s3.eu-central-003.backblazeb2.com ``` The S3 URL in the file details Log in to your [bunny.net dashboard](https://dash.bunny.net) and open **Add Pull Zone**. Give it a name (this becomes your CDN hostname) and paste the S3 URL from the previous step into the **Origin URL** field. The Host Header is generated automatically and doesn't need changing. For details, see [How to create your first Pull Zone](/docs/cdn/quickstart). Adding a Pull Zone with the B2 S3 URL as origin To use an existing Pull Zone instead, set the same Origin URL in its settings. Reconfiguring an existing Pull Zone Open the **S3 Authentication** section of your Pull Zone's **Security** settings and click **Enable AWS S3 Authentication**. Fill in the details using the keys from step 2: * **AWS Key**: your B2 `keyID` * **AWS Secret**: your B2 `applicationKey` * **AWS Region Name**: the region from your origin URL (for example, `eu-central`) Configuring AWS S3 authentication on the Pull Zone Click **Save Configuration**. With everything configured, request a file through your Pull Zone hostname, for example: ``` https://bunnytestwordpress.b-cdn.net/code-to-share.js ``` If the file is served, your private B2 bucket is protected by S3 authentication and accelerated by Bunny CDN. Replace your existing URLs with the Bunny CDN URLs in your application to start serving cached content. # DigitalOcean Spaces Source: https://bunny.net/docs/cdn/integrations/storage/digitalocean-spaces Accelerate DigitalOcean Spaces content delivery with bunny.net CDN for faster global file access. Bunny CDN pulls and caches files from your DigitalOcean Space and delivers them from a global edge network, getting your content to users faster than serving from the Space alone. This guide walks you through the setup in three steps. Prefer to keep your files on bunny.net? [Bunny Storage](/docs/storage) is globally replicated object storage with tight CDN integration, and it offers an [S3-compatible API](/docs/storage/s3) (currently in beta). Set up your Space and upload some content, following [DigitalOcean's Spaces quickstart](https://docs.digitalocean.com/products/spaces/). Note the **Space URL** shown on the Create a Space page, you'll use it as your Pull Zone origin. Disable DigitalOcean's built-in Spaces CDN. Leaving it enabled interferes with Bunny CDN. DigitalOcean Space URL Log in to your [bunny.net dashboard](https://dash.bunny.net) and create a new Pull Zone. Give it a name and set the origin URL to your Space URL from the previous step, then choose your pricing and region tiers and click **Add Pull Zone**. For details, see [How to create your first Pull Zone](/docs/cdn/quickstart). Adding a Pull Zone with the Space URL as origin Upload some files to your Space and make sure their permissions are set to **Public**, so Bunny CDN can fetch and cache them. Once the configuration has synced to the edge network, request a file through your Pull Zone hostname. For example: ``` https://bunnyspace.ams3.digitaloceanspaces.com/bunny-flying.png ``` becomes: ``` https://bunnyspace.b-cdn.net/bunny-flying.png ``` If the `b-cdn.net` URL serves the file, Bunny CDN is caching content from your Space. Replace your Space URLs with the Bunny CDN URLs in your application to start serving cached content. # OVH Public Cloud Source: https://bunny.net/docs/cdn/integrations/storage/ovh Use bunny.net CDN with OVH Public Cloud storage for faster file delivery and improved global performance. Bunny CDN caches files from your OVH Public Cloud storage container and delivers them from a global edge network, speeding up delivery and reducing bandwidth costs. This guide walks you through the setup in three steps. Prefer to keep your files on bunny.net? [Bunny Storage](/docs/storage) is globally replicated object storage with tight CDN integration, and it offers an [S3-compatible API](/docs/storage/s3) (currently in beta). If you don't already have an OVH Public Cloud storage container, set one up following [OVH's guide](https://docs.ovh.com/gb/en/storage/pcs/create-container/). Bunny CDN works with both the **Static hosting** and **Public** container options. Once your container is created, copy the full container URL from the container page. This forms the basis of your Pull Zone, so you can append a file path inside the container to your Pull Zone hostname. The OVH container URL Log in to your [bunny.net dashboard](https://dash.bunny.net) and create a new Pull Zone. Give it a name (this becomes your CDN hostname), paste the container URL into the **Origin URL** field, choose your pricing tier, and click **Add Pull Zone**. For details, see [How to create your first Pull Zone](/docs/cdn/quickstart). Adding a Pull Zone with the OVH container as origin Once the configuration has synced to the edge network, append a file name to your Pull Zone hostname, for example: ``` https://ovhstorage.b-cdn.net/bunny.png ``` If the file is served, Bunny CDN is caching content from your container. Replace your container URLs with the Bunny CDN URLs in your application to start serving cached content. # Wasabi Source: https://bunny.net/docs/cdn/integrations/storage/wasabi Speed up Wasabi cloud storage delivery with bunny.net CDN for global caching and faster file access. Bunny CDN caches files from your Wasabi bucket and delivers them from a global edge network, speeding up delivery affordably. This guide walks you through the setup in three steps. Prefer to keep your files on bunny.net? [Bunny Storage](/docs/storage) is globally replicated object storage with tight CDN integration, and it offers an [S3-compatible API](/docs/storage/s3) (currently in beta). If you don't already have a Wasabi bucket with some data in it, set one up first. Wasabi has a [tutorial video](https://www.youtube.com/watch?v=VSmSYDCuOEg) covering this. First, work out your bucket's URL. Navigate to `https://s3.wasabisys.com/BUCKETNAME/filepath` and you'll be redirected to the region that hosts your bucket, giving a URL like: ``` https://s3.us-west-1.wasabisys.com/bunnycdn/bunnycdn.png ``` For the Pull Zone origin, drop the file path and keep the bucket URL: ``` https://s3.us-west-1.wasabisys.com/bunnycdn/ ``` Log in to your [bunny.net dashboard](https://dash.bunny.net) and create a new Pull Zone. Give it a name (this becomes your CDN hostname), paste the bucket URL into the **Origin URL** field, choose your pricing tier, and click **Add Pull Zone**. For details, see [How to create your first Pull Zone](/docs/cdn/quickstart). Adding a Pull Zone with the Wasabi bucket as origin Once the configuration has synced to the edge network, append a file name to your Pull Zone hostname, for example: ``` https://wasabitest.b-cdn.net/bunnycdn.png ``` If the file is served, Bunny CDN is caching content from your bucket. Replace your Wasabi URLs with the Bunny CDN URLs in your application to start serving cached content. # IP Family Policy Source: https://bunny.net/docs/cdn/ip-family-policy Control which IP address families your Pull Zone advertises and which edge servers are eligible to serve it. The **IP Family Policy** of a Pull Zone controls which IP address families bunny.net advertises for your CDN hostnames, and which edge servers are eligible to serve them. The policy affects the DNS answers returned for your Pull Zone hostname (`*.b-cdn.net`, custom hostnames, and hostnames [accelerated through Bunny DNS](/docs/cdn/cdn-acceleration)): * **A records** advertise an IPv4 address of the selected edge server. * **AAAA records** advertise an IPv6 address of the selected edge server. * **HTTPS (SVCB) records** carry `ipv4hint` and `ipv6hint` values, along with matching A and AAAA records in the additional section. ## Available policies | Policy | Answers returned | Edge server selection | | --------------------------- | ---------------------------------------------- | ------------------------------ | | **IPv4 Only** | A only | Unrestricted | | **Dual Stack** (default) | A, plus AAAA when the selected server has IPv6 | Unrestricted | | **Dual Stack, Prefer IPv6** | A, plus AAAA when the selected server has IPv6 | IPv6-capable servers preferred | | **IPv6 Only** | AAAA only | IPv6-capable servers only | ## IPv4 Only Only IPv4 is advertised. AAAA records are never returned, even when the serving edge location supports IPv6, and HTTPS records contain an `ipv4hint` only. Edge server selection is unaffected: all eligible locations are available, regardless of their IPv6 capability. Use this policy when you require every request to arrive over IPv4, or when your clients or intermediate networks have known IPv6 problems. It is also the safest choice when something keys on the client's address, such as [IP-locked tokens](#token-authentication-and-ip-locking), rate limits, or allowlists, since a dual-stack client can otherwise switch family between requests. ## Dual Stack The default policy. IPv4 is always advertised, and IPv6 is advertised whenever the edge server chosen for the request has an IPv6 address. * **A queries** return the IPv4 address of the selected server. * **AAAA queries** return the IPv6 address of the selected server, if it has one. If it doesn't, the answer is empty (NODATA) and the client uses IPv4. * **HTTPS queries** return both `ipv4hint` and `ipv6hint` when both families are available. Routing is not biased in any way. The nearest and least loaded location is selected exactly as it would be for an IPv4-only zone, and IPv6 is used opportunistically when that location supports it. Because the Standard Network supports IPv6 on a best-effort basis, a Dual Stack zone may return AAAA records for some clients and not others depending on which Point of Presence serves them. See [Connectivity](/docs/cdn/connectivity) for network tier details. ## Dual Stack, Prefer IPv6 Both address families are still advertised exactly as with Dual Stack, but routing is biased: IPv6-capable edge servers are preferred over IPv4-only ones, even when an IPv4-only location is closer to the client. This maximizes the share of your traffic delivered over IPv6, at the cost of potentially routing some users to a more distant Point of Presence. It is also the best option when something downstream can only reach you over IPv6, such as an IPv6-only network or a service that pulls from your Pull Zone without IPv4 connectivity. By steering requests to IPv6-capable locations, this policy does its best to guarantee that an AAAA answer is available, while still returning A records so IPv4 clients keep working. **IPv6 Only** makes that guarantee absolute, but returns nothing at all when no IPv6-capable location is available. The bias is relaxed if the client is known to be IPv4. If EDNS Client Subnet tells us the client's address is IPv4, there is no benefit in preferring an IPv6-capable location, so selection falls back to normal distance and load based routing. AAAA queries keep the bias regardless, since only IPv6-capable clients act on an AAAA answer. Preferring IPv6 can route users to a Point of Presence that is not the closest one available. On the Standard Network, where IPv6 coverage is still expanding, this may measurably increase latency for users in regions without IPv6-enabled locations. ## IPv6 Only Only IPv6 is advertised, and only IPv6-capable edge servers are eligible to serve the zone. * **AAAA queries** return the IPv6 address of the selected server. * **A queries** return an empty answer with the zone's SOA record. No IPv4 address is ever advertised, even to a client that asks for one. * **HTTPS queries** contain an `ipv6hint` only, with no additional A record. * If no IPv6-capable edge server is available for the request, the response is empty, it will never fall back to IPv4. An IPv6 Only Pull Zone is unreachable for IPv4-only clients, which still make up a substantial share of global traffic. Only use this policy when you control every client that connects, such as an internal or machine-to-machine workload. ## Token authentication and IP locking [IP locking](/docs/cdn/security/token-authentication/advanced#ip-locking) binds a token to the client IP address you sign it with, and rejects any request arriving from a different address. The signed address and the address the request actually arrives from must therefore be in the same family. On a zone reachable over both families, a client can be handed an address in one family while your token was signed for the other, and every such request fails validation. To prevent that, enabling **Token IP Validation** on a Pull Zone pins the zone to a single address family: | IP Family Policy | Effective policy with Token IP Validation enabled | | ----------------------- | ------------------------------------------------- | | IPv4 Only | IPv4 Only | | Dual Stack | IPv4 Only | | Dual Stack, Prefer IPv6 | IPv4 Only | | IPv6 Only | IPv6 Only | In other words, token IP validation forces IPv4 Only unless you have explicitly chosen **IPv6 Only**, which is always respected. Sign the family your zone serves: IPv4 addresses in the default case, IPv6 addresses when the policy is IPv6 Only. Both IPv4 and IPv6 addresses can be signed. IPv4 binds either the exact address or a `/24`, and IPv6 binds the client's `/64` prefix. See [IP locking](/docs/cdn/security/token-authentication/advanced#ip-locking) for the exact matching rules. ## Choosing a policy * **Leave it on Dual Stack** unless you have a specific reason not to. It serves IPv6 wherever it's available without ever compromising routing quality. * **Choose Dual Stack, Prefer IPv6** when maximizing IPv6 delivery matters more than the last few milliseconds of latency, or when a downstream service can only reach you over IPv6 and you need AAAA answers wherever possible without losing IPv4 clients. * **Choose IPv4 Only** when something keys on the client's IPv4 address, such as IP-locked tokens, rate limits, or allowlists, or when your clients' IPv6 support is unreliable. A Pull Zone with [token IP validation](#token-authentication-and-ip-locking) falls back to this policy automatically. * **Choose IPv6 Only** only for closed, fully IPv6-capable client populations, or when every connection must arrive over IPv6 so that IP-locked tokens can be signed against IPv6 addresses. IP Family Policy applies to inbound connectivity, meaning how clients reach the CDN. Connectivity from the edge to your origin is negotiated separately and is described in [Connectivity](/docs/cdn/connectivity). # CDN Limits and Defaults Source: https://bunny.net/docs/cdn/limits Overview of default limits for bunny.net Pull Zones. This page outlines the default limits applied to Pull Zones. All limits can be increased upon request by contacting support with details about your use case. *** ## Zone Limits | Limit | Default | | --------------------------- | ------- | | Max Pull Zones | 500 | | Max Hostnames per Pull Zone | 10 | *** ## Edge Rule Limits | Limit | Default | | -------------------------- | ------- | | Max Edge Rules per Zone | 50 | | Max Triggers per Rule | 5 | | Max Conditions per Trigger | 5 | *** ## Security Limits | Limit | Default | | --------------------- | ------- | | Max Blocked Referrers | 50 | *** If you require higher limits on any of the above, please contact support with your use case and expected scale. Most limits can be adjusted. # Log Forwarding Source: https://bunny.net/docs/cdn/logging/forwarding Forward CDN access logs in real-time to your Syslog endpoint for monitoring and analysis. # What is Log Forwarding? Log Forwarding enables you to receive raw logs in real-time to the configured Syslog destination. There might be up to a 10-30 second delay between an actual request and the log hitting your Syslog endpoint. # Log format The logs are sent in the standard Syslog RFC 5424 format. The log value is formatted in the [bunny.net Log Format](/docs/cdn/logging). An example log would look like this: ```bash theme={null} <165>1 2021-01-01T22:14:15.003Z mymachine.example.com evntslog - ID47 [exampleSDID@32473 iut="3" eventSource="BunnyCDN" eventID="1011"] BOMHIT|200|1507167062421|412|390|163.172.53.229|-|https://bunnycdn.b-cdn.net/assets/landingpage/images/cdn-video-preview-from-a-blue-moon.m4v|WA|Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36|322b688bd63fb63f2babe9de30a5d262|DE ``` ### UDP reliability The UDP protocol is unaware of lost packets and does not contain an auto-retry mechanism. This means that in a case of packet loss or connectivity issue between your forwarding endpoint and our logging service, some packets might get lost on the way. While this is likely a rare occurrence, it is important to keep in mind in case your logging relies on receiving 100% of the requests. ### UDP security The [UDP protocol](https://bunny.net/academy/network/what-is-user-datagram-protocol-udp-and-how-does-it-work) does not provide a layer of security. This means all the logs are sent to your endpoint in a raw, unencrypted form. While unlikely, please note that those might be vulnerable to a potential **man-in-the-middle attack** if you send critical information as part of your logs. Please note: We aim to forward all logs, but cannot guarantee 100% delivery due to transient network issues. For precise reporting, use the CDN Logging API or Statistics API. # How to configure Log Forwarding? To enable realtime Log Forwarding, you can follow the following steps: 1. Visit your **Pull Zone** details page 2. Open the **Security** panel in the left-side menu 3. Open the **Logging** panel inside of the Security panel 4. Make sure that the **Enable Logging** feature is enabled 5. Enable the **Enable Log Forwarding** feature 6. Enter the **Hostname** of your Syslog endpoint. This can be either an IP or a host address where your listening server is enabled. 7. Enter the **Port** of your Syslog endpoint. Make sure the port is open to the internet, as otherwise our servers will not be able to reach you 8. (Optional) Configure the token 9. Click on the **Save Forwarding Configuration** button 10. Fire up a couple of requests to bunny.net and monitor your endpoint for new logs # Logging Source: https://bunny.net/docs/cdn/logging/index Access raw request logs via API or dashboard with a 3 day log retention policy. bunny.net provides raw request logs for all Pull Zones with Logging Enabled. Logs appear in near real-time and they're retained for 3 days. For longer retention, use [Permanent Log Storage](/docs/cdn/logging/permanent-storage) to automatically archive logs to Edge Storage. We expose two HTTP APIs for accessing logs: * **[Logging API v2](#logging-api-v2)** - recommended. Structured JSON, rich filtering, pagination, and per-field search. * **[Logging API v1](#logging-api-v1-legacy)** - legacy. Streams a raw pipe-delimited file for a given day; preserved for existing integrations. ## Privacy & GDPR IP addresses are anonymized by default. To enable full IP logging, sign the Data Processing Agreement (DPA) in your account settings, then disable anonymization in your Pull Zone logging settings. When anonymization is enabled, anonymized IPs are returned by the API in one of two forms depending on your Pull Zone setting: * **Remove last octet** - IPv4 is masked to a `/24` (e.g. `163.172.53.0`); IPv6 is masked to a `/64` (e.g. `2001:7d0:700d:db04::`). * **Drop all** - IPv4 becomes `0.0.0.0` and IPv6 becomes `::`. Anonymization applies on read at the API boundary. Filtering by `remoteIp` adapts to the same mask width, so the filter can never reveal information beyond what the response shows. ## Logging API v2 ```bash theme={null} GET https://logging.bunnycdn.com/v2/pullzones/{pullZoneId}/logs ``` Authenticate with your account API key or a bearer JWT: ```bash theme={null} AccessKey: your-api-key # or Authorization: Bearer ``` Logs are retained for the past 3 days. Both `from` and the range `to - from` must fall within that window. ### Example ```bash theme={null} curl -H "AccessKey: your-api-key" \ "https://logging.bunnycdn.com/v2/pullzones/1337/logs?from=2026-05-08T00:00:00Z&to=2026-05-09T00:00:00Z&status=4xx,5xx&limit=50" ``` ### Query parameters | Parameter | Type | Description | | --------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `from` | ISO 8601 UTC | Inclusive start of the time range. Defaults to `to - 24h`. Must fall within the 3-day retention window. | | `to` | ISO 8601 UTC | Exclusive end of the time range. Defaults to `now`. | | `status` | comma list | HTTP status filter. Each entry is either an exact code (`200`, `404`) or a class (`2xx`, `5xx`). Multiple entries are OR-ed. | | `cacheStatus` | comma list | Cache statuses to match exactly (e.g. `HIT,MISS,EXPIRED`). | | `country` | comma list | ISO 3166 alpha-2 country codes (e.g. `EE,DE`). | | `edgeLocation` | string | Edge location / server zone, exact match. | | `remoteIp` | IPv4 or IPv6 | Filter by client IP. The match width adapts to the zone's anonymization setting: exact when anonymization is disabled, `/24` (IPv4) or `/64` (IPv6) under last-octet anonymization, and ignored under full anonymization. | | `urlContains` | string | Case-insensitive substring match against host + path. | | `userAgentContains` | string | Case-insensitive substring match against the User-Agent header. | | `refererContains` | string | Case-insensitive substring match against the Referer header. | | `search` | space-separated | Free-text token search. A row matches if ANY token appears in ANY searched column (cache status, request ID, edge location, host, path, user agent, referer, and content range for extended-logging zones). Max 16 tokens of up to 128 characters each. | | `includeOriginShield` | boolean | Include origin-shield (edge → shield) requests. Defaults to `false`. | | `limit` | integer | Maximum entries per page. Defaults to `100`, capped at `10000`. | | `offset` | integer | Number of entries to skip. Defaults to `0`. | | `order` | string | Sort by timestamp: `asc` or `desc` (default). | | `requestId` | UUID | Exact request ID lookup. Accepts standard UUID format (with or without hyphens). | Country code, the encrypted-at-rest authorization header, and the raw client IP are not matched by the free-text `search` parameter. Use `country`, `remoteIp`, and the dedicated header filters for those. ### Response `200 OK` returns a JSON envelope with the page of entries, pagination state, and an echoed query summary: ```json theme={null} { "data": [ { "timestamp": "2026-05-08T14:32:11.265Z", "pullZoneId": 1337, "requestId": "648fd832dbc1b102134949e15a9fdb2d", "cacheStatus": "MISS", "statusCode": 404, "bytesSent": 1095, "remoteIp": "163.172.53.0", "countryCode": "FI", "edgeLocation": "WA", "scheme": "https", "host": "example.b-cdn.net", "path": "/video.mp4", "url": "https://example.b-cdn.net/video.mp4", "userAgent": "Mozilla/5.0 ...", "referer": null, "ja4Fingerprint": "t13d1513h2_8daaf6152771_1eb89897b454", "asn": 15169, "asnOrganization": "Google LLC" } ], "pagination": { "offset": 0, "limit": 100, "returned": 1, "hasMore": false }, "query": { "pullZoneId": 1337, "from": "2026-05-08T00:00:00Z", "to": "2026-05-09T00:00:00Z", "order": "desc" } } ``` ### Log entry fields | Field | Type | Description | | ----------------- | --------------- | ---------------------------------------------------------------------------- | | `timestamp` | ISO 8601 UTC | Time the request was received at the edge (millisecond precision). | | `pullZoneId` | integer | Pull Zone identifier the request was served from. | | `requestId` | string | 32-character hex unique request identifier. | | `cacheStatus` | string | Cache result (see [cache status values](#cache-status-values) below). | | `statusCode` | integer | HTTP response status code. | | `bytesSent` | integer | Total bytes sent in the response (headers + body). | | `remoteIp` | string \| null | Client IP, possibly anonymized; `null` when no IP was recorded. | | `countryCode` | string \| null | ISO 3166 alpha-2 country code derived from the client IP; `null` if unknown. | | `edgeLocation` | string | POP code that served the request. | | `scheme` | string | `http` or `https`. | | `host` | string | Request `Host` header. | | `path` | string | Request URI path (with query string if present). | | `url` | string | Fully composed URL: `{scheme}://{host}{path}`. | | `userAgent` | string \| null | `User-Agent` header; `null` when absent. | | `referer` | string \| null | `Referer` header; `null` when absent. | | `ja4Fingerprint` | string \| null | JA4 TLS client fingerprint; `null` when absent. | | `asn` | integer \| null | Autonomous System Number derived from the client IP; `null` if unknown. | | `asnOrganization` | string \| null | Organization that owns the AS; `null` if unknown. | #### Extended logging fields When extended logging is enabled for the Pull Zone (contact support), entries also include: | Field | Type | Description | | --------------------- | -------------- | ----------------------------------------------------------- | | `bodyBytesSent` | integer | Response body bytes, excluding headers. | | `contentRange` | string \| null | HTTP `Content-Range` header. | | `authorizationHeader` | string \| null | Decrypted `Authorization` header value (encrypted at rest). | Null fields are omitted from the JSON response. #### Cache status values | Value | Meaning | | ------------- | ----------------------------------------------------------------------------------------- | | `HIT` | Response served directly from cache. | | `MISS` | Response not in cache; fetched from origin. | | `BYPASS` | Cache was skipped (e.g. due to request headers, cookies, or configuration). | | `REVALIDATED` | Cached response was validated with origin (e.g. via `ETag` / `Last-Modified`) and reused. | | `STALE` | Stale cached response served (typically due to origin being unavailable or slow). | | `UPDATING` | Stale content served while a background cache update is in progress. | | `-` | No cache interaction (e.g. non-cacheable methods like `POST`). | ### Pagination The response sets `pagination.hasMore` to `true` when more results are available beyond the current page. To fetch the next page, repeat the request with `offset` advanced by `limit`: ```bash theme={null} curl -H "AccessKey: your-api-key" \ "https://logging.bunnycdn.com/v2/pullzones/1337/logs?from=...&to=...&limit=100&offset=100" ``` ### Error responses Errors return a structured JSON envelope: ```json theme={null} { "error": { "code": "invalid_request", "message": "One or more query parameters are invalid.", "details": [ "Time range cannot exceed 3 days (log retention window).", "limit must be between 1 and 10000." ] } } ``` | HTTP status | `error.code` | Meaning | | ----------- | ------------------ | ---------------------------------------------------------------- | | 400 | `invalid_request` | One or more query parameters failed validation. See `details`. | | 401 | `unauthorized` | Neither `Authorization` nor `AccessKey` header was provided. | | 403 | `forbidden` | Credentials are invalid, or the Pull Zone is suspended/disabled. | | 404 | `logging_disabled` | Logging is not enabled for this Pull Zone. | | 429 | `rate_limited` | Per-Pull-Zone rate limit exceeded (30 requests / 10 seconds). | | 500 | `internal_error` | Unexpected server error. | ## Logging API v1 (Legacy) The v1 API is preserved for existing integrations. New work should use [Logging API v2](#logging-api-v2), which returns structured JSON, supports rich filtering and pagination, and avoids the pipe-injection issues inherent to the v1 format. Download raw log files from the logging API: ```bash theme={null} GET https://logging.bunnycdn.com/{MM}-{DD}-{YY}/{pull_zone_id}.log ``` Authenticate with your account API key: ```bash theme={null} AccessKey: your-api-key ``` Use gzip compression to reduce download size: ```bash theme={null} Accept-Encoding: gzip ``` **Example:** ```bash theme={null} curl -H "AccessKey: your-api-key" \ -H "Accept-Encoding: gzip" \ "https://logging.bunnycdn.com/03-07-26/1337.log" ``` ### Query parameters | Parameter | Description | | ---------- | ---------------------------------------------------------------------------- | | `start` | Number of log lines to skip from the beginning | | `end` | End index of log lines to return | | `sort` | Sort order: `asc` or `desc` (default) | | `status` | Filter by status codes: `2`, `3`, `4`, `5` (e.g. `2,3,4,5` (default) or `2`) | | `search` | Filter log lines by string match | | `download` | Whether to return logs as a downloadable file (default: `true`) | ### Log format Each request is stored as a pipe-separated line: ``` HIT|200|1507167062421|412|390|163.172.53.0|-|https://example.b-cdn.net/video.mp4|WA|Mozilla/5.0...|322b688bd63fb63f2babe9de30a5d262|DE ``` | Field | Description | | ------------- | ------------------------------------------------------------------------------ | | Cache Status | Cache result for the request. See [cache status values](#cache-status-values). | | Status Code | HTTP response code | | Timestamp | UTC UNIX timestamp (milliseconds) | | Bytes Sent | Total bytes sent to client | | Pull Zone ID | ID of the Pull Zone | | Remote IP | Client IP (anonymized by default) | | Referer | Referer header value | | URL | Requested URL | | Edge Location | POP code that served the request | | User Agent | Client user agent | | Request ID | Unique request identifier | | Country Code | Two-letter ISO country code | Pipe (`|`) characters in user-controllable fields (Referer, URL, User-Agent, Content-Range, Authorization) are stripped from the v1 output to keep the delimiter unambiguous. If you need the original byte sequence, use v2. ### Extended logging Extended logging adds three additional fields (contact support to enable): | Field | Description | | -------------------- | ---------------------------------------------- | | Body Bytes Sent | Response body bytes (excluding headers) | | Range Header | HTTP Range header value | | Authorization Header | Authorization header value (Encrypted at rest) | ## HTTP status codes These codes apply to logged responses served by the CDN edge and are surfaced via both API versions. ### 2XX Success | Code | Description | Explanation | | ---- | --------------- | --------------------------------------------------------- | | 200 | OK | Request successful, content served | | 201 | Created | Request successful, new resource created | | 204 | No Content | Request processed, no content to return | | 206 | Partial Content | Partial GET request fulfilled (e.g., video range request) | ### 3XX Redirection | Code | Description | Explanation | | ---- | ----------------- | ---------------------------------------------- | | 301 | Moved Permanently | URL permanently changed, use new location | | 302 | Found | URL temporarily moved | | 303 | See Other | Response available at different location | | 304 | Not Modified | Cached content still valid, no transfer needed | ### 4XX Client Errors | Code | Description | Explanation | | ---- | --------------------- | ------------------------------------------------------------------ | | 400 | Bad Request | Server could not understand the request | | 401 | Unauthorized | Authentication required | | 403 | Forbidden | Server refuses to fulfill request (hotlink protection, token auth) | | 404 | Not Found | File not found | | 405 | Method Not Allowed | Request method not supported | | 410 | Gone | Resource no longer available | | 429 | Too Many Requests | Rate limit exceeded | | 499 | Client Closed Request | Client terminated connection before response completed | A 499 status code appears in logs when a client closes the connection before the server finishes responding. This commonly occurs due to mobile network interruptions, ad blockers, or users navigating away. It's normal and doesn't indicate a server problem. ### 5XX Server Errors | Code | Description | Explanation | | ---- | --------------------- | -------------------------------------------------- | | 500 | Internal Server Error | Unexpected server error | | 502 | Bad Gateway | Invalid response from upstream server | | 503 | Service Unavailable | Server temporarily overloaded or under maintenance | | 504 | Gateway Timeout | Upstream server didn't respond in time | 502 and 504 errors typically indicate the CDN edge nodes cannot reach your origin. Check if your origin firewall is blocking bunny.net IPs. # Origin Errors Source: https://bunny.net/docs/cdn/logging/origin-errors No more 502 guesswork. See exactly why your origin failed in real time. When your origin stops responding, you shouldn’t have to dig through layers of logs or guess whether it’s DNS, timeouts, or misconfiguration. Origin Errors Monitoring gives you full visibility into failed origin requests, directly in the dashboard or via API. Available for all users at no extra cost. ## Status Codes * 400 * 416 * 500 * 502 * 504 * 508 ## Error Codes * http\_request\_exception * http\_invalid\_range * http\_request\_failure * http\_timeout — request exceeded the **60-second** CDN request timeout * http\_loop\_detected * http\_invalid\_compression * network\_socket\_exception * network\_io\_error * dns\_lookup * notfound\_localdb * error ## Dashboard Browse and filter origin errors under Monitoring → Origin Errors to quickly identify where and why failures occurred. Each entry includes timestamp, edge region, pull zone ID, error type, and status code, helping you isolate the issue instantly. ## API Access Access the same data programmatically to integrate with your monitoring or alerting systems. Endpoint: *GET* `https://cdn-origin-logging.bunny.net/{pullZoneId}/{dateTime:MM-dd-yyyy}` Example Response: ```json theme={null} { "logs": [ { "logId": "a6a6b755-b6a4-46be-b523-aa82a17d4bc5", "timestamp": 1728952065848, "log": "{\"RequestUrl\":\"/apikey\",\"PullZoneId\":308006,\"Message\":\"Origin DNS lookup failed...\",\"ErrorCode\":\"dns_lookup\",\"StatusCode\":502}", "labels": { "ErrorCode": "dns_lookup", "StatusCode": "502", "ServerZone": "CA" } } ] } ``` Authenticate with your API key `AccessKey` or user JWT `Authorization`, and query by Pull Zone ID and date to retrieve recent origin error events. # Permanent Log Storage Source: https://bunny.net/docs/cdn/logging/permanent-storage Permanent Log Storage allows you to permanently store access logs inside of an Edge Storage zone. Parts are uploaded when they are closed (by size, time, or at midnight UTC). These logs are not searchable through our web interface. The stored log files are compressed using **GZip** and must be decompressed before opening. **Part rotation:** Logs are written into part files that are closed and uploaded when any of the following occurs: * The part reaches **2 GB** in size, or * The part reaches **5,000,000 lines**, or * **120 minutes** (2 hours) have passed since the part was opened, or * The part has had **no writes for 1 hour**, or * **Midnight UTC** (the current part is closed and a new day starts). So multiple parts per day are possible, and each part is uploaded shortly after it is closed. The first part of each UTC day has index `0`; subsequent parts that day use index `1`, `2`, and so on. Each filename includes a unique **random suffix** (8 hex characters) so every part is distinct in storage and uploads never overwrite each other. Because the pipeline can run on multiple log processing worker nodes, each worker writes and uploads its own parts. The filename includes a **worker ID** (server ID) so you can tell which log processing worker produced which file. The logs are stored in the selected Edge Storage zone under the following path format: ```bash theme={null} pullzone-logs////
_--.gzip ``` * `` — The pull zone's bucket name (storage location). * `//
` — UTC date of the log data. * `` — ID of the log processing worker node (server) that wrote the file. * `` — Zero-based index for that worker on that day (0, 1, 2, …). * `` — 8-character hex suffix that uniquely identifies the part file and ensures uploads never overwrite each other in storage. **Example:** `pullzone-logs/myzone/2026/03/07_100-0-a1b2c3d4.gzip` is the first part for March 7, 2026 from worker 100. The Pull Zone logs are stored in the Edge Storage which might be exposed to the world through any connected Pull Zone. We strongly recommend using a separate Edge Storage zone to keep the log files to prevent any unauthorized access. Please note: We aim to store all logs, but cannot guarantee complete logs due to transient network issues. For precise reporting, use the CDN Logging API or Statistics API. # How to configure Permanent Log Storage? To enable Permanent Log Storage you can follow the following steps: 1. Visit your **Pull Zone** details page 2. Open the **Security** panel in the left-side menu 3. Open the **Logging** panel inside of the Security panel 4. Make sure that the **Enable Logging** feature is enabled 5. Enable the **Enable Permanent Storage** feature 6. Select the desired Edge Storage zone that will contain your log files 7. Click on the **Save Storage Configuration** button 8. Check the Storage Zone the next day to find your log files # Performance Source: https://bunny.net/docs/cdn/performance/index Optimize delivery speed, reduce origin load, and control bandwidth usage. Bunny CDN includes several features to improve content delivery performance and give you control over how traffic is handled. Reduce origin traffic by routing all CDN requests through a single caching layer. Show a branded loading screen when your origin takes longer than expected to respond. Limit CDN routing to specific geographic regions for compliance and data residency. Control download speeds, request rates, and bandwidth usage per Pull Zone. ## Compression Bunny CDN automatically compresses responses using gzip, Brotli, or Zstandard based on the client's `Accept-Encoding` header. Compression is applied to: * Files with extensions: `.css`, `.js`, `.json`, `.xml`, `.svg`, `.html` * Any response with a supported MIME type (even if `Content-Type` header is missing) **Application types:** `application/javascript`, `application/json`, `application/ld+json`, `application/xml`, `application/xhtml+xml`, `application/rss+xml`, `application/atom+xml`, `application/manifest+json`, `application/x-javascript`, `application/x-web-app-manifest+json`, `application/vnd.geo+json`, `application/vnd.apple.mpegurl`, `application/x-mpegurl`, `application/dash+xml`, `application/wasm` **Font types:** `application/font`, `application/font-sfnt`, `application/vnd.ms-fontobject`, `application/x-font-opentype`, `application/x-font-truetype`, `application/x-font-ttf`, `font/eot`, `font/opentype`, `font/otf`, `font/truetype`, `font/ttf` **Text types:** `text/html`, `text/css`, `text/javascript`, `text/js`, `text/plain`, `text/xml`, `text/csv`, `text/cache-manifest`, `text/richtext`, `text/tab-separated-values`, `text/x-component`, `text/x-java-source`, `text/x-script` **Image types:** `image/svg+xml`, `image/vnd.microsoft.icon`, `image/x-icon` **Other:** `model/gltf-binary` If you need a specific MIME type added to the compression list, [contact support](https://dash.bunny.net/support). # Network Limits Source: https://bunny.net/docs/cdn/performance/network-limits Control download speeds, request rates, and bandwidth usage per Pull Zone. Network Limits are settings that can be applied to a Pull Zone to control performance and improve security. These limits help prevent abuse, control bandwidth costs, and protect against DDoS attacks. Configure these options in your Pull Zone under **Limits**: Network limits ## Download speed limits Limits the maximum transfer speed per network connection, in kB/s. Set to `0` for unlimited. This setting works together with **Limit after** (see below). ## Limit after The amount of data transferred in a single request after which the client will be rate limited, in kB. Set to `0` for unlimited (rate limiting applies immediately if Download speed limits is set). For example, if Limit after is set to `2000`, the first 2 MB of each request transfers at full speed. After that, the Download speed limit kicks in. This is useful for video delivery. It allows fast video seeking while preventing the browser from downloading too much unnecessary data ahead of playback. ## Requests limits Limits the maximum number of requests per second for a single IP. Set to `0` for unlimited. This helps prevent abuse and reduces the impact of simple DDoS attacks. ## Burst requests The number of requests per second allowed before the limit is applied. Set to `0` to disable burst. This allows users to briefly exceed the rate limit, which is useful when: * A webpage loads and makes many simultaneous asset requests * A user opens multiple tabs at once Additional requests beyond the burst allowance are slowed down to match the Requests limits setting. ## Maximum connections per IP Limits the maximum number of allowed connections to the zone per IP. Set to `0` for unlimited. Use this to: * Prevent users from downloading many files at the same time * Mitigate DDoS attacks This limit is applied per server. If a user connects to multiple Bunny locations or a location has multiple IPs, the limit applies separately to each server. ## Monthly bandwidth limit Limits the allowed bandwidth used in a month, in GB. If the limit is reached, the zone will be disabled. Set to `0` for unlimited. Once the monthly limit is hit, your Pull Zone stops working until the next billing cycle. Use this carefully to avoid unexpected downtime. # Origin Shield Source: https://bunny.net/docs/cdn/performance/origin-shield Reduce origin traffic by routing all CDN requests through a single caching layer. Origin Shield is a secondary caching layer that sits between Bunny's edge PoPs and your origin server. Instead of each PoP fetching files directly from your origin, all requests pass through a single Origin Shield location first. This consolidates cache misses into one point, dramatically reducing the number of requests that actually reach your origin, especially when the same files are requested from different regions around the world. Origin Shield is **not** a Web Application Firewall (WAF). It doesn't filter or block requests, it strictly minimizes origin traffic. For security features like WAF, DDoS protection, and bot detection, see [Bunny Shield](/docs/shield). ## How it works **Without Origin Shield** — each PoP fetches directly from your origin: ```mermaid theme={null} flowchart LR subgraph Edge PoPs Tokyo London NYC[New York] end Origin[(Origin Server)] Tokyo -->|fetch| Origin London -->|fetch| Origin NYC -->|fetch| Origin ``` Your origin receives three separate requests for the same file. **With Origin Shield** — all PoPs route through a single cache: ```mermaid theme={null} flowchart LR subgraph Edge PoPs Tokyo London NYC[New York] end Paris[[Origin Shield
Paris]] Origin[(Origin Server)] Tokyo --> Paris London --> Paris NYC --> Paris Paris -->|single fetch| Origin ``` Your origin receives one request. Subsequent PoP requests are served from the Origin Shield cache. ## Enable Origin Shield Go to **CDN** > **Pull Zones** and select your zone. Go to **Origin Shield** in the **Caching** section. Enable Origin Shield and choose a location closest to your origin server. Enable origin shield ## Choosing a region Select the Origin Shield location that is: * **Closest to your origin server's physical location**, or * **In the region with your highest cache HIT rate** If your origin is in Frankfurt, choose the European Origin Shield. If most of your traffic comes from North America regardless of where your origin is hosted, the US location may give better results. ## Trade-offs Origin Shield adds an extra network hop between edge PoPs and your origin. Depending on the distance between the Origin Shield location and your origin, this can introduce slight additional latency on cache misses. For most use cases, the reduction in origin load far outweighs this overhead. But if ultra-low latency on cache misses is critical and your origin can handle the traffic, you may want to test with and without Origin Shield. Concurrency limits are especially useful for dynamic content or CPU-intensive requests where your origin can get slower under high concurrency. ## Pricing Origin Shield is available at no extra cost. Traffic transferred from Origin Shield locations to CDN edge nodes does not incur any charges. # Routing Filters Source: https://bunny.net/docs/cdn/performance/routing-filters Limit CDN routing to specific geographic regions for compliance and data residency requirements. Routing Filters allow you to control which CDN locations serve your content. When enabled, traffic is routed only to Points of Presence (PoPs) within the selected regions. This is primarily useful for GDPR compliance and data residency requirements where you need to ensure user data stays within specific geographic boundaries. ## How it works When you enable a routing filter (such as European Union), all requests to your Pull Zone are routed exclusively to PoPs within that region. Users outside the filtered region are also routed to those same locations. Combining multiple routing filters creates a cross-section. Traffic is only routed to PoPs contained in **all** selected filters. ## Available filters ### European Union Routes all traffic exclusively through 24 PoPs within EU member states: Austria, Bulgaria, Croatia, Cyprus, Czech Republic, Denmark, Finland, France, Germany, Greece, Hungary, Ireland, Italy, Latvia, Lithuania, Luxembourg, Netherlands, Poland, Portugal, Romania, Slovakia, Slovenia, Spain, and Sweden. ## Enabling Routing Filters 1. Go to your Pull Zone in the [dashboard](https://dash.bunny.net) 2. Navigate to **Pricing & Routing** 3. Enable the desired routing filter The filter applies to both Standard and High Volume tier zones automatically. ## Performance considerations Routing Filters significantly limit global coverage. Users outside the filtered region will experience increased latency since their requests are routed to the nearest PoP within the filter, not the nearest global PoP. For example, with the EU filter enabled: * **EU users**: Excellent performance with 24 local PoPs * **Non-EU users**: Routed to EU PoPs, negating most CDN performance benefits Only enable Routing Filters if you have specific compliance requirements that outweigh the global performance trade-off. ## Limitations Routing Filters only apply to CDN Pull Zone traffic. DNS traffic continues to use the global DNS network, but DNS requests generally don't contain personally identifiable information. # Smart Preloader Source: https://bunny.net/docs/cdn/performance/smart-preloader Show a branded loading screen when your origin takes longer than expected to respond. Smart Preloader monitors dynamic HTML requests and displays a customizable loading screen when your origin is slow to respond. Instead of leaving users staring at a blank page, they see your branding with a loading animation while the request completes in the background. Smart Preloader is part of the [Bunny Optimizer](/docs/optimizer) suite and requires a DNS-accelerated Pull Zone. ## How it works 1. A user requests a page from your site 2. Bunny forwards the request to your origin immediately 3. If your origin responds within the trigger delay, the response is returned normally (no preloader shown) 4. If your origin exceeds the trigger delay, Bunny returns the preloader screen while keeping the original request alive 5. The preloader automatically reconnects to the original request and displays your content when ready This approach adds near-zero overhead—the preloader only appears when your origin is already slow. ## Trigger conditions The Smart Preloader only activates when all of these conditions are met: The request must be for dynamic HTML content. Static assets (images, CSS, JS) don't trigger the preloader. Your origin must take longer than the configured trigger delay to respond. The request must come from a browser expecting an HTML response. ## Configuration ### Trigger delay Set the time threshold for when the preloader should appear. If your site typically responds in 700ms, set the trigger delay to 700ms—users only see the preloader when something is slower than usual. ### Custom HTML You can replace the default preloader with your own HTML. Include the `{{preloader_script}}` tag in your HTML body to ensure the reconnection script is injected: ```html theme={null} Loading...
Loading

Please wait...

{{preloader_script}} ``` Your custom HTML must not contain JavaScript or HTML errors that could prevent the injected reconnection script from executing. If the script fails, the request won't reconnect to your origin response. ## Technical details The preloader returns HTTP status `529` to signal browsers not to cache the temporary response. Bunny may set cookies on preloader responses to reconnect users to their original request. These are never used for tracking. If your origin doesn't respond at all, users receive a standard `502` or `504` error—not the preloader indefinitely. Works alongside Perma-Cache, SafeHop, and Origin Shield with no conflicts. ## Performance impact Smart Preloader is designed to add minimal overhead: * When origin responds in time: **Zero overhead** (preloader never shown) * When origin is slow: **\~150ms** additional load time in worst-case scenarios, primarily from the reconnection script With Bunny's average global edge latency under 26ms, the reconnection process is nearly instantaneous for most users. # Perma-Cache Source: https://bunny.net/docs/cdn/perma-cache Permanently store cached files on geo-replicated storage to eliminate repeated origin fetches. Perma-Cache is a secondary permanent cache layer that sits between the CDN and your origin. When a cache MISS occurs on the CDN, the system first checks if the file exists in your geo-replicated storage before fetching from your origin. Due to the optimized routing and global footprint of Bunny's geo-replicated storage, this provides a significant performance boost for uncached content and reduces traffic to your origin. Instead of going from the CDN to your server every time, there's a high chance the file is already available right next to the CDN nodes on globally replicated storage. ## How it works ```mermaid theme={null} flowchart LR CDN[CDN Edge] PC[[Perma-Cache]] Origin[(Origin)] CDN -->|cache MISS| PC PC -->|file found| CDN PC -.->|file not found| Origin Origin -.->|async download| PC ``` 1. When a cache MISS happens on the CDN, the request is first sent to the Perma-Cache storage 2. If the file exists in storage, it's returned to the CDN immediately. Your origin is never contacted. 3. If the file doesn't exist, the request passes through to your origin. The file is then asynchronously downloaded to Perma-Cache storage in the background The next time any CDN node requests this file as part of a cache MISS, it will already be available on the storage node. The file is effectively permanently stored in the system. Perma-Cache connects to any Edge Storage zone and uses a special folder structure. See the [Folder structure](#folder-structure) section below for details. ## Enable Perma-Cache Go to **Storage** and create a new Storage Zone. Enable geo-replication if you want files distributed across multiple regions. Go to **CDN** > **Pull Zones** and select your zone. Click **Caching**, select **Perma-Cache**, and choose your Storage Zone from the dropdown. Click **Save Configuration**. Perma-Cache will begin filling as cache MISSes occur. If you initially see a very small number of cached files, that just means the CDN is already doing a great job keeping its own cached files. Perma-Cache only fills on cache MISSes. ## Cache headers The CDN returns two headers that indicate where content was served from: ### CDN-Cache header | Value | Meaning | | ------ | ----------------------------------------------------------------- | | `HIT` | Content was served from CDN edge cache | | `MISS` | Content was not in edge cache, fetched from Perma-Cache or origin | ### Perma-Cache header | Value | Meaning | | ------ | ------------------------------------------- | | `HIT` | Content was served from Perma-Cache storage | | `MISS` | Content was fetched from origin | ### Header combinations | CDN-Cache | Perma-Cache | What happened | | --------- | ----------- | ------------------------------------------------------------------------------------------------------------- | | `MISS` | `MISS` | File not in CDN or Perma-Cache. Fetched from origin. Perma-Cache will store it in the background. | | `HIT` | `MISS` | File served from CDN edge cache. Perma-Cache status is cached, so it may show `MISS` until CDN cache expires. | | `MISS` | `HIT` | File not in CDN edge cache but loaded from Perma-Cache. | | `HIT` | `HIT` | File available in both CDN cache and Perma-Cache. | The CDN aims to cache files as long as possible, but cache duration depends on available space at each PoP and how frequently the file is requested. Files with low request rates may be evicted sooner. Use Perma-Cache to ensure content is always served from Bunny infrastructure. ## Cache purging behavior Perma-Cache integrates with the file purging API: * **Single URL purge:** The file is first deleted from Perma-Cache storage, then purged from the CDN. A fresh file is fetched from your origin on the next request. * **Full Pull Zone purge:** Perma-Cache files are not deleted. Instead, the system switches to a new directory structure within the storage zone. You'll need to manually delete the old caching folder if needed. Wildcard purging and tag-based purging do not work when Perma-Cache is enabled. ## Important notes Don't use Perma-Cache as a substitute for permanent storage. A cache MISS on the CDN does not 100% guarantee that a file will appear in Perma-Cache. Always keep files on your origin. Purging the cache will cause Perma-Cache to re-fetch from the origin. * **Origin Shield conflict:** You cannot use Perma-Cache and Origin Shield simultaneously. These features are mutually exclusive. * **Storage-backed Pull Zones:** If your Pull Zone is directly connected to a Storage Zone as its origin, Perma-Cache is not available (you're already hosting content on Bunny storage). * **Geo-replication:** If enabled on your Storage Zone, files are replicated across the global storage network for improved availability and performance. ## Folder structure Perma-Cache uses a special directory structure within your storage zone to support cache vary settings and purging. ``` /__bcdn_perma_cache__/ └── pullzone____/ └── path/to/my/file/ └── ___image.jpg___/ └── ___file___ ``` | Component | Description | | ------------------------------- | -------------------------------------------------------------------------------------- | | `__bcdn_perma_cache__` | Root folder used by all Pull Zones connected to this storage zone | | `pullzone____` | Pull Zone folder. The `unique_id` increments on full zone purge | | `path/to/my/file/` | Normalized request path (multiple slashes combined into one) | | `___image.jpg___/` | File name enclosed in `___` | | `___file___` | The actual cached file. If Vary settings are used, variations are stored as MD5 hashes | **Full path example:** ``` /storage/__bcdn_perma_cache__/pullzone__mysite__20138242/assets/images/___logo.png___/___file___ ``` When a full Pull Zone purge occurs, the `unique_id` increments and a new folder is used. Manually delete old folders to reclaim storage space. # Pricing Source: https://bunny.net/docs/cdn/pricing Choose between Standard tier for global low-latency or Volume tier for cost-effective high-bandwidth delivery. Bunny CDN offers two pricing tiers to match your delivery needs. ## Standard tier Access the full Bunny network with 119 PoPs for ultra-low latency. Ideal for website acceleration, ad delivery, and applications where every millisecond matters. | Region | Price | | ---------------------- | ---------- | | Europe & North America | \$0.01/GB | | Asia & Oceania | \$0.03/GB | | South America | \$0.045/GB | | Middle East & Africa | \$0.06/GB | ## Volume tier A smaller network of 10 strategically placed PoPs optimized for high-bandwidth delivery at the lowest cost. Perfect for video streaming, software distribution, and large file downloads. | Usage | Price | | ------------- | --------------------------------------- | | First 500 TB | \$0.005/GB | | 500 TB - 1 PB | \$0.004/GB | | 1 PB - 2 PB | \$0.003/GB | | 2 PB+ | [Contact us](https://bunny.net/contact) | ## Choosing a tier From a configuration perspective, both tiers work identically. The difference is in network coverage and pricing: | Feature | Standard | Volume | | -------- | ------------------------- | ------------------------------- | | PoPs | 119 | 10 | | Pricing | Region-based | Flat global rate | | Best for | Low latency, global reach | High bandwidth, cost efficiency | See the [Network page](https://bunny.net/network) for a full list of Standard and Volume PoP locations. ## Change a Pull Zone's pricing tier You can change a Pull Zone's tier at any time: In the [bunny.net dashboard](https://dash.bunny.net), go to **CDN** and select your Pull Zone. Click **Pricing & Routing**. Here you can select the tier, and enable or disable specific pricing regions for serving your content. Click **Confirm** to apply your changes. # Purge Cache Source: https://bunny.net/docs/cdn/purge-cache Clear cached content from your Pull Zone to serve fresh files from your origin. Purge cached files when you need visitors to see updated content immediately rather than waiting for cache expiration. You can purge the entire Pull Zone, or purge only files matching a specific tag. Purging large zones puts temporary load on your origin server while content is re-cached. Performance may decrease briefly until edge nodes repopulate. Bunny CDN does not monitor your origin for file changes. Once a file is cached, it stays cached until its `Cache-Control` lifetime expires or it's evicted to make room for more popular content. To reflect a change immediately, purge the cache or serve the file under a new query string (such as `?v=2`). ## Dashboard Go to your Pull Zone in the [dashboard](https://dash.bunny.net) and click **Purge Cache** in the top right corner. Purge cache button Leave the search tag empty to purge the entire Pull Zone. If you enter a tag, only files with a matching `CDN-Tag` header are removed. The rest of your cache stays intact. Purge cache by tag Click **Purge** to clear the cache. ## API You can also purge cache programmatically. See the [Core API reference](/docs/api-reference/core) for full details. ### Full Pull Zone purge ```bash theme={null} curl -X POST "https://api.bunny.net/pullzone/{id}/purgeCache" \ -H "AccessKey: YOUR_API_KEY" ``` ### Purge by tag Tag responses from your origin with the `CDN-Tag` header, then purge all files matching that tag: ```bash theme={null} curl -X POST "https://api.bunny.net/pullzone/{id}/purgeCache" \ -H "AccessKey: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"CacheTag": "product-123"}' ``` Useful for invalidating all assets related to a specific product, user, or category without clearing your entire cache. `CDN-Tag` header values are limited to 1024 bytes. Longer values are truncated. To purge a specific URL across all pull zones, use the Purge URL endpoint in the [Core API](/docs/api-reference/core). ## Rate limits To ensure platform stability, the Purge API is protected by rate limits. These limits are designed to keep normal purge workflows fast while preventing abuse or accidental overload. Purge requests are rate-limited using a token bucket mechanism: * Limits are applied per account * Limits are applied per purge type * Each purge item consumes 1 token * Tokens refill steadily over time If a request exceeds the available rate limit, the API returns a `429 Too Many Requests` response with guidance on when to retry. ### Purge types Purge requests are classified into one of the following types: **Exact Purge** A single URL without wildcards where the path does not end with `/`. ``` https://example.com/image.png ``` **Prefix (Wildcard) Purge** The URL contains a `*`, or the path ends with `*` or `/`. ``` https://example.com/images/* https://example.com/assets/ ``` ### Default rate limits | Purge Type | Burst Capacity | Refill Rate | Approx. Sustained Rate | | ---------- | -------------- | -------------- | ---------------------- | | Exact | 120 tokens | 5 tokens/sec | \~300 per minute | | Prefix | 20 tokens | 0.5 tokens/sec | \~30 per minute | If your workload requires higher purge throughput, please [contact support](https://dash.bunny.net/support) to discuss custom rate limits. ## Perma-Cache behavior If you're using [Perma-Cache](/docs/cdn/perma-cache): * **Full zone purge**: Clears CDN cache but Perma-Cache switches to a new directory (manually delete the old folder if needed) * **Tag-based purging**: Not supported with Perma-Cache enabled # Query String Sort Source: https://bunny.net/docs/cdn/query-string-sort Improve cache efficiency by normalizing query parameter order in cache keys. A caching proxy server treats each URL as a distinct file based on the path and query parameters. However, query parameters can often appear in different orders while producing the same output. For example, these two URLs return the exact same dynamically generated image: ```bash theme={null} image.jpg?width=300&height=200 image.jpg?height=200&width=300 ``` Without Query String Sort, both URLs are cached separately, wasting storage and reducing your cache HIT rate. ## How it works Query String Sort automatically rearranges query parameters alphabetically when constructing the cache key. This means: * Both URLs above resolve to the same cached file * The cached file can be found regardless of parameter order * You avoid duplicate cache entries for identical content ## Enable Query String Sort Go to **CDN** > **Pull Zones** and select your zone. Click **Caching** in the left menu. Check the **Query String Sort** option and save. ## When to use it Enable Query String Sort when: * Your application generates URLs with query parameters in varying orders * You use dynamic image transformations (width, height, quality, etc.) * You want to maximize cache efficiency for parameterized requests This feature only affects how the cache key is constructed. The actual URL sent to your origin remains unchanged. # Quickstart Source: https://bunny.net/docs/cdn/quickstart Create your first Pull Zone and start delivering content in minutes. A Pull Zone connects your website to bunny.net's global CDN network. When visitors request your content, bunny.net automatically fetches it from your server, caches it at edge locations worldwide, and delivers it from the location closest to each visitor. Once created, the file available at: ``` https://mywebsite.com/image.jpg ``` Will also now be available at: ```bash theme={null} https://mywebsite.b-cdn.net/image.jpg ``` In the [bunny.net dashboard](https://dash.bunny.net), select **Add Pull Zone** from the **+ Add** sidebar launcher: Add Pull Zone Enter a name for your Pull Zone. This becomes your CDN hostname — for example, naming it `mysite` creates `mysite.b-cdn.net`. The name can only contain letters and numbers. Pull Zone Name If you plan to use a custom hostname (like `cdn.mysite.com`), this default hostname won't be visible to your users. Choose where bunny.net fetches your original content from: Enter your website URL (e.g., `https://mywebsite.com`). The CDN fetches content directly from your existing server when a file is first requested. Pull Zone Origin Host header (optional): The host HTTP header sent to the origin. If left empty, the hostname is automatically extracted from your Origin URL. Only change this if your origin expects a different hostname, such as when using an IP address as the origin. Select an existing Bunny Storage zone from the dropdown. Use this option if you're hosting files in Bunny Storage and want to deliver them through the CDN. Storage Zone Dropdown No host header configuration is needed when using a Storage Zone. Select a pricing tier based on your use case: Choose pricing tier | Tier | Best For | | --------------- | ------------------------------------------------------------------------------------------------------------------------------ | | **Standard** | Small files and websites. Exceptional global latency for high performance solutions like website acceleration and ad delivery. | | **High Volume** | Large files and video. Cost-optimized for global high bandwidth solutions using a smaller set of high performance PoPs. | Choose the geographic regions where you want your content served from: Pricing zones Disabling a region doesn't block visitors from that area — their requests are automatically routed to the nearest enabled region. Click **Add Pull Zone** to finish. You'll see setup instructions and integration options for platforms like WordPress. Use your own domain instead of b-cdn.net. Connect with WordPress, CMS platforms, and cloud storage. Customize request handling at the edge. Set up hotlink protection and token authentication. # Global Network Regions Source: https://bunny.net/docs/cdn/regions Bunny CDN spans 6 continents with 119+ points of presence across 77+ countries. Bunny CDN operates a global edge network designed to deliver content with minimal latency from locations closest to your users. The network continuously expands to provide better coverage and performance worldwide. ## Network Coverage The Bunny CDN network spans: * **119+ Points of Presence (PoPs)** * **77+ Countries** * **6 Continents** ## Regional Distribution The network is distributed across all major continents: | Region | Points of Presence | | ------------- | ------------------ | | Africa | 7 | | Asia | 24 | | Europe | 34 | | North America | 20 | | Oceania | 6 | | South America | 14 | ## Performance The global network delivers exceptional performance with an average global latency of **25ms**. In high-density regions like Europe, latency drops to sub-22ms, ensuring fast content delivery to users worldwide. ## Continuous Expansion The Bunny CDN network is continuously growing. New points of presence are regularly added to improve coverage and reduce latency in emerging markets and high-demand regions. When you create a Pull Zone, traffic automatically routes to the nearest edge location based on the user's geographic location, ensuring optimal performance without additional configuration. ## Related Resources * [Origin Shield](/docs/cdn/performance/origin-shield) - Protect your origin with a single shield location * [Routing Filters](/docs/cdn/performance/routing-filters) - Control which regions serve your content * [Performance Overview](/docs/cdn/performance) - Optimize your CDN configuration # Request Coalescing Source: https://bunny.net/docs/cdn/request-coalescing Combine multiple simultaneous requests to the same resource into a single origin request. Request Coalescing combines multiple simultaneous requests for the same uncached resource into a single request to your origin. Instead of forwarding every request individually, Bunny ties subsequent requests to the initial one and streams response bytes simultaneously to all waiting clients as soon as they arrive. ## How it works 1. Multiple users request the same uncached file at the same time 2. Bunny sends a single request to your origin 3. As the response streams back, it's simultaneously delivered to all waiting clients 4. The file is cached for future requests Request coalescing
comparison This happens in real-time with near-zero added latency—waiting requests receive data as it arrives, not after the full response is cached. ## When to use it Request Coalescing is ideal for: * **Live streaming**: Thousands of viewers requesting the same video segments simultaneously * **High-traffic public APIs**: Cacheable responses served to many users at once * **Traffic spikes**: Sudden bursts of requests for the same resources Request Coalescing reduces origin load and can improve cache hit rates during high-concurrency scenarios. ## Enabling Request Coalescing 1. Go to your Pull Zone in the [dashboard](https://dash.bunny.net) 2. Navigate to **Caching** 3. Enable **Request Coalescing** ## Per-request control with Edge Rules To override the zone-level setting for specific paths or request conditions, create an Edge Rule with the **Enable Request Coalescing** or **Disable Request Coalescing** action 1. Go to **Edge Rules** in your Pull Zone settings and click **Add Rule** 2. Select **Enable Request Coalescing** or **Disable Request Coalescing** 3. Add conditions to match the requests you want to target, such as a URL path pattern, file extension, or request header A common pattern is to leave Request Coalescing off at the zone level and enable it only on paths serving publicly cacheable content, keeping authenticated routes safely excluded. ## Important limitations **Do not use Request Coalescing with user-specific dynamic content.** If your origin returns different responses based on authentication or user context, enabling this feature could cause personal information to be shared between users making simultaneous requests. Request Coalescing triggers on any uncached request—both static and dynamic resources. Only enable it for Pull Zones serving publicly cacheable content. ### Not a guarantee of single requests Request Coalescing does not guarantee only one request ever reaches your origin. It only combines requests that arrive **simultaneously** for the same resource. Your origin may still receive multiple requests if: * Requests arrive sequentially rather than at the same time * Requests come from different CDN PoPs (coalescing runs independently on each edge node) # How to Seamlessly Migrate Your Domain to bunny.net Source: https://bunny.net/docs/cdn/seamless-migration Issue SSL certificates via DNS or HTTP validation before pointing your domain to bunny.net, enabling seamless zero-downtime migration. This feature allows you to issue an SSL certificate for your hostname **before pointing your domain to bunny.net**, avoiding HTTPS disruption during migration. You can verify domain control using either **DNS (TXT record) validation** or **HTTP (file) validation**, pick whichever you can perform on your current setup. In both cases issuance automatically transitions to **HTTP validation for renewals** once traffic is switched to bunny.net. In the [bunny.net dashboard](https://dash.bunny.net), select **Add Pull Zone** and configure your origin. Add Pull Zone Open your Pull Zone and add your custom hostname (e.g., `cdn.example.com`). Add Hostname The hostname must be fully configured on the Pull Zone before requesting a certificate. Prove you control the hostname using **one** of the methods below. Choose **DNS** if you can edit your domain's DNS records, or **HTTP** if you can place a file on the server your domain currently points to. HTTP validation does not support wildcard hostnames, and only one verification (DNS or HTTP) can be pending per hostname at a time. Initiate certificate issuance using DNS validation: ```bash theme={null} curl --request POST \ --url https://api.bunny.net/pullzone/requestExternalDnsCertificate \ --header 'AccessKey: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data ' { "Hostname": "cdn.example.com" } ' ``` This request returns the DNS TXT record required for domain verification. View full endpoint documentation Add the returned TXT record to your domain’s DNS zone. Example: ```bash theme={null} _acme-challenge.example.com TXT "verification-token" ``` Wait until the record is publicly resolvable before continuing. Finalize the process once the TXT record is live: ```bash theme={null} curl --request POST \ --url https://api.bunny.net/pullzone/completeExternalDnsCertificate \ --header 'AccessKey: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data ' { "Hostname": "cdn.example.com" } ' ``` This validates the DNS record and issues the certificate. View full endpoint documentation Initiate certificate issuance using HTTP validation: ```bash theme={null} curl --request POST \ --url https://api.bunny.net/pullzone/requestExternalHttpCertificate \ --header 'AccessKey: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data ' { "Hostname": "cdn.example.com" } ' ``` This request returns the challenge file's path and its contents (`FilePath` and `FileContent`). View full endpoint documentation On the server your domain **currently** points to, serve the returned `FileContent` at the returned `FilePath` so it is reachable over plain HTTP (port 80): ```text theme={null} http://cdn.example.com/.well-known/acme-challenge/ ``` The response body must exactly match the `FileContent` value from the previous step. Redirects to HTTPS are followed during validation. Finalize the process once the file is reachable: ```bash theme={null} curl --request POST \ --url https://api.bunny.net/pullzone/completeExternalHttpCertificate \ --header 'AccessKey: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data ' { "Hostname": "cdn.example.com" } ' ``` This fetches the challenge file and issues the certificate. View full endpoint documentation After the certificate is issued, update your domain’s DNS records to point to your Pull Zone hostname (e.g., `yourzone.b-cdn.net`). * Use a **CNAME record** for subdomains (e.g., `cdn.example.com`) * Use an **ALIAS/ANAME record** if configuring an apex/root domain HTTPS will be available immediately after traffic is switched. After the initial issuance: * Certificates automatically switch to **HTTP-01 validation** * Renewals happen automatically * No further DNS changes are required * For **DNS** validation, the TXT record must be publicly resolvable before completing the request; propagation time depends on your DNS provider * For **HTTP** validation, the challenge file must be reachable over plain HTTP (port 80) on the server your domain currently points to, and wildcard hostnames are not supported * Only one verification (DNS or HTTP) can be pending per hostname at a time; starting a new request replaces any previous pending one * Certificate issuance depends on successful validation by the certificate authority # Geographic Blocking Source: https://bunny.net/docs/cdn/security/geographic-blocking Block access to your Pull Zone from specific countries using bunny.net's Traffic Manager. You can block access to a Pull Zone from specific countries using the **Traffic Manager**, for legal, regulatory, or other reasons. ## Block countries In the [bunny.net dashboard](https://dash.bunny.net), go to **Delivery > CDN** and select your Pull Zone. Click **Traffic Manager** in the Pull Zone menu. You'll see a world map with two options: **Redirected countries** and **Blocked countries**. Add countries to **Blocked countries** by clicking them on the map or using the dropdown, then save your changes. ## How blocking works Blocked countries are blocked at the DNS level. A DNS request to the Pull Zone from a blocked country resolves to `127.0.0.1`, so the connection never reaches the CDN and the user sees a connection error. Because the request is blocked before HTTP, it isn't logged on the Pull Zone and incurs no cost. Location is determined by geolocating the end user's EDNS client subnet using the MaxMind GeoIP2 database. If the resolver doesn't provide an EDNS client subnet, the resolver's own IP address is used instead. **Redirected countries** is a related Traffic Manager option. Instead of blocking, it routes requests to the most affordable pricing region (North America / Europe), which you can use to control which regions serve which countries. IP-based geographic blocking isn't foolproof. Users can bypass it with a VPN or proxy. For stricter control, combine it with [Token Authentication](/docs/cdn/security/token-authentication) country restrictions. # Hotlink Protection Source: https://bunny.net/docs/cdn/security/hotlink-protection Prevent other websites from embedding your content and increasing your bandwidth costs. Hotlink protection stops external websites from directly embedding your images, videos, or other assets. Requests from domains not on your allowed list receive a `403 Forbidden` response. ## Setup Go to your Pull Zone in the [dashboard](https://dash.bunny.net) and click **Security** in the side menu. Enter a domain (e.g., `www.example.com`) and click **Add Allowed Referrer**. Repeat for each domain that should have access. Allowed referrers Use wildcards for subdomains: `*.example.com` allows all subdomains, but doesn't include the root domain. Add `example.com` separately if needed. ## Block direct URL access Once you have allowed referrers configured, an additional option appears: **Block Direct URL File Access**. When enabled, requests with an empty referrer header (e.g., someone typing the URL directly into their browser) are also blocked. Be careful with this option. Empty referrers can come from legitimate sources like email clients, some mobile apps, or privacy-focused browsers. ## Common allowed referrers If you're using hotlink protection but want social media previews to work (for `og:image` tags), add referrers for the platforms you use: * `*.facebook.com` * `*.twitter.com` * `*.linkedin.com` * `*.pinterest.com` # JA4 Fingerprinting Source: https://bunny.net/docs/cdn/security/ja4-fingerprinting Identify clients using TLS fingerprinting and access the JA4 fingerprint via request headers. JA4 fingerprinting identifies clients based on characteristics of their TLS handshake. Unlike IP addresses or User-Agent strings, which can be easily changed or spoofed, TLS fingerprints reflect how a client actually implements the TLS protocol. This makes JA4 a powerful signal for identifying automated traffic, detecting bot frameworks, and improving security decisions across the CDN. bunny.net automatically computes a JA4 fingerprint for incoming HTTPS requests and exposes it to your origin server via the `CDN-JA4` request header. The same signal is also used internally to strengthen DDoS mitigation, bot detection, and other security protections. ## What is JA4? JA4 is a modern TLS fingerprinting method that improves on earlier techniques such as **JA3**. While JA3 fingerprints TLS clients based on the raw TLS handshake values, JA4 introduces normalization and better handling of modern TLS features to produce more consistent and reliable fingerprints. The fingerprint is derived from the **ClientHello** message sent during the TLS handshake. This message advertises the cryptographic capabilities of the client and varies depending on the TLS library, operating system, and application making the connection. Key characteristics used in the fingerprint include: * TLS protocol version * Cipher suites * TLS extensions * Supported elliptic curves * Signature algorithms * Application Layer Protocol Negotiation (ALPN) Because these values are determined by the client's TLS stack, they tend to remain stable for a given browser, operating system, or automation framework. ## How a JA4 fingerprint is formed During the TLS handshake, the client sends a **ClientHello** message containing a structured list of supported cryptographic features. JA4 processes and normalizes this data to generate a compact fingerprint string that represents the TLS client implementation. The fingerprint incorporates multiple normalized components of the handshake, including: * TLS version * Cipher suite ordering * TLS extension set * Supported elliptic curves * Signature algorithm preferences * ALPN protocol negotiation These components are normalized and hashed into a deterministic fingerprint. Example JA4 fingerprint: ``` t13d1516h2_8daaf6152771_02713d6af862 ``` Clients using the same TLS stack and configuration will typically produce the same JA4 fingerprint across connections. ## Accessing the JA4 fingerprint bunny.net forwards the computed JA4 fingerprint to your origin server via the following request header: ``` CDN-JA4 ``` Example request header: ``` CDN-JA4: t13d1516h2_8daaf6152771_02713d6af862 ``` You can use this value at your origin to: * Identify automated traffic * Detect suspicious clients * Correlate requests across sessions * Implement custom security or rate-limiting logic ## Security and DDoS mitigation JA4 fingerprints are also used internally by bunny.net as part of our security infrastructure. Because TLS fingerprints are significantly harder to spoof than IP addresses or User-Agent headers, they provide an additional signal for identifying malicious clients and coordinated bot activity. This signal contributes to multiple protection mechanisms, including: * DDoS mitigation * Bot detection * Abuse prevention * Traffic anomaly detection By combining JA4 fingerprints with other network and behavioral signals, bunny.net can more accurately detect malicious traffic while minimizing the impact on legitimate users. ## Best practices When using JA4 fingerprints in your own systems: * Treat JA4 as **one signal among many**, not a unique identifier. * Combine it with IP reputation, request patterns, and behavioral analysis. * Monitor for unusual spikes or changes in fingerprint distribution. JA4 fingerprints are most effective when used as part of a broader traffic analysis and security strategy. # Advanced Token Authentication Source: https://bunny.net/docs/cdn/security/token-authentication/advanced Generate secure URLs with HMAC-SHA256 tokens, geo-restrictions, directory access, speed limits, and IP locking. Advanced token authentication uses HMAC-SHA256 signing and supports directory-level tokens for video streaming, country-based restrictions, IP locking, speed limits, and query parameter control. ## URL structure Signed URLs use either a [query string or path-based format](#query-string-vs-path-based-tokens): ``` https://myzone.b-cdn.net/videos/playlist.m3u8?token=HS256-abc123&expires=1598024587&token_path=%2Fvideos%2F ``` ``` https://myzone.b-cdn.net/bcdn_token=HS256-abc123&expires=1598024587&token_path=%2Fvideos%2F/videos/playlist.m3u8 ``` ## Parameters | Parameter | Required | Description | | ------------------------- | -------- | ---------------------------------------------------------------- | | `token` | Yes | Hashed signature | | `expires` | Yes | UNIX timestamp in seconds when the URL becomes invalid | | `token_path` | No | URL-encoded path prefix for directory-level access | | `token_countries` | No | Comma-separated allowed country codes (ISO 3166-1) | | `token_countries_blocked` | No | Comma-separated blocked country codes (ISO 3166-1) | | `token_ignore_params` | No | When `true`, query parameters are excluded from token validation | | `limit` | No | Download speed limit in kB/s | ## Directory tokens By default, tokens are valid only for the exact URL path. Directory tokens allow access to any file within a path prefix, essential for video streaming where players request multiple segment files. Signing with `token_path=/videos/stream1/` allows access to all files in that directory: ``` /videos/stream1/playlist.m3u8 /videos/stream1/segment1.ts /videos/stream1/segment2.ts ``` ## IP locking IP locking binds a token to a specific client IP address. Any request from a different IP will be rejected, even if the token is otherwise valid. This is useful for preventing token sharing or URL redistribution. To use IP locking, pass the client's address as the `userIp` parameter when signing the URL. The address is included in the HMAC signature but is not appended to the URL itself. Both IPv4 and IPv6 addresses are supported, and each family binds at a different prefix length: | Family | Binds at | Behavior | | ------ | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | IPv4 | `/32` or `/24` | Signing a full address (`203.0.113.42`) binds that exact address. Signing the network address of a `/24` (`203.0.113.0`) accepts any client within that `/24`. | | IPv6 | `/64` | Only the `/64` prefix is bound, so any address within it is accepted. | IPv6 always binds at `/64` rather than the full address, because most clients change the rest of their address regularly for privacy. Binding the full address would stop validating as soon as it changed, often within hours, while a client's `/64` stays stable. For IPv4, you can widen the bind by signing the network address of a `/24` (for example `203.0.113.0` instead of `203.0.113.42`). Tokens signed that way validate for any client within that `/24`, which is useful for clients whose address shifts within a subnet. IP locking requires the **Token IP Validation** setting to be enabled on your Pull Zone. Enabling this without including the IP in your tokens will cause all requests to fail. ### Address family matching The signed address must be in the same family the request arrives over. A token signed with an IPv4 address cannot validate a request that arrives over IPv6, nor the reverse. Enabling **Token IP Validation** therefore pins the Pull Zone to a single family: the zone serves IPv4 only, unless its [IP Family Policy](/docs/cdn/ip-family-policy#token-authentication-and-ip-locking) is set to **IPv6 Only**, which is respected and makes the zone serve IPv6 only. Sign whichever family the zone serves. IP locking can cause issues for users behind proxies, VPNs, or with frequently changing IP addresses (e.g. mobile networks). ## Expiration By default, the token expiry is calculated as a relative offset from the current time using `expirationTime` (in seconds). If you need the URL to expire at a specific point in time, use `expiresAt` to set an absolute UTC timestamp instead. When `expiresAt` is set, `expirationTime` is ignored. ## Ignore query parameters By default, all query string parameters present on the URL are included in the token signature. If a parameter is added, removed, or changed after signing, the token will fail validation. Set `ignoreParams` to `true` when you need to append arbitrary query parameters to signed URLs after generation, for example, analytics tags, cache-busting parameters, or player configuration. When enabled, the `token_ignore_params=true` parameter is included in the signature instead of the actual query parameters. ## Speed limits The `limit` parameter restricts the maximum download speed for the request in kB/s. Pass the desired speed as the `speedLimit` parameter when signing. A value of `0` means no limit. ``` https://myzone.b-cdn.net/video.mp4?token=HS256-abc123&limit=500&expires=1598024587 ``` ## Country restrictions Use `token_countries` to allow access only from specific countries, or `token_countries_blocked` to allow all except specific countries (only one should be necessary per token). Country codes follow the [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) format (e.g. `US`, `GB`, `DE`). ## Generate the token ``` token = "HS256-" + flags + Base64URL(HMAC-SHA256(security_key, signature_path + expires + user_ip + signing_data)) ``` Where: * **`security_key`** is used as the HMAC key (not included in the message) * **`signature_path`** is the URL path, or the `token_path` override if set * **`user_ip`** is the optional IP address, omitted entirely when not set. IPv6 addresses are masked to their `/64` prefix before signing * **`signing_data`** is the alphabetically-sorted parameters joined as `key=value` pairs separated by `&`, excluding `token` and `expires` * **`flags`** is `1-` when an IP was signed, and empty otherwise, producing tokens of the form `HS256-1-` After Base64 encoding, replace `+` with `-`, `/` with `_`, and remove `=` padding characters. ## Code examples Tested implementations are available for C#, Python, Node.js, PHP, Java, Go, and Rust in the [BunnyCDN.TokenAuthentication](https://github.com/BunnyWay/BunnyCDN.TokenAuthentication) repository. ```csharp C# theme={null} var url = TokenSigner.SignUrl(t => { t.Url = "https://myzone.b-cdn.net/videos/stream1/playlist.m3u8"; t.SecurityKey = "your-security-key"; t.ExpiresAt = DateTimeOffset.UtcNow.AddHours(1); t.IsDirectory = true; t.TokenPath = "/videos/stream1/"; t.CountriesAllowed = new List { "GB" }; }); ``` ```python Python theme={null} from token import sign_url url = sign_url( "https://myzone.b-cdn.net/videos/stream1/playlist.m3u8", "your-security-key", expiration_time=3600, is_directory=True, path_allowed="/videos/stream1/", countries_allowed="GB", ) ``` ```javascript Node.js theme={null} const { signUrl } = require('./token'); const url = signUrl( 'https://myzone.b-cdn.net/videos/stream1/playlist.m3u8', 'your-security-key', 3600, // expirationTime '', // userIp true, // isDirectory '/videos/stream1/', // pathAllowed 'GB', // countriesAllowed ); ``` ```php PHP theme={null} require_once 'url_signing.php'; $url = sign_bcdn_url( 'https://myzone.b-cdn.net/videos/stream1/playlist.m3u8', 'your-security-key', 3600, // expiration_time '', // user_ip true, // is_directory '/videos/stream1/', // path_allowed 'GB', // countries_allowed ); ``` ```java Java theme={null} import BunnyCDN.TokenSigner; String url = TokenSigner.signUrl( "https://myzone.b-cdn.net/videos/stream1/playlist.m3u8", "your-security-key", 3600, // expirationTime "", // userIp true, // isDirectory "/videos/stream1/", // pathAllowed "GB", // countriesAllowed "" // countriesBlocked ); ``` ```go Go theme={null} import bunnycdn "bunnycdn-token-authentication" url, err := bunnycdn.SignUrl( "https://myzone.b-cdn.net/videos/stream1/playlist.m3u8", "your-security-key", 3600, // expirationTime "", // userIp true, // isDirectory "/videos/stream1/", // pathAllowed "GB", // countriesAllowed "", // countriesBlocked false, // ignoreParams nil, // expiresAt 0, // speedLimit ) ``` ```rust Rust theme={null} use bunnycdn_token_authentication::sign_url; let url = sign_url( "https://myzone.b-cdn.net/videos/stream1/playlist.m3u8", "your-security-key", 3600, // expiration_time "", // user_ip true, // is_directory "/videos/stream1/", // path_allowed "GB", // countries_allowed "", // countries_blocked false, // ignore_params None, // expires_at 0, // speed_limit )?; ``` ## Query string vs path-based tokens The `isDirectory` parameter controls how the token is embedded in the URL: * **`false` (query string)** - the token and parameters are appended as query string parameters. Suitable for direct file downloads and simple URL signing. * **`true` (path-based)** - the token is embedded in the URL path as `/bcdn_token=...`. This is required for HLS/DASH video delivery, where the player resolves relative segment URLs against the manifest path. Placing the token in the path ensures segment requests automatically inherit authentication without modifying the player. # Basic Token Authentication Source: https://bunny.net/docs/cdn/security/token-authentication/basic Generate expiring URLs using MD5-based token authentication. Basic token authentication uses MD5, which is cryptographically insecure. This method is deprecated and may be removed in a future release. We strongly recommend you use [Advanced Token Authentication](/docs/cdn/security/token-authentication/advanced) with HMAC-SHA256 for new implementations. Basic token authentication uses MD5 hashing to create signed URLs with expiration times and optional IP validation. ## URL structure ``` https://myzone.b-cdn.net/video.mp4?token=m0EMEkV3pNAKFB33gZuv_Q&expires=1456761770 ``` | Parameter | Description | | --------- | ------------------------------------------------------------------------------------------------------- | | `token` | Base64-encoded MD5 hash of the security key, path, and expiration | | `expires` | UNIX timestamp in seconds when the URL becomes invalid (milliseconds and nanoseconds are not supported) | ## Generate the token ``` token = Base64(MD5(security_key + path + expiration)) ``` To validate against a specific IP address: ``` token = Base64(MD5(security_key + path + expiration + ip_address)) ``` After Base64 encoding, replace `+` with `-`, `/` with `_`, and remove `=` characters. ## Code examples ```php PHP theme={null} $securityKey = 'your_security_key'; $path = '/path/to/file.mp4'; $expires = time() + 3600; $hashableBase = $securityKey . $path . $expires; // Optional: $hashableBase .= '192.168.1.1'; $token = md5($hashableBase, true); $token = base64_encode($token); $token = strtr($token, '+/', '-_'); $token = str_replace('=', '', $token); $url = "https://myzone.b-cdn.net{$path}?token={$token}&expires={$expires}"; ``` ```csharp C# theme={null} var securityKey = "your_security_key"; var path = "/path/to/file.mp4"; var expires = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 3600; var hashableBase = securityKey + path + expires; // Optional: hashableBase += "192.168.1.1"; using var md5 = System.Security.Cryptography.MD5.Create(); var hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(hashableBase)); var token = Convert.ToBase64String(hashBytes); token = token.Replace("+", "-").Replace("/", "_").Replace("=", ""); var url = $"https://myzone.b-cdn.net{path}?token={token}&expires={expires}"; ``` ```javascript JavaScript theme={null} const crypto = require("crypto"); const securityKey = "your_security_key"; const path = "/path/to/file.mp4"; const expires = Math.round(Date.now() / 1000) + 3600; let hashableBase = securityKey + path + expires; // Optional: hashableBase += '192.168.1.1'; const md5Hash = crypto.createHash("md5").update(hashableBase).digest(); let token = md5Hash.toString("base64"); token = token.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); const url = `https://myzone.b-cdn.net${path}?token=${token}&expires=${expires}`; ``` ```python Python theme={null} import hashlib from base64 import b64encode from time import time security_key = 'your_security_key' path = '/path/to/file.mp4' expires = int(time()) + 3600 hashable = f"{security_key}{path}{expires}" # Optional: hashable += '192.168.1.1' md5_hash = hashlib.md5(hashable.encode()).digest() token = b64encode(md5_hash).decode() token = token.replace('+', '-').replace('/', '_').replace('=', '') url = f"https://myzone.b-cdn.net{path}?token={token}&expires={expires}" ``` ## IP validation IP validation binds the token to a specific IP address. Append the user's IP to the hash before generating the token. The IP is not included in the URL. IP validation can cause issues for users behind proxies or with changing IP addresses. # Token Authentication Source: https://bunny.net/docs/cdn/security/token-authentication/index Protect your content with signed URLs that expire and can be restricted by location or IP. Token authentication blocks requests to your Pull Zone unless a valid signed token is included. Use it to protect premium content, create expiring download links, or restrict access by country or IP. ## Choose a method | Method | Hash | Features | | ------------------------------------------------------- | ------ | ----------------------------------------------------------------------- | | [Basic](/docs/cdn/security/token-authentication/basic) | MD5 | Expiry, optional IP validation | | [Advanced](/docs/cdn/security/token-authentication/advanced) | SHA256 | Expiry, IP validation, geo-restrictions, directory tokens, speed limits | Both methods can be used on the same Pull Zone. Start with Basic for simple expiring URLs. Use Advanced if you need geo-restrictions, directory-level access for video streaming, or speed limits. ## Enable token authentication Go to **CDN**, select your Pull Zone, then click **Security**. Toggle **Token Authentication** on and copy the **URL Token Authentication Key**. Enable token authentication Keep your security key secret. Anyone with access to this key can generate valid tokens. ## How it works Your application signs each URL by creating a hash from your security key, the URL path, and an expiration time: ``` https://myzone.b-cdn.net/video.mp4?token=abc123&expires=1598024587 ``` Bunny recalculates the hash on each request. If it matches and the expiration hasn't passed, the request succeeds. IPv6 is automatically disabled when token authentication is enabled. # Smart Cache Source: https://bunny.net/docs/cdn/smart-cache Prevent accidental caching of sensitive content by limiting caching to known static file types. By default, Bunny caches all responses from your origin that include cacheable headers like `Cache-Control` or `Expires`. If your server is misconfigured, this could result in sensitive or personalized content being cached and served to other users. Smart Cache prevents this by only caching responses with specific file extensions and MIME types. Dynamic content passes through to your origin on every request. Pull Zones accelerated by Bunny DNS have Smart Cache enabled by default. ## Enable Smart Cache Go to **CDN** > **Pull Zones** and select your zone. Click **Caching** in the left menu. Check the **Smart Cache** option at the top of the page. When Smart Cache determines a request is cacheable, the standard cache expiration time settings apply. ## Cacheable extensions Smart Cache only caches files with these extensions: | Images | Video | Audio | | --------------- | -------------- | -------------- | | avif, bmp, gif | 3g2, 3gp, asf | aif, flac, mid | | heic, ico, jpg | avi, flv, m3u8 | midi, mp3, mpa | | jpeg, pict, png | m4u, mkv, mp4 | ogg, wav, wma | | svg, svg2, tif | mpg, swf, ts | | | tiff, webp | vob, webm, yuv | | | Fonts | Documents | Archives | | ------------- | ------------------- | ----------------- | | eot, otf, ttf | csv, doc, docx | 7z, bz2, gz | | woff, woff2 | odt, pdf, ppt | iso, jar, rar | | | pptx, ps, psd | tar, xz, zip, zst | | | srt, txt, xls, xlsx | | | Code & Binaries | Other | | ----------------- | ------------- | | bat, class, css | apk, bin, dat | | dll, ejs, exe, js | dmg, eps, pls | ## MIME types excluded from caching These MIME types are never cached, regardless of extension: | MIME Type | | ------------------ | | `text/html` | | `application/json` | | `application/xml` | ## Override Smart Cache To cache a file type that Smart Cache normally excludes, create an Edge Rule with the **Override Cache Time** action. This bypasses Smart Cache's decision and caches the response for your specified duration. Go to **Edge Rules** in your Pull Zone settings and click **Add Rule**. Select **Override Cache Time** and enter the cache duration in seconds. Add conditions to match the requests you want to cache, such as file extension or URL path. This is useful for caching HTML pages or API responses that you know are safe to cache, such as static site generators or public API endpoints. ## Improving your cache hit rate A healthy Pull Zone typically sees a cache hit rate above 95%. A rate below 70% usually points to a configuration issue causing requests to be fetched from your origin instead of served from cache. Common causes and fixes: * **Missing or restrictive `Cache-Control` headers:** Bunny follows the origin's `Cache-Control` header to decide whether and how long to cache. Check it with `curl -I https://your-zone.b-cdn.net/path/file.css`. If it's missing, set to `no-cache`, or has a very low `max-age`, the file won't cache well. Set an appropriate `max-age`, or override caching with an Edge Rule (see [Override Smart Cache](#override-smart-cache)). * **Changing query strings:** Each unique URL, including its query string, is cached separately, so `/style.css?v=1` and `/style.css?v=2` are two cache entries. If the query string changes but the content doesn't, disable **URL Query String** under [Vary Cache](/docs/cdn/vary-cache#url-query-string) so all variants share one cache file. Review your workflow first, since this affects every query-string variant. * **New or recently purged zone:** Cache hit rate builds up over time. A brand-new or just-purged zone shows a low rate until traffic warms the cache, usually within a day. * **Dynamic content via Smart Cache:** With Smart Cache enabled, dynamic content is treated as non-cacheable, which lowers the reported rate on zones that serve many dynamic assets. * **Infrequently requested files:** Files that aren't requested for a period (typically 5 to 7 days) may be evicted from cache. The more popular a file, the higher its hit rate. * **Mostly static content:** Consider [Perma-Cache](/docs/cdn/perma-cache) to permanently store files on edge storage. Note that Perma-Cache hits are still reported as MISS in CDN cache metrics. * **Simultaneous mass requests:** For livestreaming or live events where many users request the same uncached asset at once, enable [Request Coalescing](/docs/cdn/request-coalescing) to funnel them into a single origin request. ## Video and large file delivery Bunny CDN supports HTTP range requests, which let players fetch specific byte ranges so viewers can skip ahead in a video. Range requests are enabled by default for cached content. For content that isn't cached yet, or videos that buffer when you skip ahead or large files that are only seekable once fully downloaded, two settings help: * **Enable Optimize for Video Delivery:** In your Pull Zone **Caching** settings, enable **Optimize for Video Delivery**. This slices large files into small chunks that are fetched independently, so any part of a file, including range requests against uncached content, can be served at any time regardless of cache state. Without it, an uncached file is downloaded from the origin as one large object and is only seekable from the start until fully cached. * **Encode video with the header at the start:** If seeking still fails even when the file is cached, your video likely has its metadata (the moov atom) at the end of the file, forcing the browser to download the whole file before it can play. Re-encode with a web-optimized (fast-start) preset so the header sits at the beginning. RTMP streaming isn't supported on the CDN. For live and on-demand video streaming, use [Bunny Stream](/docs/stream). ## ETag support Bunny CDN supports the [`ETag`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) response header, which identifies a specific version of a file so browsers and the CDN can tell whether a cached copy is still valid. * If your origin sends an `ETag` and **Optimize for Video Delivery** is disabled, Bunny passes the `ETag` through and caches the response accordingly. * If Bunny compresses the file (Brotli or Gzip), the `ETag` is converted to a weak `ETag` prefixed with `W/` (for example, `"123456789"` becomes `W/"123456789"`), since compression changes the file's binary output. # SSL for Custom Domains Source: https://bunny.net/docs/cdn/ssl-setup Enable free Let's Encrypt SSL or upload your own certificate for custom hostnames. Bunny provides free auto-renewing SSL certificates via Let's Encrypt, or you can upload your own certificate from a commercial provider. ## Prerequisites Your custom domain must point to Bunny using a CNAME record before SSL validation can succeed. See [Custom Hostname](/docs/cdn/custom-hostname) for setup instructions. On Cloudflare, disable the proxy option (orange cloud icon). Proxying hides DNS resolution and prevents SSL validation from working. To issue a certificate *before* repointing your domain and avoid HTTPS downtime during migration, use [Seamless Domain Migration](/docs/cdn/seamless-migration) instead, which validates ownership over DNS first. ## Free Let's Encrypt certificate Open your Pull Zone in the [dashboard](https://dash.bunny.net), navigate to **General** > **Hostnames**, and add your custom hostname if you haven't already. Find your hostname in the **Linked Hostnames** section and click **Enable**. Select hostname and enable SSL Choose **Add Free Let's Encrypt Certificate** and click **Continue**. Enable HTTPS with Let's Encrypt Bunny issues and installs the certificate automatically. Renewal is handled for you. Verify your CNAME record is correctly configured and click **Continue** to complete validation. CNAME configuration Visit your domain using `https://` and confirm the certificate is valid. You can also use [SSL Labs](https://www.ssllabs.com/ssltest/) to test. ## Wildcard certificates When your domain is hosted on a [Bunny DNS](/docs/dns) zone, Bunny automatically issues and renews a free Let's Encrypt wildcard certificate (`*.yourdomain.com`). Just follow the [Free Let's Encrypt certificate](#free-lets-encrypt-certificate) steps above with your wildcard hostname. If your domain is hosted elsewhere, move it to Bunny DNS ([DNS quickstart](/docs/dns/quickstart)), or generate the wildcard certificate yourself (for example, with certbot using DNS validation) and [upload it manually](#custom-certificate). ## ACME passthrough to origin If your origin requests its own certificates over ACME, Bunny forwards requests to `/.well-known/acme-challenge/` through to the origin so it can complete validation and have a certificate issued directly to it. ## Custom certificate Use this option for certificates from commercial providers, or for wildcard certificates when your domain is not managed by Bunny DNS. Bunny requires Nginx-compatible format. Combine your certificate chain into a single file by placing your domain certificate at the top, followed by intermediate certificates in order. Save as a single `.pem` file (e.g., `fullchain.pem`). You'll also need your private key file. Open your Pull Zone, go to **Hostnames**, find your hostname, and click **Enable**. Choose **Upload your own certificate** and click **Continue**. Enable custom HTTPS Paste your certificate chain and private key into the respective fields, then click **Upload**. Upload custom SSL certificate Wait for the certificate to propagate across the network. ## Troubleshooting ### SSL validation fails Common causes: * **DNS not propagated**: Use [dnschecker.org](https://dnschecker.org) to confirm your CNAME is resolving globally. After updating a DNS record, wait a few minutes before requesting the certificate so the change can propagate, otherwise our servers may read a stale cached record * **Incorrect CNAME record**: Verify with a [DNS lookup tool](https://toolbox.googleapps.com/apps/dig/) that your hostname returns a CNAME pointing to the exact hostname of your Pull Zone. If you use [Bunny DNS](/docs/dns), point your custom domain's CNAME to your `b-cdn.net` Pull Zone hostname * **Cloudflare proxy enabled**: Disable the orange cloud icon on your CNAME record * **Geolocation blocks**: Let's Encrypt validates from multiple regions (including USA and Europe). If you've blocked these regions via Traffic Manager or Edge Rules, validation may fail * **CAA records**: If your domain has CAA DNS records, add `letsencrypt.org` to the allowed issuers ### Rate limiting Requesting certificates too many times in a short period can trigger Let's Encrypt rate limits (up to one week). Be patient when troubleshooting DNS issues before retrying. If you need SSL immediately while rate-limited, upload a [custom certificate](#custom-certificate) from another CA. ### Debugging a failed request When a certificate request fails via the dashboard or API, the returned error includes an ACME challenge URL (like `https://acme-v02.api.letsencrypt.org/acme/chall/...`). Open it to see the exact reason the issuance failed. If you're still stuck, contact [support@bunny.net](mailto:support@bunny.net). ## Root domains CNAME records aren't allowed at the apex level (`yourdomain.com`) by most DNS providers. You have two options: 1. Use a subdomain like `www.yourdomain.com` with a CNAME, then redirect the apex to it 2. Use [Bunny DNS](/docs/dns) with [CDN Acceleration](/docs/cdn/cdn-acceleration), which handles this automatically # Generate a HAR File Source: https://bunny.net/docs/cdn/troubleshooting/generate-a-har-file Capture a HAR file from your browser so support can see exactly what your browser sent and received from the CDN. A HAR (HTTP Archive) file is a JSON recording of every request your browser made during a session, including URLs, headers, timings, and response data. When support asks for one, it's the fastest way for us to see what the edge actually returned instead of working from a description. Exporting one takes about a minute and only uses your browser's built-in developer tools. There's nothing to install. A HAR file can contain cookies, authorization headers, API keys, and request bodies from the pages it captured. Only share it through your support ticket, never publicly. If something in the capture is sensitive, mention it to your support agent instead of sending the file. ## Before you start Order matters. Open developer tools and enable log persistence **before** you reproduce the issue, otherwise the recording won't contain the request that failed. Reproduce the issue against the hostname that's actually affected. If the problem only appears on one Pull Zone hostname, one file type, or from one region, capture it exactly that way — a recording of a working request tells us very little. ## Export a HAR file Navigate to the page or URL served by your Pull Zone where you're seeing the issue. Press `F12`, or `Ctrl + Shift + I` on Windows and Linux, or `Cmd + Option + I` on macOS. You can also right-click the page and choose **Inspect**. Select **Network** in the developer tools toolbar. Tick **Preserve log**. Without it, the request list is cleared on every page reload. Reload the page or repeat the steps that trigger the problem, and wait for it to fail. Right-click anywhere in the request list and choose **Save all as HAR with content**, then pick a location for the file. Navigate to the page or URL served by your Pull Zone where you're seeing the issue. Press `F12`, or `Ctrl + Shift + I` on Windows and Linux, or `Cmd + Option + I` on macOS. Select **Network** in the developer tools toolbar. Open the gear menu in the Network panel and make sure **Persist Logs** is checked. Reload the page or repeat the steps that trigger the problem, and wait for it to fail. Right-click anywhere in the request list and choose **Save All As HAR**. The same option is available in the gear menu. This is a one-time setup. Go to **Safari → Settings → Advanced** and tick **Show features for web developers**. On older versions, the setting is called **Show Develop menu in menu bar**. Navigate to the page or URL served by your Pull Zone where you're seeing the issue. Press `Cmd + Option + I`, or choose **Develop → Show Web Inspector**. Select **Network** in the Web Inspector toolbar, then turn on **Preserve Log**. Reload the page or repeat the steps that trigger the problem, and wait for it to fail. Click the export icon in the Network panel toolbar to save the HAR file. Mobile browsers, including iOS Safari and Chrome for Android, can't export a HAR file on the device itself. Reproduce the issue in a desktop browser instead. If the problem only happens on mobile, tell your support agent the device, OS version, and browser, and they'll suggest another way to capture the traffic. ## What to send with the file Attach the `.har` file to your support ticket along with: * The exact URL that failed, and the Pull Zone or hostname it belongs to * The time the capture was taken, including your time zone * The `CDN-RequestId` response header value, which lets us find the request in our logs * The `Server` response header, which identifies the PoP that handled the request. See [Find Your PoP and Run Diagnostics](/docs/cdn/troubleshooting/run-traceroute). * What you expected to happen, and what happened instead For latency or routing problems, a [diagnostic report](https://tools.bunny.net/diagnostic-report) alongside the HAR file gives us a fuller picture. See [Connectivity](/docs/cdn/connectivity) for network-level troubleshooting. Don't have a ticket yet? Open one from the [bunny.net dashboard](https://dash.bunny.net) or email [support@bunny.net](mailto:support@bunny.net). ## Troubleshooting Developer tools only record while they're open. Open the Network panel and enable **Preserve log** or **Persist Logs** first, then reproduce the issue. Requests made before the panel was open aren't recorded. These options usually go missing on older browser builds. Update to the latest version of Chrome, Edge, Firefox, or Safari and check the Network panel again. Long captures grow quickly, especially with response content included. Re-record a short capture that contains only the failing request, or compress the `.har` file into a `.zip` before attaching it. Chromium-based browsers such as Brave, Opera, Vivaldi, and Arc follow the same steps as Chrome and Edge. If yours works differently, let your support agent know which browser and version you're using. If the failing request comes from a server, CLI, or mobile app, a HAR file won't capture it. Send us the request and response headers instead, along with the `CDN-RequestId` value and a `curl -v` output if you can produce one. # Find Your PoP and Run Diagnostics Source: https://bunny.net/docs/cdn/troubleshooting/run-traceroute Identify which CDN edge server is handling your requests and run network diagnostics. ## Find your PoP Every response from Bunny CDN includes a `Server` header that identifies which PoP and server handled the request: ``` Server: BunnyCDN-{PoP Code}-{Server-ID} ``` **To find this header:** 1. Open your browser developer tools (`Ctrl + Shift + I` or `Cmd + Option + I`) 2. Go to the **Network** tab 3. Load a URL from your Pull Zone 4. Click on the request and find the `Server` header in the response headers **Alternative:** Use the [Bunny Diagnostic Tool](https://tools.bunny.net/diagnostic-report) to automatically show your connection details and PoP routing. ## Detect CDN requests on your origin When Bunny CDN fetches content from your origin, it includes identifying headers you can use for whitelisting or custom logic: | Header | Value | | -------------- | ------------------ | | `CDN-ServerId` | Internal server ID | | `Via` | `BunnyCDN` | ### Other detection methods **Custom hostname:** Use a unique, hard-to-guess hostname as your Origin URL that only Bunny servers access. **Edge Server IP list:** Whitelist CDN IPs programmatically: * IPv4: `https://bunnycdn.com/api/system/edgeserverlist` * IPv6: `https://bunnycdn.com/api/system/edgeserverlist/IPv6` CDN IPs can change. Automate IP list updates to prevent connectivity issues. **Custom header via Edge Rules:** Add a secret header in Edge Rules that your origin validates for authentication. # Website Still Slow Source: https://bunny.net/docs/cdn/troubleshooting/still-slow Tools and techniques to diagnose and fix performance issues after CDN setup. If your website is still slow after setting up Bunny CDN, use these tools to identify bottlenecks. ## Diagnostic tools ### Pingdom Website Speed Test [tools.pingdom.com](https://tools.pingdom.com) Quick and easy performance testing from multiple global locations. Provides a performance grade, optimization suggestions, and a detailed request waterfall. ### GTmetrix [gtmetrix.com](https://gtmetrix.com) Detailed PageSpeed and YSlow analysis with historic performance charts. Default tests run from Vancouver, Canada. Upgrade to GTmetrix PRO for additional test locations. ### WebPagetest [webpagetest.org](https://www.webpagetest.org) The most powerful free benchmarking tool. Features include: * Tests from locations worldwide (including Asia, South America, Africa) * Video recording of page load * Repeat view testing * Connection throttling * Detailed waterfall analysis ### Bunny Diagnostic Report [tools.bunny.net/diagnostic-report](https://tools.bunny.net/diagnostic-report) Bunny's built-in tool showing your connection metrics, PoP routing, and CDN configuration status. ## Common issues **Low cache hit rate:** Check your [cache configuration](/docs/cdn/smart-cache) and ensure cacheable content has appropriate headers. **Origin slow to respond:** Consider enabling [Origin Shield](/docs/cdn/performance/origin-shield) to reduce origin load, or check your origin server performance. **Large assets:** Enable [Bunny Optimizer](/docs/optimizer) for automatic image optimization, or compress assets before uploading. **Too many requests:** Combine CSS/JS files, use sprites for icons, and implement lazy loading for images. # Vary Cache Source: https://bunny.net/docs/cdn/vary-cache Customize the cache key to store multiple versions of files based on browser support, location, device type, or request parameters. By default, Bunny uses the request URL as the cache key. Vary Cache lets you extend the cache key with additional factors—browser capabilities, geographic location, device type, or cookies—so your origin can serve different content to different users from the same URL. If [Bunny Optimizer](/docs/optimizer) is enabled, WebP and URL Query String vary settings are automatically turned on for image files. ## Configure Vary Cache Go to your Pull Zone in the [dashboard](https://dash.bunny.net) and click **Caching** in the side menu. Click **General** inside the Caching section. Enable the desired Vary Cache options (WebP, AVIF, Query String, etc.). Click **Save Vary Configuration**. Save Vary Cache configuration ## Vary Cache settings ### WebP support Stores separate cached versions based on whether the browser supports WebP images. When enabled, browsers that support WebP receive WebP-optimized content while others receive the original format—all from the same URL. Only applies to these extensions: `jpg`, `jpeg`, `webp`, `png`, `gif` ### AVIF support Similar to WebP, this varies the cache based on the browser's ability to display AVIF images. Useful for serving next-gen image formats to supported browsers. ### URL Query String By default, query strings are ignored when constructing the cache key. These URLs would all return the same cached file: ``` https://cdn.example.com/image.jpg https://cdn.example.com/image.jpg?width=300 https://cdn.example.com/image.jpg?width=200 ``` When enabled, each unique query string is treated as a separate file. This is useful for: * Dynamic responses generated from query parameters * Cache busting via versioned URLs * Bunny Optimizer transformations ### User Country Code Includes the end-user's GeoIP country code in the cache key. Your origin can return different content based on the `CDN-RequestCountryCode` header, and each country-specific version is cached separately. Enabling User Country Code creates a separate cached copy per country, which can significantly decrease your cache HIT rate. It also disables individual URL cache purging—you'll only be able to purge the entire zone. ### Requested Hostname Uses the hostname from the request as part of the cache key. With this enabled, requests to `cdn.example.com` and `example.b-cdn.net` on the same Pull Zone are cached separately. This allows your origin to return different content based on the requested domain. Combine with the **Forward Host Header** setting so your origin receives the actual hostname. ### Mobile/Desktop Varies the cache based on whether the request comes from a mobile or desktop device, determined by the User-Agent header. When enabled, Bunny sends a `CDN-MobileDevice` header to your origin with a value of `true` or `false`. Your origin can use this to serve device-appropriate content (such as different image sizes), and each version is cached separately. ### Cookie Varies the cache based on specific cookie values. When enabled, enter the cookie names you want to use as part of the cache key. **Still seeing cookies on your CDN domain after enabling Disable Cookies?** The Disable Cookies feature strips `Set-Cookie` headers traveling over the CDN, but it can't remove wildcard cookies, for example a `.example.com` cookie set by Google Analytics, because those are set directly by your main domain rather than the CDN. They have practically no performance impact. To avoid them entirely, serve the CDN from a domain that isn't a subdomain of your site, or use the `b-cdn.net` hostname. Custom header-based vary is available via internal configuration. Contact [support@bunny.net](mailto:support@bunny.net) if you need to vary cache on a header not listed here. ### Request Headers Varies the cache based on the value of one or more request headers. When enabled, enter the header names you want to include as part of the cache key. This allows your origin to return different content for the same URL while still benefiting from CDN caching. Common use cases include: * Localized content using Accept-Language * Feature flags and staged rollouts using custom headers * API responses that vary by client capabilities * Device-specific experiences using custom application headers For example, varying by `Accept-Language` creates separate cache entries for requests such as: ``` Accept-Language: en-US Accept-Language: fr-FR Accept-Language: de-DE ``` Each unique header value combination creates a separate cache entry. ## Cache key impact Each vary setting you enable multiplies the number of cached versions per URL: | Setting | Cache versions per URL | | --------------- | -------------------------------- | | WebP | 2 (supported / not supported) | | AVIF | 2 (supported / not supported) | | Mobile/Desktop | 2 (mobile / desktop) | | Country Code | Up to 195+ (one per country) | | Query String | Unlimited (one per unique query) | | Cookie | Varies by cookie values | | Request Headers | Varies by header values | Enabling multiple settings compounds this effect. For example, WebP + Mobile + Country Code could create `2 × 2 × 195 = 780` cached versions of a single URL. High cache cardinality reduces your cache HIT rate and increases origin load. Only enable vary settings you actually need. # Verify your configuration Source: https://bunny.net/docs/cdn/verify-configuration Verify that your website is properly configured to use bunny.net CDN. After integrating your website with bunny.net, it's worth confirming that everything is configured correctly for optimal performance and delivery. This article covers a few ways to check that bunny.net is working and to test how your website performance has improved around the world. ## Check the network requests and source code After setting up bunny.net, you'll want to confirm that your website is serving assets from the CDN, especially if you're using a third party plugin such as WordPress. Open your website in your browser and press `Ctrl + Shift + I` (or `Cmd + Option + I` on macOS) to open the developer tools. Click on the **Network** tab, select the **Img** filter, and refresh the page. If everything is set up correctly, you'll see a list of requests to the image files on your website. Hover over a request to see the full image URL, and pay attention to the hostname the file was served from. If the images are served from the hostname configured in your bunny.net account, such as `yourzone.b-cdn.net`, your website is successfully integrated with bunny.net and serving data from it. You can do the same by opening the source code of your website with `Ctrl + U`. This shows the code used to render your page, where you can check that the URLs to static files such as images, CSS, and JavaScript are using the correct bunny.net hostname. ## Check your statistics and cache hit rate Another simple way to confirm that bunny.net is serving your data is to open the **Statistics** page in your [bunny.net dashboard](https://dash.bunny.net) under **Monitoring** > **Statistics**. If you see traffic and requests there, it's a positive sign that bunny.net is delivering traffic for your website. Pay attention to the cache hit rate at the bottom of the statistics page. A cache hit rate below 50%, particularly with decent traffic, suggests configuration issues. For help improving a low cache hit rate, see [Smart Cache](/docs/cdn/smart-cache) and the [Website Still Slow](/docs/cdn/troubleshooting/still-slow) troubleshooting guide. ## Test with a performance testing tool Once you've confirmed your setup, you can see how fast your website performs and find ways to improve it further. See [Website Still Slow](/docs/cdn/troubleshooting/still-slow) for a list of easy to use performance testing tools. When testing, remember to: * Perform multiple tests, especially if you've recently created a new zone, to give bunny.net enough time to cache your website. * Treat any errors or slow performance as a sign of unresolved issues. For more information, see our [troubleshooting](/docs/cdn/troubleshooting/still-slow) articles. # WebSockets Source: https://bunny.net/docs/cdn/websockets bunny.net supports WebSockets to deliver low-latency, bidirectional communication between your applications and users across the globe. By combining WebSockets with our edge network, you can scale real-time features like chat, live updates, multiplayer games, or IoT data streaming with unmatched performance and reliability. # Use Cases * Real-time Applications: Build interactive apps like messaging, live dashboards, or multiplayer games. * Event Streaming: Push updates instantly for stock tickers, live scores, or monitoring feeds. * IoT & Device Data: Stream data from connected devices securely at scale. * Collaboration Tools: Enable whiteboards, document co-editing, or video chat signaling. # Enabling WebSockets You can enable WebSockets directly in the dashboard: 1. Navigate to your Pull Zone -> General -> WebSockets 2. Toggle the "WebSockets" switch. 3. Your site will immediately now support establishing WebSocket connections at our edge. # Connection Limits Each Pull Zone has a maximum number of simultaneous WebSocket connections it will accept. New zones default to 500 concurrent connections, and you can raise or lower this limit at any time from the dashboard or API. * The minimum limit is 100 connections. * You can self-serve up to 25,000 concurrent connections. * Limits are rounded up to the nearest 100. Need more than 25,000 concurrent connections? Our team can help tailor a plan to fit your needs. Simply contact sales via support to discuss higher limits. # Pricing WebSockets are billed pay-as-you-go based on how long connections stay open. There is no monthly subscription and no separate plan to choose. Your maximum-connection limit only caps how many simultaneous connections a zone will accept; it does not affect how you are billed. | Metric | Price | | --------------- | -------------------------------------- | | Connection time | \$0.235 per million connection-minutes | | Bandwidth | Same rate as standard CDN bandwidth | Usage is metered on the total time your connections remain open. One connection-minute is a single open connection for one minute, so 1,000 connections held open for one minute equals 1,000 connection-minutes. Charges are calculated continuously and added to your account throughout the month. For example, holding 1,000 concurrent connections open continuously for a 30-day month is roughly 43.2 million connection-minutes, or about \$10.15 for the month. WebSocket bandwidth is charged at the same rate as regular CDN bandwidth and is included in your monthly CDN bill. # Changelog Source: https://bunny.net/docs/changelog Latest updates and improvements across bunny.net products. ## S3 API: virtual hosted-style URLs, conditional requests, and expanded error codes | Operation / Behavior | Change | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | **URL styles** | Virtual hosted-style URLs (`https://bucket-name.[region]-s3.storage.bunnycdn.com/key`) are now supported alongside path-style URLs. | | **HeadObject** | Returns `ETag` and supports conditional operations: `If-Match`, `If-None-Match`, `If-Modified-Since`, `If-Unmodified-Since`. | | **GetObject** | Returns `ETag` and supports the same conditional operations: `If-Match`, `If-None-Match`, `If-Modified-Since`, `If-Unmodified-Since`. | | **CopyObject** | Preserves `Content-Type`, returns `ETag`, supports `x-amz-metadata-directive`, and accepts conditional copy headers. | | **Rate limiting** | Rate-limited requests return a `SlowDown` (`503`) response with a `Retry-After` header. | ## Bunny Stream Live - Closed Preview We've opened up bunny Stream customers to signup for our new live streaming preview. The signup banner can be found within your video library! Once approved you'll be able to live stream at scale globally to your audiences. All managed with the new dashboard control room & API. Features include: RTMP ingest & distribution, scheduling, countdown, DVR, VOD and more. ## Investigative Event Logs for Bunny Shield Bunny Shield Event Logs now support advanced filtering and grouping across Feature, Action, Rule ID, IP / Range, Country, ASN, JA4 Fingerprint, User-Agent, and URL. Combine multiple signals, such as IP, User-Agent, and JA4, to investigate related activity and uncover attack patterns across Shield features. You can also investigate events across multiple days within the 3-day retention window and export results as CSV for further analysis. [Learn more](/docs/api-reference/shield/event-logs/search-filter-and-group-event-logs-for-a-shield-zone) ## Connect a Bunny Database from the Edge Scripting creation flow You can now connect a Bunny Database directly from the Edge Scripting creation flow. Create a new database or pick an existing one, and we'll automatically add the connection credentials (`BUNNY_DATABASE_URL` and `BUNNY_DATABASE_AUTH_TOKEN`) as script secrets. There's also a new Database CRUD API script template available to get you started. Add Database ## Cloud sandbox environments via Bunny CLI The Bunny CLI now supports creating and managing on-demand cloud sandbox environments backed by Bunny Magic Containers. Each sandbox is a fully isolated Ubuntu container with Node.js, Bun, Python, and Claude Code pre-installed, with 10 GB of persistent storage. Create, SSH into, copy files, expose public URLs, and manage environment variables — all from the CLI. [Learn more](/docs/cli/commands/sandbox) ## IP Family Policy for Pull Zones Pull Zones now support an IP Family Policy that controls which address families (IPv4, IPv6, or both) are advertised and which edge servers are eligible to serve the zone. Four policies are available: IPv4 Only, Dual Stack (default), Dual Stack Prefer IPv6, and IPv6 Only. Token authentication IP locking now also supports IPv6 addresses, binding at the client's `/64` prefix. [Learn more](/docs/cdn/ip-family-policy) ## Read-only password support for S3 API Bunny Storage's S3-compatible API now supports read-only access keys. Use a dedicated read-only secret access key for S3 connections that should only have read permissions, adding an extra layer of access control to your storage zones. [Learn more](/docs/storage/s3) ## Verified bot customization for Shield Zones Bunny Shield now lets you control how verified bots are handled per Shield Zone — allow, block, or take no action — with per-bot and per-category overrides across seven categories: SEO, AI Scraper, AI Tool, Tool, Ads, Preview, and Social. Verification supports both reverse DNS and published IP range checks, and verified bot categories are exposed to the Rule Engine via the `VERIFIED_BOT_CATEGORY` variable for custom WAF rules. [Learn more](/docs/shield/verified-bots) ## Before cache execution for Edge Scripts (Preview) Edge Scripts can now run **before** the CDN cache layer, giving you full control over every request instead of only executing on cache misses. When enabled on a Pull Zone, middleware scripts gain two new hooks — `onClientRequest` (intercept requests before the cache lookup) and `onClientResponse` (modify responses before they reach the client, including cached ones). Standalone scripts also support this mode, running on every request and deciding how responses are cached. [Learn more](/docs/scripting/before-cache) ## Bunny Player extended Chromecast support We've just updated the Bunny player with a rebuilt Chromecast experience. Casting now supports eDRM (Widevine-protected) videos, closing a long-standing gap versus the legacy player. The TV experience feels native, with the video title and thumbnail on screen and player colors and captions appearance (color, font, size, language) synced from the embed. Viewers keep full playback control while casting — audio track selection works before and during a session, and playback rate changes apply on the TV. Session handling is seamless too: playback continues from the current position when casting starts and transfers back on stop, with page reloads, replays after the video ends with a smoother experience overall. [Learn more](/docs/stream/player-settings) ## Cache API documentation reorganized into dedicated section The Edge Scripting Cache API documentation has been restructured from a single page into a multi-page section covering the API overview and limitations, managing default and named caches, reading and writing cache entries (`match`, `put`, `delete`), and end-to-end examples including a new on-demand refresh and purge recipe. The `waitUntil` runtime reference has also been expanded with a full API signature and a cache-population example. [Learn more](/docs/scripting/cache) ## HTTP validation for Seamless Domain Migration Seamless Domain Migration now supports HTTP (file-based) validation alongside the existing DNS (TXT record) method for issuing SSL certificates before pointing your domain to bunny.net. Choose DNS if you control your domain's DNS records, or HTTP if you can place a challenge file on the server your domain currently points to. [Learn more](/docs/cdn/seamless-migration) ## Symbolic links, timestamps, and permissions in Edge Scripting The Edge Scripting Node.js file system module now supports symbolic links (`symlink()`, `readlink()`, `lstat()`), returns correct file timestamps (`atime`, `mtime`, `ctime`, `birthtime`), and enforces owner-level file and directory permissions (`chmod()`, `access()`, `Stats.mode`). [Learn more](/docs/scripting/node-fs) ## S3 API 'Public Preview' launched S3-Compatible API is now available for all! We have also added a new S3 Storage Zone region: Sydney (SYD) & Uploadpart now supports Content-MD5 validation. [Learn more](/docs/storage/s3) ## AVIF, HEIC and HEIF are now GAAVIF, HEIC and HEIF are now GA Support for AVIF, HEIC and HEIF as input formats, and AVIF as an output format, has been promoted to [General Availability](/docs/product-release-stages)Support for AVIF, HEIC and HEIF as input formats, and AVIF as an output format, has been promoted to [General Availability](/docs/product-release-stages). [Learn more](/docs/optimizer/dynamic-images/formats) ## Edge Scripting Runtime API and `waitUntil` documentation New Runtime API reference page for Edge Scripting, documenting the `waitUntil` function that allows scripts to continue running after a request completes — useful for maintaining WebSocket connections and background tasks. The WebSocket guide has also been updated with a practical `waitUntil` example. [Learn more](/docs/scripting/runtime) ## Custom Response Pages for Bunny Shield Customize the challenge, block, and rate limit pages shown to visitors when Bunny Shield takes action. Replace default response pages with fully branded HTML experiences that match your website, including custom styling, messaging, and support information. Available on Advanced and higher Shield plans. [Learn more](/docs/shield/custom-response-pages) ## Direct play URLs now support query parameters and documentation improvements Added direct play URL and embed URL examples with query parameters, documented the `-auto` captions flag, and improved cross-linking between Stream pages. Updated MP4 fallback documentation to reflect support for 240p, 360p, 720p & 1080p resolutions. [Learn more](/docs/stream/storage-structure) ## CDN Connectivity documentation New documentation covering IPv4 and IPv6 connectivity across the bunny.net CDN, including network tier support, origin connectivity, CDN Acceleration with dual-stack origins, and Origin Shield for guaranteed IPv6 connectivity. [Learn more](/docs/cdn/connectivity) ## Edge Scripting support now in CLI The Bunny CLI now includes full Edge Scripts management (`bunny scripts`): create, deploy, delete scripts, manage deployments and rollbacks, environment variables and secrets, and custom domains with SSL. [Learn more](/docs/cli) ## Storage root directory deletion support The Storage Edge API DELETE endpoint now supports deleting the root directory (`/`) of a storage zone. By default, root deletion is blocked as a safety guard. Pass `allowRootDelete=true` as a query parameter or request header to bypass this protection. [Learn more](/docs/storage/http#deleting-the-root-directory) ## AVIF output area limit Bunny Optimizer now limits AVIF output to a maximum area of 4 megapixels. Larger images automatically fall back to another format (WebP, JPEG, or GIF) to keep latency low. A new `304` processing error code is returned when this limit is reached. [Learn more](/docs/optimizer/dynamic-images/formats) ## Vary Cache by Request Headers You can now vary cached content based on the value of one or more request headers, such as `Accept-Language`. Each unique header value combination creates a separate cache entry, enabling localized content, feature flags, and device-specific experiences without sacrificing CDN caching. [Learn more](/docs/cdn/vary-cache) ## WebSocket pay-as-you-go pricing WebSocket billing has moved to pay-as-you-go pricing based on connection-minutes, replacing the previous tiered monthly subscription model. Connection limits can now be configured from 100 to 25,000 concurrent connections per zone. [Learn more](/docs/cdn/websockets) ## Stream Transcribing supported languages reference The Transcribing documentation now includes a complete reference table of all 56 supported languages with their ISO 639-1 codes. [Learn more](/docs/stream/transcribing) ## CDN Logging API enhancements The CDN Logging API now supports exact request ID lookup and includes additional fields such as JA4 TLS client fingerprint, ASN, and ASN organization in the response. [Learn more](/docs/cdn/logging/index) ## Stream compact controls added to player settings Maximize screen real estate on the bunny player with Compact Controls. Ideal for mobile and embedded players. You can now enable compact controls on your video library via the 'Compact Controls' toggle within player settings. [Learn more](/docs/stream/player-settings) ## Stream Video storage size info API endpoint & Dashboard update We've created and extended the 'Get Video Storage Size Info' endpoint, giving customers a granular, per-video breakdown of your encoded videos storage consumption over API. [Learn more](/docs/api-reference/stream/manage-videos/get-video-storage-size-info) This feature has been added to the Stream dashboard for granular video storage usage breakdown via the (i) information icon - selectable within your video library or the video page. [Learn more](/docs/stream/encoding) ## CDN Logging API v2 The CDN Logging API v2 now returns structured JSON with filtering by status, cache status, country, IP, edge location, URL, User-Agent, and Referer. [Learn more](/docs/cdn/logging) ## S3 API 'Closed Preview' Added support for 4 new S3 Storage Zone regions: London (UK), Stockholm (SE), Los Angeles (LA) and Johannesburg (JH). ## Bunny CLI The new Bunny CLI lets you manage bunny.net resources from your terminal. The initial release supports Bunny Database: create databases, open an interactive SQL shell with `bunny db shell`, generate auth tokens, and browse data with `bunny db studio`. [Learn more](/docs/cli) ## Per-request control for Request Coalescing You can now enable or disable Request Coalescing per request using Edge Rules, allowing more granular control over caching behavior. [Learn more](/docs/cdn/request-coalescing) ## Stream compact controls - bunny player feature Maximize screen real estate on the bunny player with Compact Controls. Ideal for mobile and embedded players. You can now enable compact controls on your video library via the 'update video library' API endpoint or by adding 'compactControls=true' to your player embed parameters. [Learn more](/docs/api-reference/core/stream-video-library/update-video-library#response-enable-compact-controls) ## Stream 'Add video library' API update The Stream 'Add video library' API endpoint can now pre-configure premium encoding, transcribing, video resolutions and more in a single API call.
[Learn more](/docs/api-reference/core/stream-video-library/add-video-library)
## AVIF support The Optimizer now supports AVIF as both an input and output format, offering better compression than WebP with excellent quality. [Learn more](/docs/optimizer/dynamic-images/formats) ## Image Upscaling Introduced the 'upscaling' parameter to allow images to be enlarged beyond their original dimensions using resampling. [Learn more](/docs/optimizer/dynamic-images/resizing) ## Seamless Domain Migration Introducing seamless domain migration with SSL certificate issuance via DNS verification, allowing zero-downtime transitions to bunny.net. [Learn more](/docs/cdn/seamless-migration) ## API Guardian Introducing API Guardian, a new feature in Bunny Shield that provides schema-aware protection for your APIs. It enforces your OpenAPI contract at the edge, ensuring requests and responses match your application's expectations, and stops invalid or abusive traffic before it reaches your origin. [Learn more](/docs/shield/api-guardian) ## bunny player This release makes playback smoother and more reliable everywhere. We've upgraded media-chrome to 4.19.0, fixed Firefox H.265 seeking and referrer handling, tightened Chromecast sync, and cleaned up iOS fullscreen transitions. Early-play and JIT videos start has improved, heatmaps work on token-protected embeds, and we've refined French UI translations and expanded RUM monitoring to new regions. ## Stream API Upgraded Smart feature language model to v5.4 for higher quality generation. JIT watermarking is now more reliable, accurate, and consistent. Cleanup unconfigured resolutions endpoint now supports MP4 removal [Learn more](/docs/api-reference/stream/manage-videos/cleanup-unconfigured-resolutions#parameter-delete-mp4-files) ## S3 API 'Closed Preview' CopyObject now handles source objects up to 5 GB and plays nicely with multipart uploads, CORS works across pre-signed URLS. ## Transcribing upgrade Transcribing language model has been upgraded to v1.2.0 for faster and higher quality transcriptions. [Learn more](/docs/stream/transcribing) ## Free encoding Increased capacity and performance improvements to Stream Free encoding for faster transcoding and shorter queue time. [Learn more](/docs/stream/encoding) ## S3 API 'Closed Preview' Added presigned URL support for S3 via the AWS CLI presign command. ## Language detection for transcribing Transcribing now includes language detection information, making it easier to manage multi-language video content. [Learn more](/docs/stream/transcribing) ## Vimeo2Bunny CLI A new command-line tool to migrate videos from Vimeo to Bunny Stream. Videos transfer directly from Vimeo via Fetch Video URL — nothing is downloaded to your machine. [Learn more](/docs/stream/vimeo2bunny) ## Edge Rule pattern matching Use Lua-based pattern matching in Edge Rule conditions to match structured request values such as URLs, headers, cookies, and query strings. [Learn more](/docs/cdn/edge-rules/pattern-matching) ## Database Shell The Bunny Database Shell (`bsql`) is a standalone, interactive SQL shell for querying and managing your database from the terminal. [Learn more](/docs/database/connect/database-shell) ## Webhook signature validation Validate webhook signatures to verify that incoming notifications are genuinely from Bunny Stream and have not been tampered with. [Learn more](/docs/stream/webhooks) ## Player 2.0 The new Bunny Stream video player is here with a modern interface and improved performance. It's enabled by default for all new video libraries. [Learn more](/docs/stream/player) ## Templates Deploy pre-built application templates with just a few clicks. Each template includes a ready-to-run container image, and some include a sidecar database. [Learn more](/docs/magic-containers/templates) ## Quick Deploy A streamlined deployment flow that lets you go from zero to deployed in seconds, with everything presented in a single form. [Learn more](/docs/magic-containers/quick-deploy) ## JA4 fingerprinting JA4 TLS fingerprinting is now available on all pull zones. Identify clients based on their TLS handshake characteristics via the `CDN-JA4` request header. [Learn more](/docs/cdn/security/ja4-fingerprinting) ## Logging and Permanent Log Storage updates Logging documentation has been updated with improved details on log retention, and Permanent Log Storage now includes specifics on part rotation and compression. [Learn more](/docs/cdn/logging/permanent-storage) ## Mobile SDK token authentication Token authentication is now available for the Stream Mobile SDKs, adding an extra layer of security for mobile video playback. [Learn more](/docs/stream/mobile-sdk-token-authentication) ## Node.js file system API A Node.js-compatible file system API is now available in Edge Scripts, allowing you to read and write files directly at the edge. [Learn more](/docs/scripting/node-fs) ## Graceful Shutdown New applications now have a default grace period of 30 seconds (up from 1 second), giving your applications more time to clean up resources and complete in-flight requests during rolling updates and scaling events. [Learn more](/docs/magic-containers/graceful-shutdown) ## Smart Chapters Automatically generate chapters for your videos using AI. Smart Chapters analyzes your video's caption track to create chapter markers. [Learn more](/docs/stream/smart-chapters) ## SQL API Execute SQL queries against your Bunny Database over HTTP using the SQL API. [Learn more](/docs/database/connect/sql-api) ## Database SDKs Connect to Bunny Database using official SDKs for TypeScript, Go, Rust, and .NET. [Learn more](/docs/database/connect/typescript) # Changelog Source: https://bunny.net/docs/cli/changelog Release notes for the Bunny CLI. ## Sandbox commands `bunny sandbox` manages on-demand cloud sandbox environments backed by Bunny Magic Containers. Create sandboxes (`create`), connect via SSH (`ssh`), run commands (`exec`), copy files (`cp`), browse remote files (`files`), expose ports as public HTTPS endpoints (`url add`/`list`/`delete`), and manage persistent environment variables (`env set`/`list`/`delete`). Each sandbox is a fully isolated Ubuntu container with Node.js, Bun, Python, and Claude Code pre-installed, with a 10 GB persistent volume mounted at `/workplace`. [Learn more](/docs/cli/commands/sandbox) ## Scriptable DNS commands and record presets `bunny dns scripts` brings [Scriptable DNS](/docs/dns/scriptable/introduction) to the CLI: `init` scaffolds a project from starter examples (geo, closest, weighted, failover, pullzone) with the [`@bunny.net/scriptable-dns-types`](https://github.com/BunnyWay/cli/tree/main/packages/scriptable-dns-types) package preconfigured for editor autocomplete, `deploy` uploads and publishes your entry file, and `attach` points a hostname at the script by adding a `SCRIPT` record to a zone. `bunny dns records preset` applies curated record sets for common providers and tasks (Google Workspace, Microsoft 365, DMARC, and more) in one step, and `bunny dns zones link` lets a directory resolve its zone without passing a domain. [Learn more](/docs/cli/commands/dns) ## Edge Scripts commands `bunny scripts` now covers the full Edge Scripts lifecycle: `create` a script without scaffolding, `deploy` and manage `deployments` (including rollbacks), `env` for environment variables and secrets, `domains` for custom domains with SSL, `stats` for usage statistics, and `delete`. The `init` command adds `--template-repo` for custom templates and `--github-actions` / `--no-github-actions` flags. [Learn more](/docs/cli/commands/scripts) ## DNS commands (experimental) `bunny dns` adds CLI management for DNS zones and records. Create, list, update, and remove records, import and export BIND zone files, manage DNSSEC, view query statistics, and configure query logging with optional IP anonymization. This command is experimental and hidden from `--help` while it stabilizes. [Learn more](/docs/cli/commands/dns) ## Shell completion Run `bunny completion` to generate a shell completion script for tab completion in your terminal. [Learn more](/docs/cli/configuration#shell-completion) ## Database shell session note The `bunny db shell` documentation now notes that sessions last 30 minutes when no explicit token is provided. [Learn more](/docs/cli/commands/db) # bunny api Source: https://bunny.net/docs/cli/commands/api Make raw authenticated HTTP requests to any bunny.net API endpoint. `bunny api` sends a raw HTTP request to any bunny.net API endpoint with authentication handled automatically via your configured API key. It's useful for calling endpoints the CLI doesn't expose directly, scripting, and exploring the API. ```bash theme={null} # List pull zones bunny api GET /pullzone # Get a specific pull zone bunny api GET /pullzone/12345 # List storage zones bunny api GET /storagezone # Create a pull zone with a JSON body bunny api POST /pullzone --body '{"Name":"my-zone","OriginUrl":"https://example.com"}' # Delete a DNS zone bunny api DELETE /dnszone/12345 # Pipe body from stdin echo '{"Name":"my-zone","OriginUrl":"https://example.com"}' | bunny api POST /pullzone # Show request/response details bunny api GET /pullzone --verbose ``` | Flag | Alias | Description | | -------- | ----- | ----------------- | | `--body` | `-b` | JSON request body | ## Paths and methods * The method is **case-insensitive** (`get` and `GET` both work). * Paths are relative to `https://api.bunny.net`. * See the [API Reference](/docs/api-reference) for a complete list of endpoints. ## Request body You can pass a JSON body three ways: 1. Inline with `--body`: ```bash theme={null} bunny api POST /pullzone --body '{"Name":"my-zone","OriginUrl":"https://example.com"}' ``` 2. From stdin: ```bash theme={null} echo '{"Name":"my-zone","OriginUrl":"https://example.com"}' | bunny api POST /pullzone ``` 3. From a file via shell redirection: ```bash theme={null} bunny api POST /pullzone < body.json ``` ## Tip: inspect requests Use `--verbose` to print the full request URL, headers, and raw response. Handy when exploring an endpoint or debugging auth. ```bash theme={null} bunny api GET /pullzone --verbose ``` # Authentication Source: https://bunny.net/docs/cli/commands/auth Log in, log out, and verify the active account with `bunny login`, `bunny logout`, and `bunny whoami`. ## `bunny login` Authenticate with bunny.net via the browser. A successful login stores credentials under the selected profile (defaults to `default`). ```bash theme={null} # Browser-based login bunny login # Log in to a specific profile bunny login --profile staging # Overwrite an existing profile without prompting bunny login --force ``` | Flag | Description | | ----------- | ----------------------------------------------- | | `--profile` | Profile name to log in to (default: `default`) | | `--force` | Overwrite an existing profile without prompting | Prefer API keys over browser auth in CI. Run `bunny config init --api-key bny_xxx` to seed a profile, or export `BUNNYNET_API_KEY`. ## `bunny logout` Remove a stored authentication profile. ```bash theme={null} bunny logout bunny logout --force bunny logout --profile staging ``` | Flag | Description | | ----------- | -------------------------------------- | | `--profile` | Profile to remove (default: `default`) | | `--force` | Skip confirmation prompts | ## `bunny whoami` Show the currently authenticated account: name, email, active profile, and account ID. ```bash theme={null} bunny whoami # Logged in as Jamie Barton (jamie@bunny.net) 🐇 # Profile: default # Account ID: 3d5a9c1e-7f42-4b8a-9c36-2e8d1f6a4b70 bunny whoami --output json bunny whoami --profile staging ``` | Flag | Description | | ----------- | --------------------------------------- | | `--profile` | Profile to inspect (default: `default`) | | `--output` | `text` or `json` | # bunny config Source: https://bunny.net/docs/cli/commands/config Manage CLI configuration and named profiles. Profiles let you keep multiple authenticated configurations (personal, staging, production) and switch between them with `--profile`. See [Configuration](/docs/cli/configuration) for an overview. ## `bunny config init` Initialize CLI configuration. Prompts for an API key unless one is provided. ```bash theme={null} # Interactive: prompts for an API key bunny config init # Non-interactive bunny config init --api-key bny_xxxxxxxxxxxx ``` | Flag | Description | | ----------- | ---------------------------------------------------------- | | `--api-key` | API key to store in the profile (skips interactive prompt) | | `--profile` | Profile name to initialize (default: `default`) | ## `bunny config show` Print the resolved configuration: active profile, API key status, and API URL. ```bash theme={null} bunny config show bunny config show --output json bunny config show --profile staging ``` | Flag | Description | | ----------- | --------------------------------------- | | `--output` | `text` or `json` | | `--profile` | Profile to inspect (default: `default`) | ## `bunny config profile create` Create a new named profile. ```bash theme={null} bunny config profile create staging bunny config profile create staging --api-key bny_xxxxxxxxxxxx ``` | Flag | Description | | ----------- | ----------------------------------- | | `--api-key` | API key to store in the new profile | ## `bunny config profile delete` Delete a named profile. ```bash theme={null} bunny config profile delete staging ``` # bunny db Source: https://bunny.net/docs/cli/commands/db Create, manage, and query Bunny Databases from the command line. `bunny db` provides full lifecycle management for [Bunny Database](/docs/database): creating databases, linking them to local projects, running SQL interactively, generating auth tokens, and viewing tables in a browser. ## Database resolution Most `db` commands accept an optional `` positional argument. When omitted, the CLI resolves the target in this order: 1. Explicit `` argument 2. `.bunny/database.json` manifest written by `bunny db link` 3. `BUNNY_DATABASE_URL` in a `.env` file (walked up from the current directory) matched against your database list 4. Interactive selection prompt For `bunny db shell`, the CLI also reads `BUNNY_DATABASE_AUTH_TOKEN` from `.env` to skip token generation. Both variables can be written automatically by `bunny db create` or `bunny db quickstart`. *** ## `bunny db create` Create a new database. Interactively prompts for name and region selection (automatic, single region, or manual) when flags are omitted. After creation, prompts to link the directory, generate an auth token, and save credentials to `.env`. ```bash theme={null} # Interactive: prompts for name and region mode bunny db create # Single region bunny db create --name mydb --primary FR # Multi-region with replicas bunny db create --name mydb --primary FR,DE --replicas UK,NY # Fully non-interactive (CI / scripts) bunny db create --name mydb --primary FR --link --token --save-env --output json ``` | Flag | Description | | ------------------ | ---------------------------------------------------------------------------------------- | | `--name` | Database name | | `--primary` | Comma-separated primary region IDs (e.g. `FR` or `FR,DE`) | | `--replicas` | Comma-separated replica region IDs (e.g. `UK,NY`) | | `--storage-region` | Override the auto-detected storage region | | `--link` | Link the current directory to the new database (skips prompt). Use `--no-link` to skip. | | `--token` | Generate a full-access auth token (skips prompt). Use `--no-token` to skip. | | `--save-env` | Save `BUNNY_DATABASE_URL` and `BUNNY_DATABASE_AUTH_TOKEN` to `.env`. Requires `--token`. | In `--output json` mode, prompts are suppressed entirely. Flags are the only way to opt in to linking, token creation, and `.env` writes. The JSON output gains `linked`, `token`, and `saved_to_env` fields reflecting what happened. ## `bunny db list` List all databases. Shows ID, name, status, primary region, and size. ```bash theme={null} bunny db list bunny db list --output json ``` ## `bunny db show` Show details for a single database. ```bash theme={null} bunny db show bunny db show bunny db show --output json ``` ## `bunny db link` Link the current directory to a database. Writes `{ id, name }` to `.bunny/database.json` so subsequent `db` commands resolve the target without `BUNNY_DATABASE_URL` in `.env`. With no argument, lists all databases for interactive selection. ```bash theme={null} # Interactive selection bunny db link # Direct link by ID bunny db link ``` `bunny db create` offers to link the new database, and `bunny db delete` automatically removes a stale link when it points at the deleted database. ## `bunny db delete` Permanently delete a database. Requires double confirmation (or `--force` to skip). ```bash theme={null} bunny db delete bunny db delete --force ``` | Flag | Description | | --------- | ------------------------- | | `--force` | Skip confirmation prompts | ## `bunny db regions` Manage primary and replica regions for a database. ### `bunny db regions list` ```bash theme={null} bunny db regions list bunny db regions list ``` ### `bunny db regions add` ```bash theme={null} bunny db regions add --primary FR,DE bunny db regions add --replicas UK,NY bunny db regions add --primary FR --replicas UK ``` | Flag | Description | | ------------ | ----------------------------------------- | | `--primary` | Comma-separated primary region IDs to add | | `--replicas` | Comma-separated replica region IDs to add | ### `bunny db regions remove` ```bash theme={null} bunny db regions remove --primary FR bunny db regions remove --replicas UK,NY ``` | Flag | Description | | ------------ | -------------------------------------------- | | `--primary` | Comma-separated primary region IDs to remove | | `--replicas` | Comma-separated replica region IDs to remove | ### `bunny db regions update` Interactively update region configuration. Shows all available regions with current ones pre-selected. Toggle on/off and confirm. ```bash theme={null} bunny db regions update bunny db regions update ``` ## `bunny db usage` Show usage statistics for a database. ```bash theme={null} bunny db usage bunny db usage --period 7d bunny db usage --output json ``` ## `bunny db quickstart` Generate a quickstart guide for connecting to a database from your preferred language. ```bash theme={null} bunny db quickstart bunny db quickstart --lang bun ``` ## `bunny db shell` Open an interactive SQL shell for a database. Supports multiple output modes, sensitive column masking, persistent history, and dot-commands for quick introspection. When no `--token` is supplied and `BUNNY_DATABASE_AUTH_TOKEN` is not set, the shell session is active for 30 minutes. Re-run the command to reconnect, or pass `--token` / set `BUNNY_DATABASE_AUTH_TOKEN` to use your own credentials. ```bash theme={null} # Interactive shell (auto-detects database from .env) bunny db shell # Specify a database ID bunny db shell # Execute a query and exit bunny db shell "SELECT * FROM users" bunny db shell "SELECT * FROM users" bunny db shell --execute "SELECT COUNT(*) FROM posts" # Output modes bunny db shell -m json -e "SELECT * FROM users" bunny db shell -m csv -e "SELECT * FROM users" bunny db shell -m markdown -e "SELECT * FROM users" # Execute a SQL file bunny db shell -e seed.sql bunny db shell seed.sql # Show sensitive columns unmasked bunny db shell --unmask # Direct connection (skip API lookup) bunny db shell --url libsql://... --token ey... ``` | Flag | Alias | Description | | ----------- | ----- | ---------------------------------------------------------- | | `--execute` | `-e` | Execute a SQL statement and exit | | `--mode` | `-m` | Output mode: `default`, `table`, `json`, `csv`, `markdown` | | `--unmask` | | Show sensitive column values unmasked | | `--url` | | Database URL (skips API lookup) | | `--token` | | Auth token (skips token generation) | ### Dot-commands Available in interactive mode: | Command | Description | | ------------------ | ----------------------------------------- | | `.tables` | List all tables | | `.describe TABLE` | Show column details for a table | | `.schema [TABLE]` | Show CREATE statements | | `.indexes [TABLE]` | List indexes | | `.fk TABLE` | Show foreign keys for a table | | `.er` | Show entity-relationship overview | | `.count TABLE` | Count rows in a table | | `.size TABLE` | Show table stats (rows, columns, indexes) | | `.truncate TABLE` | Delete all rows from a table | | `.dump [TABLE]` | Dump schema and data as SQL | | `.read FILE` | Execute SQL statements from a file | | `.mode [MODE]` | Set output mode | | `.timing` | Toggle query execution timing | | `.mask` | Enable sensitive column masking | | `.unmask` | Disable sensitive column masking | | `.clear-history` | Clear command history | | `.help` | Show available commands | | `.quit` / `.exit` | Exit the shell | ### Sensitive column masking Columns matching patterns like `password`, `secret`, `api_key`, `auth_token`, `ssn`, etc. are masked by default (`********`). Email columns are partially masked (`a••••e@example.com`). Use `.unmask` or the `--unmask` flag to reveal values. ## `bunny db studio` Open a read-only table viewer in your browser. Spins up a local server, generates a short-lived auth token if needed, and opens the studio UI. ```bash theme={null} # Auto-detect database (link, .env, or interactive) bunny db studio # Specific database bunny db studio # Custom port bunny db studio --port 3000 # Don't auto-open the browser bunny db studio --no-open # Use explicit credentials (skips API lookup) bunny db studio --url libsql://... --token ey... ``` | Flag | Description | | ----------- | ----------------------------------------------- | | `--port` | Port for the local studio server (default 4488) | | `--url` | Database URL (skips API lookup) | | `--token` | Auth token (skips token generation) | | `--no-open` | Don't automatically open the browser | ## `bunny db tokens` Generate and invalidate database auth tokens. ### `bunny db tokens create` Generate an auth token for a database. The database ID can be provided as a positional argument or auto-detected from `BUNNY_DATABASE_URL` in a `.env` file. ```bash theme={null} # Provide database ID explicitly bunny db tokens create # Auto-detect from .env BUNNY_DATABASE_URL bunny db tokens create # Read-only token bunny db tokens create --read-only # Token with expiry (duration shorthand or RFC 3339) bunny db tokens create --expiry 30d bunny db tokens create --expiry 2026-12-31T23:59:59Z ``` | Flag | Description | | -------------- | ------------------------------------------------------------------------ | | `--read-only` | Generate a read-only token (default: full access) | | `-e, --expiry` | Token expiry. Duration (`30d`, `12h`, `1w`, `1m`, `1y`) or RFC 3339 date | ### `bunny db tokens invalidate` Invalidate all auth tokens for a database. Prompts for confirmation unless `--force` is passed. ```bash theme={null} bunny db tokens invalidate bunny db tokens invalidate --force ``` | Flag | Description | | --------- | ------------------------- | | `--force` | Skip confirmation prompts | # bunny sandbox Source: https://bunny.net/docs/cli/commands/sandbox Create and manage on-demand cloud sandbox environments backed by Bunny Magic Containers. `bunny sandbox` manages on-demand cloud sandbox environments backed by [Bunny Magic Containers](/docs/magic-containers). Each sandbox is a fully isolated Ubuntu container with Node.js, Bun, Python, and Claude Code pre-installed. A 10 GB persistent volume is mounted at `/workplace`, your default working directory. Claude Code is pre-installed but needs your own Anthropic credentials before it can do anything: pass an API key at create time (prefer `--env-file .env` so the key stays out of your shell history), or run `claude` inside the sandbox and complete the login prompt it prints. Both survive restarts and redeploys: baked env vars live on the container, and the login flow writes to `/workplace/.claude` on the persistent volume. Sandbox credentials (app ID, SSH endpoint, agent token) are stored in the CLI's local config file (`~/.config/bunnynet.json` by default) so you can reconnect without re-creating. *** ## `bunny sandbox create` Create and start a new sandbox. Waits for the container's SSH port to become reachable before returning. ```bash Command (create) theme={null} # Create a sandbox with the default name "sandbox" bunny sandbox create # Create a named sandbox bunny sandbox create my-sandbox # Create in a specific region bunny sandbox create my-sandbox --region NY # Bake in environment variables (persisted for the sandbox's lifetime) bunny sandbox create my-sandbox -e NODE_ENV=production -e PORT=8080 bunny sandbox create my-sandbox --env-file .env # Give Claude Code your Anthropic API key at create time (.env holds ANTHROPIC_API_KEY) bunny sandbox create my-sandbox --env-file .env ``` ```text Response (create) theme={null} Sandbox "my-sandbox" is ready. App ID: Ab3xK9mPq2wR7nL SSH: 203.0.113.42:8023 Run commands with: bunny sandbox exec my-sandbox ``` | Flag | Alias | Description | Default | | ------------ | ----- | -------------------------------------------------- | ------- | | `--region` | | Region ID to deploy in (e.g. `AMS`, `NY`, `LA`, …) | `AMS` | | `--env` | `-e` | Environment variable as `KEY=VALUE` (repeatable) | | | `--env-file` | | Load environment variables from a dotenv file | | Variables set at creation are baked into the container and persist across restarts. Values from `--env` override those loaded from `--env-file`. To change them later, use [`bunny sandbox env`](#bunny-sandbox-env). Once ready, the output shows the app ID and SSH address. Public URLs come later via [`bunny sandbox url add`](#bunny-sandbox-url-add). ## `bunny sandbox list` List all sandboxes saved in your local config. ```bash Command (list) theme={null} bunny sandbox list bunny sandbox ls # alias ``` ```text Response (list) theme={null} Name App ID SSH my-sandbox Ab3xK9mPq2wR7nL 203.0.113.42:8023 ``` ## `bunny sandbox delete` Delete a sandbox and permanently destroy the underlying Magic Containers app. ```bash Command (delete) theme={null} bunny sandbox delete my-sandbox # Skip the confirmation prompt bunny sandbox delete my-sandbox --force bunny sandbox rm my-sandbox -f # alias ``` ```text Response (delete) theme={null} Delete sandbox "my-sandbox" (app Ab3xK9mPq2wR7nL)? (y/N) y Sandbox "my-sandbox" deleted. ``` | Flag | Alias | Description | Default | | --------- | ----- | ------------------------ | ------- | | `--force` | `-f` | Skip confirmation prompt | `false` | ## `bunny sandbox exec` Run a shell command inside a sandbox over SSH. Defaults to `/workplace` as the working directory. ```bash Command (exec) theme={null} # Run a command bunny sandbox exec my-sandbox ls -la # Run in a different directory bunny sandbox exec my-sandbox --cwd /tmp env # Pipe-friendly: exit code is propagated bunny sandbox exec my-sandbox -- cat /etc/os-release # Inject temporary environment variables for this command only bunny sandbox exec my-sandbox --env DEBUG=1 -- node app.js bunny sandbox exec my-sandbox --env-file .env -- printenv # Give up after 30 seconds (exit code 124) bunny sandbox exec my-sandbox --timeout 30 -- bun run build ``` ```text Response (exec) theme={null} total 16 drwxr-xr-x 3 root root 4096 Jul 16 09:12 . drwxr-xr-x 1 root root 4096 Jul 16 09:10 .. -rw-r--r-- 1 root root 1254 Jul 16 09:12 app.js drwxr-xr-x 2 root root 4096 Jul 16 09:11 src ``` The command's stdout and stderr stream to your terminal unchanged, and its exit code is propagated, so `exec` works in scripts and pipes. | Flag | Alias | Description | Default | | ------------ | ----- | --------------------------------------------------------------- | ------------ | | `--cwd` | | Working directory inside the sandbox | `/workplace` | | `--env` | | Environment variable as `KEY=VALUE` (repeatable) | | | `--env-file` | | Load environment variables from a dotenv file | | | `--timeout` | | Close the SSH connection and exit `124` after this many seconds | | Variables passed here apply only to that single command and are **not** persisted. For persistent variables, use [`bunny sandbox env`](#bunny-sandbox-env). ## `bunny sandbox cp` Copy a file between your machine and a sandbox over SFTP. Exactly one of the two paths must reference a sandbox as `:`; the other is a local path. Remote paths follow the same rules as elsewhere: relative paths resolve against `/workplace`. ```bash Command (cp) theme={null} # Upload a file into the sandbox bunny sandbox cp ./app.js my-sandbox:/workplace/app.js # Upload relative to /workplace bunny sandbox cp ./app.js my-sandbox:app.js # A trailing slash on the destination keeps the source filename bunny sandbox cp ./app.js my-sandbox:/workplace/src/ # Download a file from the sandbox bunny sandbox cp my-sandbox:/workplace/out.log ./out.log # Download into an existing directory (keeps the source filename) bunny sandbox cp my-sandbox:/workplace/out.log ./logs/ ``` ```text Response (cp) theme={null} Copied ./app.js -> my-sandbox:/workplace/app.js ``` Uploads preserve the local file's Unix mode (so executables stay executable). Only single files are supported; directory and sandbox-to-sandbox copies are not. ## `bunny sandbox files` Manage files inside a sandbox over SFTP. A bare sandbox name targets `/workplace`; use `:` for a specific directory (relative paths resolve against `/workplace`). ```bash Command (files) theme={null} # List /workplace bunny sandbox files list my-sandbox bunny sandbox files ls my-sandbox # alias # List a specific directory bunny sandbox files ls my-sandbox:/workplace/src bunny sandbox files ls my-sandbox:src # Machine-readable listing (name, type, size, mode) bunny sandbox files ls my-sandbox --output json ``` ```text Response (files) theme={null} Name Type Size Mode src directory 755 app.js file 1.2 KB 644 run.sh file 182 B 755 ``` To copy files in and out, use [`bunny sandbox cp`](#bunny-sandbox-cp). ## `bunny sandbox ssh` Open a full interactive SSH session. Drops you into a bash shell at `/workplace`. Type `exit` or press Ctrl-D to close. ```bash theme={null} bunny sandbox ssh my-sandbox # Set temporary environment variables for the session bunny sandbox ssh my-sandbox -e DEBUG=1 --env-file .env ``` | Flag | Alias | Description | Default | | ------------ | ----- | ------------------------------------------------ | ------- | | `--env` | `-e` | Environment variable as `KEY=VALUE` (repeatable) | | | `--env-file` | | Load environment variables from a dotenv file | | Variables apply only to the session and are not persisted. ## `bunny sandbox url` Manage public CDN endpoints for ports running inside a sandbox. Useful for exposing a dev server or API to the internet. ### `bunny sandbox url add` Expose a container port as a public HTTPS endpoint. Waits until the URL is provisioned and prints it. ```bash Command (url add) theme={null} # Expose port 3000 (endpoint named "port-3000") bunny sandbox url add my-sandbox 3000 # Custom endpoint name bunny sandbox url add my-sandbox 8080 --label my-api ``` ```text Response (url add) theme={null} Endpoint "port-3000" created. Port: 3000 URL: https://app-3000.b-cdn.net ``` | Flag | Description | Default | | --------- | ----------------------------- | ------------- | | `--label` | Display name for the endpoint | `port-` | ### `bunny sandbox url list` List all user-created endpoints for a sandbox (built-in `api` and `ssh` endpoints are hidden). ```bash Command (url list) theme={null} bunny sandbox url list my-sandbox bunny sandbox url ls my-sandbox # alias ``` ```text Response (url list) theme={null} ID Name Type Port URL Ab3xK9mPq2wR7nL-port-3000-Kj7mNp3qRx port-3000 cdn 3000 https://app-3000.b-cdn.net ``` ### `bunny sandbox url delete` Delete a public endpoint by name. ```bash Command (url delete) theme={null} bunny sandbox url delete my-sandbox port-3000 # Skip confirmation bunny sandbox url delete my-sandbox my-api --force bunny sandbox url rm my-sandbox my-api -f # alias ``` ```text Response (url delete) theme={null} Delete endpoint "port-3000" from sandbox "my-sandbox"? (y/N) y Endpoint "port-3000" deleted. ``` | Flag | Alias | Description | Default | | --------- | ----- | ------------------------ | ------- | | `--force` | `-f` | Skip confirmation prompt | `false` | ## `bunny sandbox env` Manage a sandbox's **persistent** environment variables, the ones baked into the container. Unlike the temporary `--env` passed to `exec`/`ssh`, these survive across sessions. Changing them redeploys the sandbox with the new environment (running processes restart). ### `bunny sandbox env set` Set one or more persistent variables, merging with the existing set. ```bash Command (env set) theme={null} # Set a single variable bunny sandbox env set my-sandbox NODE_ENV=production # Set several at once bunny sandbox env set my-sandbox API_URL=https://api.example.com LOG_LEVEL=debug # Load from a dotenv file bunny sandbox env set my-sandbox --env-file .env ``` ```text Response (env set) theme={null} Persisted 2 variable(s): API_URL, LOG_LEVEL The sandbox is redeploying to apply the change. ``` | Flag | Description | Default | | ------------ | --------------------------------------------- | ------- | | `--env-file` | Load environment variables from a dotenv file | | ### `bunny sandbox env list` List the sandbox's persistent variables. The internal `AGENT_TOKEN` is hidden. ```bash Command (env list) theme={null} bunny sandbox env list my-sandbox bunny sandbox env ls my-sandbox # alias ``` ```text Response (env list) theme={null} Name Value LOG_LEVEL debug NODE_ENV production ``` ### `bunny sandbox env delete` Remove one or more persistent variables. Names that are not set are reported and skipped; if none match, the command errors and nothing is redeployed. ```bash Command (env delete) theme={null} bunny sandbox env delete my-sandbox NODE_ENV bunny sandbox env rm my-sandbox API_URL LOG_LEVEL # alias bunny sandbox env unset my-sandbox API_URL # alias ``` ```text Response (env delete) theme={null} Removed 1 variable(s): NODE_ENV Not set (ignored): DEBUG The sandbox is redeploying to apply the change. ``` # bunny scripts Source: https://bunny.net/docs/cli/commands/scripts Scaffold, deploy, and manage Edge Scripts from the command line. `bunny scripts` provides a full workflow for [Edge Scripting](/docs/scripting): scaffold a project from a template, create and deploy scripts, manage deployments, environment variables, and custom domains, and link an existing script to a local project. Most subcommands default to the script linked in `.bunny/script.json`. Pass an ID (or `--id `) to target another script. ## `bunny scripts init` Create a new Edge Script project from a template. ```bash theme={null} # Interactive wizard bunny scripts init # Non-interactive, no GitHub Actions workflow bunny scripts init --name my-script --type standalone --template Empty --no-github-actions --deploy # Non-interactive, keep the GitHub Actions workflow bunny scripts init --name my-script --type standalone --template Empty --github-actions --deploy # Use a custom template repo (GitHub owner/repo shorthand) bunny scripts init --repo owner/my-template # Use a custom template repo (full git URL) bunny scripts init --template-repo https://github.com/owner/my-template ``` | Flag | Description | | --------------------------- | ------------------------------------------------------------------------------------ | | `--name` | Project directory name | | `--type` | Script type: `standalone` or `middleware` | | `--template` | Template name | | `--template-repo`, `--repo` | Git repository URL or GitHub `owner/repo` shorthand to use as template | | `--github-actions` | Keep the template's GitHub Actions workflow (use `--no-github-actions` to remove it) | | `--deploy` | Create script on bunny.net after scaffolding | | `--skip-git` | Skip git initialization | | `--skip-install` | Skip dependency installation | When `--repo` / `--template-repo` is given without `--type`, the script type defaults to `standalone`. With `--github-actions`, git is initialized automatically, the template's `.github/` workflow is kept, and after creating the script you'll be shown the `SCRIPT_ID` to add as a GitHub repo secret. With `--no-github-actions`, the `.github/` directory is removed and git init is prompted (or skipped via `--skip-git`). The `.changeset/` directory is always removed from the template. Bunny scripts don't use it. ## `bunny scripts create` Create a new Edge Script on bunny.net without scaffolding a project. Use this when you have an existing project, for example after running `bunny scripts init` without `--deploy`, and need a remote script before running `bunny scripts deploy`. ```bash theme={null} # Create using current directory name + link .bunny/script.json bunny scripts create # Explicit name and type bunny scripts create my-script --type middleware # Skip pull zone creation and directory linking bunny scripts create my-script --no-pull-zone --no-link ``` | Flag | Description | | ------------------ | ---------------------------------------------------------------------------------------- | | `--type` | Script type: `standalone` or `middleware` (defaults to manifest, prompts if interactive) | | `--pull-zone` | Create a linked pull zone (default: true). Use `--no-pull-zone` to skip. | | `--pull-zone-name` | Name for the linked pull zone | | `--link` | Link this directory to the new script (default: true). Use `--no-link` to skip. | ## `bunny scripts deploy` Deploy code to an Edge Script. Uploads code and publishes by default. ```bash theme={null} # Deploy and publish bunny scripts deploy dist/index.js # Deploy without publishing bunny scripts deploy dist/index.js --skip-publish # Deploy to a specific script bunny scripts deploy dist/index.js 12345 ``` | Flag | Description | | ---------------- | ------------------------------ | | `--skip-publish` | Upload code without publishing | After publishing, the live URL and any custom domains are printed. `bunny scripts deploy` works regardless of how the script was created or whether GitHub Actions is configured. The last deployment always wins, whether triggered by a GitHub Action or a manual CLI deploy. ## `bunny scripts link` Link the current directory to a remote Edge Script. Creates a `.bunny/script.json` manifest file. ```bash theme={null} # Interactive: select from list bunny scripts link # Non-interactive bunny scripts link --id ``` | Flag | Description | | ------ | ----------------------------- | | `--id` | Script ID to link (bypass UI) | ## `bunny scripts list` List all Edge Scripts. ```bash theme={null} bunny scripts list bunny scripts ls bunny scripts list --output json ``` ## `bunny scripts show` Show details for an Edge Script. Uses the linked script from `.bunny/script.json` if no ID is provided. Output includes the script's hostnames (system and custom) with their SSL status. ```bash theme={null} bunny scripts show bunny scripts show ``` ## `bunny scripts stats` Show usage statistics for an Edge Script: request, CPU, and cost totals over the period, plus a per-bucket requests-served bar chart in text mode (buckets are labelled with friendly UTC dates, e.g. `May 19, 2026`, or date + time with `--hourly`). Defaults to the last 30 days. When no ID is given, the command resolves the linked script from `.bunny/script.json`. If there is no link either, it prompts you to pick a script and offers to link the directory for next time. In `--output json` mode the picker is skipped and the command errors instead. Pass an ID or run `bunny scripts link` in CI. ```bash theme={null} bunny scripts stats bunny scripts stats 12345 --from 2026-05-01 --to 2026-05-31 bunny scripts stats 12345 --hourly bunny scripts stats 12345 --output json # Pick interactively without being asked to link (e.g. one-off checks) bunny scripts stats --no-link ``` | Flag | Description | | ---------- | ---------------------------------------------------------------------------------- | | `--from` | Start date (YYYY-MM-DD); defaults to 30 days ago | | `--to` | End date (YYYY-MM-DD); defaults to today | | `--hourly` | Group statistics by hour instead of by day | | `--link` | After an interactive pick, link the directory (use `--no-link` to skip the prompt) | ## `bunny scripts delete` Delete an Edge Script. Uses the linked script if no ID is provided. Requires double confirmation (or `--force` to skip). ```bash theme={null} bunny scripts delete bunny scripts delete bunny scripts delete --force ``` | Flag | Description | | --------- | ------------------------- | | `--force` | Skip confirmation prompts | ## `bunny scripts deployments` Manage Edge Script deployments. ### `bunny scripts deployments list` List deployments for an Edge Script. Uses the linked script if no ID is provided. ```bash theme={null} bunny scripts deployments list bunny scripts deployments ls bunny scripts deployments list bunny scripts deployments list --output json ``` ### `bunny scripts deployments publish` Publish (roll back to) a past deployment by its release ID, as shown in `deployments list`. `bunny scripts deploy` already uploads and publishes in one step; use this to re-publish an earlier release without touching the current code. Uses the linked script if no ID is provided. ```bash theme={null} bunny scripts deployments publish bunny scripts deployments publish bunny scripts deployments publish --force ``` | Flag | Description | | --------- | ---------------------------- | | `--force` | Skip the confirmation prompt | ## `bunny scripts env` Manage environment variables and secrets for an Edge Script. All subcommands default to the linked script; pass `--id ` to target another. ### `bunny scripts env list` List environment variables and secrets. ```bash theme={null} bunny scripts env list bunny scripts env ls bunny scripts env list --output json ``` ### `bunny scripts env set` Set an environment variable or secret. Runs interactively when arguments are omitted. The variable name is uppercased. ```bash theme={null} bunny scripts env set MY_VAR value bunny scripts env set # interactive bunny scripts env set API_KEY secret-value --secret ``` | Flag | Description | | ---------- | --------------------------------------- | | `--secret` | Store as an encrypted secret | | `--id` | Edge Script ID (uses linked if omitted) | ### `bunny scripts env remove` Remove an environment variable or secret. Shows an interactive picker when no name is given; prompts for confirmation unless `--force`. ```bash theme={null} bunny scripts env remove MY_VAR bunny scripts env rm MY_VAR -f ``` ### `bunny scripts env pull` Pull environment variables to a local `.env` file. ```bash theme={null} bunny scripts env pull bunny scripts env pull bunny scripts env pull --force ``` | Flag | Description | | --------- | ---------------------------------------------- | | `--force` | Overwrite an existing `.env` without prompting | ## `bunny scripts domains` Manage custom domains for an Edge Script. A script's domains live on its linked pull zone, so these commands operate on that pull zone. All subcommands default to the linked script; pass `--id ` to target another, and `--pull-zone ` when a script has more than one linked pull zone. ### `bunny scripts domains add` Add a custom domain. SSL is **not** requested by default. A free certificate can only be issued once your DNS points at bunny.net, so the command prints the `CNAME` record to create and the follow-up command to enable HTTPS. Pass `--ssl` to issue a certificate immediately; HTTP is redirected to HTTPS by default (opt out with `--no-force-ssl`). ```bash theme={null} # Add a domain and get DNS instructions bunny scripts domains add shop.example.com # Add and request SSL now (DNS must already be pointed at bunny.net). HTTPS forced bunny scripts domains add shop.example.com --ssl # Add and request SSL without forcing HTTPS bunny scripts domains add shop.example.com --ssl --no-force-ssl ``` | Flag | Description | | ---------------- | ----------------------------------------------------------------------- | | `--ssl` | Issue a free SSL certificate now and force HTTPS (requires DNS pointed) | | `--no-force-ssl` | When issuing SSL, keep serving HTTP instead of redirecting to HTTPS | | `--id` | Edge Script ID (uses linked script if omitted) | | `--pull-zone` | Pull zone ID (required if the script has multiple linked zones) | ### `bunny scripts domains ssl` Request a free SSL certificate for a custom domain. Run this after the domain's DNS points at bunny.net (see the `CNAME` printed by `domains add`). HTTP is redirected to HTTPS by default; pass `--no-force-ssl` to keep plain HTTP. ```bash theme={null} bunny scripts domains ssl shop.example.com bunny scripts domains ssl shop.example.com --no-force-ssl ``` ### `bunny scripts domains list` List the domains on a script's pull zone, with SSL and Force SSL status. ```bash theme={null} bunny scripts domains list bunny scripts domains ls bunny scripts domains list --output json ``` ### `bunny scripts domains remove` Remove a custom domain. System hostnames controlled by bunny.net cannot be removed. ```bash theme={null} bunny scripts domains remove shop.example.com bunny scripts domains remove shop.example.com --force ``` ## `bunny scripts docs` Open the Edge Scripts documentation in your browser. ```bash theme={null} bunny scripts docs ``` # Configuration Source: https://bunny.net/docs/cli/configuration Profiles, global options, output formats, and environment variables. ## Profiles The CLI supports multiple named configurations called **profiles**. Each profile stores its own API key, so you can switch between personal, staging, and production accounts. ```bash theme={null} # Default profile bunny login # Named profile bunny login --profile staging # Run any command against a profile bunny db list --profile staging ``` See [`bunny config`](/docs/cli/commands/config) for creating, inspecting, and deleting profiles. ## Global options These flags are available on every command. | Flag | Alias | Description | Default | | ----------- | ----- | ------------------------------------------------------------ | --------- | | `--profile` | `-p` | Configuration profile to use | `default` | | `--verbose` | `-v` | Enable verbose output | `false` | | `--output` | `-o` | Output format: `text`, `json`, `table`, `csv`, or `markdown` | `text` | | `--api-key` | | API key (takes priority over profile and environment) | | | `--version` | | Show version | | | `--help` | | Show help | | ## Output formats Use `--output` (or `-o`) to control how results are printed. This is particularly useful for scripting or piping into other tools. | Format | Description | | ---------- | ------------------------------------------------------------ | | `text` | Human-friendly borderless tables with bold headers (default) | | `json` | Structured JSON for scripting and piping | | `table` | Bordered ASCII table | | `csv` | Comma-separated values with proper escaping | | `markdown` | GitHub-flavored pipe tables | ```bash theme={null} bunny db list --output json bunny db list --output csv > databases.csv bunny db list --output markdown ``` ## Environment variables | Variable | Description | | ------------------------ | --------------------------------------------------------------- | | `BUNNYNET_API_KEY` | API key (overrides profile-based key) | | `BUNNYNET_API_URL` | API base URL (default: `https://api.bunny.net`) | | `BUNNYNET_DASHBOARD_URL` | Dashboard URL for auth flow (default: `https://dash.bunny.net`) | | `NO_COLOR` | Disable colored output ([no-color.org](https://no-color.org)) | ### Database-specific Some `bunny db` commands also read from a `.env` file walked up from the current directory: | Variable | Read by | | --------------------------- | --------------------------------------------------- | | `BUNNY_DATABASE_URL` | All `db` commands. Auto-detects the target database | | `BUNNY_DATABASE_AUTH_TOKEN` | `bunny db shell`. Skips on-demand token generation | Both variables can be written automatically by `bunny db create --save-env` or `bunny db quickstart`. ## Shell completion Generate a shell completion script with `bunny completion` and add the output to your shell profile to enable tab completion. ```bash theme={null} bunny completion >> ~/.zshrc ``` ## Resolving credentials When a command needs an API key, the CLI resolves it in this order: 1. `--api-key` flag 2. `BUNNYNET_API_KEY` environment variable 3. API key stored in the selected profile (`--profile` or `default`) If none of those are set, the command will prompt you to run `bunny login`. # Bunny CLI Source: https://bunny.net/docs/cli/index Manage bunny.net from your terminal The Bunny CLI is currently in [Public Preview](/docs/product-release-stages#stage-2-public-preview). Commands and flags may evolve during this period, [open an issue](https://github.com/BunnyWay/cli/issues) if you see something broken, or have suggestions to improve the experience. The Bunny CLI is a single binary (`bunny`) for managing bunny.net resources from your terminal. It's useful for scripting, CI/CD, local development workflows, and quickly inspecting resources without leaving your shell. Browse the source, follow releases, and report issues on the Bunny CLI repository ## Key features * **Single binary**: install via `curl` or `npm`, authenticate once, run anywhere * **Databases**: create, link, shell into, and inspect Bunny Databases from the command line * **Edge Scripts**: scaffold, deploy, and manage Edge Scripts, their deployments, environment variables, and custom domains * **Raw API access**: `bunny api` lets you hit any bunny.net endpoint with auth handled automatically * **Profiles**: switch between multiple accounts (personal, staging, production) with `--profile` * **Scriptable output**: `--output json` / `csv` / `markdown` for pipelines, CI, and LLMs ## Getting started Install the CLI, log in, and run your first command curl installer and npm options Profiles, environment variables, output formats, and global options `bunny login`, `bunny logout`, and `bunny whoami` ## Commands Create and manage Bunny Databases, open an interactive SQL shell, and generate auth tokens Scaffold, deploy, and manage Edge Scripts, deployments, env vars, and custom domains Make raw authenticated requests to any bunny.net API endpoint Manage CLI configuration and named profiles Browser-based or API key authentication # Installation Source: https://bunny.net/docs/cli/installation Install the Bunny CLI via shell installer or npm. ## curl Download a prebuilt binary for your platform and add it to your `PATH`: ```bash theme={null} curl -fsSL https://cli.bunny.net/install.sh | sh ``` `install.sh` is a POSIX shell script, so piping to `sh`, `bash`, or `zsh` all work. ## npm If you already use Node.js, install globally from npm: ```bash theme={null} npm install -g @bunny.net/cli ``` ## Verify your installation ```bash theme={null} bunny ``` ## Next steps Log in and create your first database `bunny login`, `bunny logout`, `bunny whoami` # Quickstart Source: https://bunny.net/docs/cli/quickstart Install the Bunny CLI, authenticate, and start managing your bunny.net resources. Install `bunny` using the shell installer or npm: ```bash theme={null} curl -fsSL https://cli.bunny.net/install.sh | sh ``` The installer downloads a prebuilt binary for your platform and places it on your `PATH`. Works with `sh`, `bash`, or `zsh`. ```bash theme={null} npm install -g @bunny.net/cli ``` See [Installation](/docs/cli/installation) for more options. Log in with your bunny.net account: ```bash theme={null} bunny login ``` Opens your browser to complete sign-in. Credentials are stored in your CLI config under the `default` profile. ```bash theme={null} bunny config init --api-key bny_xxxxxxxxxxxx ``` Set up a profile directly from a bunny.net [API key](/docs/account/api-keys). Confirm the CLI is authenticated with the expected account: ```bash theme={null} bunny whoami # Logged in as Jamie Barton (jamie@bunny.net) 🐇 # Profile: default # Account ID: 3d5a9c1e-7f42-4b8a-9c36-2e8d1f6a4b70 ``` If you manage multiple accounts, see [profiles](/docs/cli/configuration#profiles) for switching between them with `--profile`. With the CLI authenticated, you can manage any bunny.net resource from your terminal: Create and manage Bunny Databases, open an interactive SQL shell, and generate auth tokens Scaffold, deploy, and manage Edge Scripts and their custom domains Make raw authenticated requests to any bunny.net API endpoint Manage CLI configuration and named profiles # Changelog Source: https://bunny.net/docs/database/changelog Latest updates and improvements to Bunny Database. ## Bunny CLI Manage Bunny Database from your terminal with the new [Bunny CLI](/docs/cli). Create databases, open an interactive SQL shell with `bunny db shell`, generate auth tokens, and browse tables with `bunny db studio`. [Learn more](/docs/cli/commands/db) ## Database Shell The Bunny Database Shell (`bsql`) is a standalone, interactive SQL shell for querying and managing your database from the terminal. It supports dot-commands, multiple output formats, and persistent history. [Read the announcement](https://bunny.net/blog/introducing-the-interactive-bunny-database-shell/) or [learn more](/docs/database/connect/database-shell). ## Public Preview Bunny Database is now available in [Public Preview](/docs/product-release-stages#stage-2-public-preview). A globally distributed, SQLite-compatible database service that handles scaling and replication automatically. [Read the announcement](https://bunny.net/blog/meet-bunny-database-the-sql-service-that-just-works/). # Auth & Access Source: https://bunny.net/docs/database/connect/authorization Manage database URLs and access tokens for authentication To connect to your Bunny Database, you'll need your **Database URL** and an **Access Token**. Both can be obtained and managed from the Dashboard or the [Bunny CLI](/docs/cli/quickstart). ## Accessing credentials Navigate to **Dashboard > Edge Platform > Database > \[Select Database] > Access** to view and manage your database connection details. Database Access From this page, you can: * View your Database URL * Generate new access tokens * Regenerate all access tokens * Copy or download existing tokens ## Database URL Your Database URL is the endpoint used to connect to your database. It follows this format: ```bash theme={null} libsql://[your-database-id].lite.bunnydb.net ``` This URL is required by all client libraries and SDKs when establishing a connection to your database. ## Access tokens Access tokens authenticate your requests to the database. Bunny Database provides two types of tokens: * **Full Access**: Read and write permissions for all database operations * **Read Only**: Limited to SELECT queries and read operations only ### Generating new tokens To create a new access token for an additional application while keeping existing tokens valid: 1. Navigate to **Dashboard > Edge Platform > Database > \[Select Database] > Access** 2. Click **Generate Tokens** 3. New full-access and read-only tokens will be created 4. Copy or download your tokens immediately Tokens are only displayed once. If you lose them, you'll need to generate another. Don't have the CLI installed? See the [CLI quickstart](/docs/cli/quickstart) to install and authenticate. Generate a full-access token for the linked database: ```bash theme={null} bunny db tokens create ``` Generate a read-only token that expires after 30 days: ```bash theme={null} bunny db tokens create --read-only --expiry 30d ``` After generation, the CLI offers to save `BUNNY_DATABASE_AUTH_TOKEN` (and `BUNNY_DATABASE_URL` if missing) to your `.env` file. Pass `--no-save` to skip the prompt, or target a specific database by passing its ID: ```bash theme={null} bunny db tokens create db_01KCHBG8C5KSFGG0VRNFQ7EK7X ``` See [`bunny db`](/docs/cli/commands/db) for the full command reference. ### Regenerating tokens If your tokens are exposed or compromised, regenerate them to invalidate all existing tokens: 1. Navigate to **Dashboard > Edge Platform > Database > \[Select Database] > Access** 2. Click **Regenerate Tokens** 3. All previous tokens will be immediately invalidated 4. New full-access and read-only tokens will be created 5. Copy or download your new tokens immediately Regenerating tokens will invalidate **all** existing tokens for all databases. Update all applications using the old tokens to prevent connection failures. Invalidate every existing token for the database: ```bash theme={null} bunny db tokens invalidate ``` This is destructive, so the CLI asks for confirmation first. Use `--force` to skip the prompt in automated environments. To invalidate the old tokens and immediately create a replacement in one step: ```bash theme={null} bunny db tokens invalidate --regenerate --save-env ``` `--save-env` writes the replacement token to your `.env` file (requires `--regenerate`). Invalidating tokens revokes **all** existing tokens for the database. Update all applications using the old tokens to prevent connection failures. ## Using credentials with client libraries Pass your Database URL and access token to the client library when creating a connection: ```ts TypeScript highlight={5} theme={null} import { createClient } from "@libsql/client/web"; const client = createClient({ url: "libsql://[your-database-id].lite.bunnydb.net", authToken: "your-access-token", }); ``` ```rust Rust highlight={5} theme={null} use libsql::Builder; let db = Builder::new_remote( "libsql://[your-database-id].lite.bunnydb.net".to_string(), "your-access-token".to_string(), ) .build() .await?; ``` ```go Go highlight={3} theme={null} import _ "github.com/tursodatabase/libsql-client-go/libsql" url := "libsql://[your-database-id].lite.bunnydb.net?authToken=your-access-token" db, err := sql.Open("libsql", url) ``` ```csharp .NET highlight={6} theme={null} using Libsql.Client; var client = DatabaseClient.Create(opts => { opts.Url = "libsql://[your-database-id].lite.bunnydb.net"; opts.AuthToken = "your-access-token"; }); ``` ## Using credentials with HTTP API When making direct HTTP requests to the database API endpoint (`/v2/pipeline`), include your access token as a Bearer token in the Authorization header: ```bash highlight={2} theme={null} curl -X POST https://[your-database-id].lite.bunnydb.net/v2/pipeline \ -H "Authorization: Bearer your-access-token" \ -H "Content-Type: application/json" \ -d '{ "requests": [ { "type": "execute", "stmt": { "sql": "SELECT * FROM users" } } ] }' ``` ## Using credentials with Magic Containers and Edge Scripting When you add database credentials to a [Magic Container](/docs/database/connect/magic-containers) or [Edge Script](/docs/database/connect/scripting), they are automatically available as environment variables: * `BUNNY_DATABASE_URL`: Your database URL * `BUNNY_DATABASE_AUTH_TOKEN`: Your access token Access them in your code like any other environment variable: ```bash theme={null} process.env.BUNNY_DATABASE_URL ``` # Database Shell Source: https://bunny.net/docs/database/connect/database-shell Connect to Bunny Database using the interactive SQL shell bunny.net Database Shell The Bunny Database Shell (`bsql`) is a standalone, interactive SQL shell for querying and managing your database from the terminal. It supports dot-commands, multiple output formats, sensitive column masking, and persistent history. In this quickstart you will learn how to: * Install the Database Shell * Connect to a remote Bunny Database * Execute queries interactively and non-interactively Bunny Database is currently in [Public Preview](/docs/product-release-stages#stage-2-public-preview). Features and APIs may evolve during this period. Already using the [Bunny CLI](/docs/cli/quickstart)? The same shell is built in as `bunny db shell`, which resolves your database and credentials automatically: ```bash theme={null} bunny db shell # interactive REPL bunny db shell -e "SELECT * FROM users LIMIT 10" # execute and exit ``` See [`bunny db`](/docs/cli/commands/db) for the full command reference. ## Quickstart You will need an existing database to continue. If you don't have one, [create one](/docs/database/quickstart). Navigate to **Dashboard > Edge Platform > Database > \[Select Database] > Access** to find your database URL and generate an access token. You should store these as environment variables to keep them secure. Install the Database Shell globally using npm: ```bash npm theme={null} npm install -g @bunny.net/database-shell ``` ```bash pnpm theme={null} pnpm add -g @bunny.net/database-shell ``` ```bash yarn theme={null} yarn global add @bunny.net/database-shell ``` Start an interactive shell session: ```bash theme={null} bsql libsql://.lite.bunnydb.net --token ``` If you store your credentials in a `.env` file as `BUNNY_DATABASE_URL` and `BUNNY_DATABASE_AUTH_TOKEN`, you can connect without passing any flags: ```bash theme={null} bsql ``` See [Authorization](/docs/database/connect/authorization) for more on environment variables. Run a SQL query directly in the shell: ```sql theme={null} SELECT * FROM users; ``` You can also execute a query without entering interactive mode: ```bash theme={null} bsql libsql://.lite.bunnydb.net --token "SELECT * FROM users" ``` ## CLI Flags | Flag | Description | | ----------------- | ---------------------------------------------------------- | | `--token ` | Auth token for the database | | `--mode ` | Output mode: `default`, `table`, `json`, `csv`, `markdown` | | `--unmask` | Show sensitive column values unmasked | | `--timing` | Show query execution timing | | `--help` | Show help | ## Execute a SQL file You can execute a `.sql` file directly from the command line: ```bash theme={null} bsql libsql://.lite.bunnydb.net --token seed.sql ``` ## Output modes Change the output format using the `--mode` flag or the `.mode` dot-command inside an interactive session: | Mode | Description | | ---------- | ----------------------------- | | `default` | Borderless table with headers | | `table` | Bordered ASCII table | | `json` | JSON array of row objects | | `csv` | Comma-separated values | | `markdown` | GitHub-flavored pipe table | ```bash theme={null} bsql libsql://.lite.bunnydb.net --token "SELECT * FROM users" --mode json ``` ## Dot-commands The following dot-commands are available inside an interactive session: | Command | Description | | ------------------ | ----------------------------------- | | `.tables` | List all tables | | `.describe TABLE` | Show column details | | `.schema [TABLE]` | Show CREATE statements | | `.indexes [TABLE]` | List indexes | | `.fk TABLE` | Show foreign keys for a table | | `.er` | Show entity-relationship overview | | `.count TABLE` | Count rows | | `.size TABLE` | Show table stats | | `.truncate TABLE` | Delete all rows from a table | | `.dump [TABLE]` | Dump schema and data as SQL | | `.read FILE` | Execute SQL from a file | | `.mode [MODE]` | Set output mode | | `.timing` | Toggle query timing | | `.mask` | Enable sensitive column masking | | `.unmask` | Disable sensitive column masking | | `.save NAME` | Save the last query as a named view | | `.view NAME` | Execute a saved view | | `.views` | List all saved views | | `.unsave NAME` | Delete a saved view | | `.clear-history` | Clear command history | | `.help` | Show available commands | | `.quit` / `.exit` | Exit the shell | ## Sensitive column masking Columns matching patterns like `password`, `secret`, `api_key`, `auth_token`, and `ssn` are masked by default. Email columns are partially masked (e.g. `a****e@example.com`). Toggle masking with `.mask` / `.unmask` in interactive mode, or pass `--unmask` as a CLI flag. ## Saved views Save frequently used queries as named views to recall them later: ```sql theme={null} → SELECT name, count(*) as orders FROM users JOIN orders USING (user_id) GROUP BY name ORDER BY orders DESC LIMIT 10; → .save top-customers ✓ View "top-customers" saved. → .view top-customers ``` Manage saved views with `.views`, `.view NAME`, and `.unsave NAME`. ## Browse tables in your browser If you have the [Bunny CLI](/docs/cli/quickstart) installed, `bunny db studio` opens a visual database explorer in your browser for the resolved database: ```bash theme={null} bunny db studio # auto-detect database and open the browser bunny db studio --port 3000 # use a custom port bunny db studio --no-open # start the server without opening a browser ``` `bunny db studio` is experimental and provides a view-only table browser. Use `bunny db shell` to run queries or modify data. Like `bunny db shell`, it resolves the database and credentials automatically, generating a short-lived token when one isn't already available. The server runs until you stop it with `Ctrl+C`. See [`bunny db`](/docs/cli/commands/db) for the full command reference. # .NET Source: https://bunny.net/docs/database/connect/dotnet Get started with Bunny Database and .NET using the Bunny.LibSQL.Client In this .NET quickstart you will learn how to: * Retrieve database credentials * Install the Bunny.LibSQL.Client package * Connect to a remote Bunny Database * Define models and run migrations * Execute queries using LINQ While foundational ORM and querying features are available, several enhancements are still in progress. You can report issues, contribute, or learn more on [GitHub](https://github.com/BunnyWay/bunny-libsql-client). ## Quickstart You will need an existing database to continue. If you don't have one, [create one](/docs/database/quickstart). Navigate to **Dashboard > Edge Platform > Database > \[Select Database] > Access** to find your database URL and generate an access token. You should store these as environment variables to keep them secure. Install the package via NuGet: ```bash theme={null} dotnet add package Bunny.LibSql.Client ``` Create a database context class that inherits from `LibSqlDbContext`: ```csharp theme={null} public class AppDb : LibSqlDbContext { public AppDb(string dbUrl, string accessKey) : base(new LibSqlClient(dbUrl, accessKey)) {} public LibSqlTable Users { get; set; } } ``` Create model classes with attributes to define the table structure: ```csharp theme={null} [Table("Users")] public class User { [Key] public int id { get; set; } [Index] public string name { get; set; } public string email { get; set; } } ``` Create an instance of your database context and apply migrations: ```csharp theme={null} var db = new AppDb( Environment.GetEnvironmentVariable("BUNNY_DATABASE_URL"), Environment.GetEnvironmentVariable("BUNNY_DATABASE_AUTH_TOKEN") ); await db.ApplyMigrationsAsync(); ``` You can query your database using LINQ: ```csharp theme={null} var users = await db.Users.ToListAsync(); foreach (var user in users) { Console.WriteLine($"User: {user.name}"); } ``` ## Managing Records ### Insert Insert records using `InsertAsync`: ```csharp theme={null} await db.Users.InsertAsync(new User { id = 1, name = "Kit", email = "kit@example.com" }); ``` ### Update Update records using `UpdateAsync`: ```csharp theme={null} var user = await db.Users.Where(u => u.id == 1).FirstOrDefaultAsync(); user.name = "Kit Updated"; await db.Users.UpdateAsync(user); ``` ### Delete Delete records using `DeleteAsync`: ```csharp theme={null} var user = await db.Users.Where(u => u.id == 1).FirstOrDefaultAsync(); await db.Users.DeleteAsync(user); ``` ## Querying with LINQ ### Basic Query ```csharp theme={null} var users = await db.Users .Where(u => u.name.StartsWith("K")) .ToListAsync(); ``` ### Eager Loading with Include Load related entities using `Include()`: ```csharp theme={null} var usersWithOrders = await db.Users .Include(u => u.Orders) .ToListAsync(); ``` ### Aggregates Perform aggregate queries with `CountAsync()` and `SumAsync()`: ```csharp theme={null} var userCount = await db.Users.CountAsync(); var totalPrice = await db.Orders.SumAsync(o => o.price); ``` Always use the `Async` variants like `ToListAsync()`, `CountAsync()`, and `SumAsync()` to execute queries. Skipping the async call will not run the query. ## Transactions Use transactions to group multiple operations together: ```csharp theme={null} await db.Client.BeginTransactionAsync(); try { await db.Users.InsertAsync(new User { name = "Kit", email = "kit@example.com" }); await db.Users.InsertAsync(new User { name = "Sam", email = "sam@example.com" }); await db.Client.CommitTransactionAsync(); } catch { await db.Client.RollbackTransactionAsync(); throw; } ``` ## Direct SQL Queries For raw SQL access, use the underlying client directly. ### Run a command ```csharp theme={null} await db.Client.QueryAsync("DELETE FROM Users WHERE id = 1"); ``` ### Get a scalar value ```csharp theme={null} var count = await db.Client.QueryScalarAsync("SELECT COUNT(*) FROM Users"); ``` ## Model Attributes | Attribute | Description | | ------------- | --------------------------------------------------------------------------------- | | `Table` | Specifies a custom table name for the entity. If omitted, class name is used. | | `Key` | Marks the property as the primary key of the table. | | `Index` | Creates an index on the annotated property for faster lookups. | | `ForeignKey` | Defines a relationship to another table by specifying the foreign key property. | | `AutoInclude` | Enables eager loading of the related property automatically during queries. | | `Unique` | Marks the field with the UNIQUE constraint, ensuring a unique value in every row. | | `ManyToMany` | Defines a many-to-many relationship through a join table. | ## Supported Data Types | C# Type | Description | SQLite Type | | ---------- | ------------------------------- | -------------------------- | | `string` | Textual data | `TEXT` | | `int` | 32-bit integer | `INTEGER` | | `long` | 64-bit integer | `INTEGER` | | `double` | Double-precision floating point | `REAL` | | `float` | Single-precision floating point | `REAL` | | `decimal` | Decimal number | `REAL` | | `DateTime` | Date and time | `INTEGER` (UNIX timestamp) | | `bool` | Boolean value | `INTEGER` (`0` or `1`) | | `byte[]` | Binary data | `BLOB` | | `F32Blob` | Vector F32 blob (AI embeddings) | `F32_BLOB` | Nullable variants (e.g., `int?`, `bool?`) are also supported and will map to nullable columns. ## Relationships Define relationships between models using attributes: ```csharp theme={null} [Table("Users")] public class User { [Key] public int id { get; set; } public string name { get; set; } [AutoInclude] public List Orders { get; set; } = new(); } [Table("Orders")] public class Order { [Key] public int id { get; set; } [ForeignKeyFor(typeof(User))] public int user_id { get; set; } public decimal price { get; set; } } ``` Query with relationships: ```csharp theme={null} var users = await db.Users .Include(u => u.Orders) .ToListAsync(); foreach (var user in users) { Console.WriteLine($"User: {user.name}"); foreach (var order in user.Orders) { Console.WriteLine($" Order: {order.price}"); } } ``` # Go Source: https://bunny.net/docs/database/connect/go Get started with Bunny Database and Go using the libSQL client In this Go quickstart you will learn how to: * Retrieve database credentials * Install the libSQL client * Connect to a remote Bunny Database * Execute a query using SQL ## Quickstart You will need an existing database to continue. If you don't have one, [create one](/docs/database/quickstart). Navigate to **Dashboard > Edge Platform > Database > \[Select Database] > Access** to find your database URL and generate an access token. You should store these as environment variables to keep them secure. Install the libSQL client package: ```bash theme={null} go get github.com/tursodatabase/libsql-client-go/libsql ``` Create a database connection with your database URL and auth token: ```go theme={null} package main import ( "database/sql" "fmt" "os" _ "github.com/tursodatabase/libsql-client-go/libsql" ) func main() { url := fmt.Sprintf("%s?authToken=%s", os.Getenv("BUNNY_DATABASE_URL"), os.Getenv("BUNNY_DATABASE_AUTH_TOKEN"), ) db, err := sql.Open("libsql", url) if err != nil { fmt.Fprintf(os.Stderr, "failed to open db %s: %s", url, err) os.Exit(1) } defer db.Close() } ``` You can execute a SQL query against your database using `Query()` for reads or `Exec()` for writes: ```go theme={null} rows, err := db.Query("SELECT * FROM users") if err != nil { fmt.Fprintf(os.Stderr, "failed to execute query: %v\n", err) os.Exit(1) } defer rows.Close() for rows.Next() { var id int var name string if err := rows.Scan(&id, &name); err != nil { fmt.Fprintf(os.Stderr, "failed to scan row: %v\n", err) os.Exit(1) } fmt.Printf("id: %d, name: %s\n", id, name) } if err := rows.Err(); err != nil { fmt.Fprintf(os.Stderr, "error during iteration: %v\n", err) os.Exit(1) } ``` If you need to use placeholders for values, you can do that: ```go Positional theme={null} rows, err := db.Query("SELECT * FROM users WHERE id = ?", 1) ``` ```go Named theme={null} rows, err := db.Query("SELECT * FROM users WHERE id = :id", sql.Named("id", 1)) ``` ## Placeholders libSQL supports the use of positional and named placeholders within SQL statements: ```go Positional theme={null} rows, err := db.Query("SELECT * FROM users WHERE id = ?", 1) ``` ```go Named theme={null} rows, err := db.Query("SELECT * FROM users WHERE id = :id", sql.Named("id", 1)) ``` libSQL supports the same named placeholder characters as SQLite — `:`, `@` and `$`. ## Executing Writes Use `Exec()` for INSERT, UPDATE, and DELETE statements: ```go theme={null} result, err := db.Exec("INSERT INTO users (name) VALUES (?)", "Kit") if err != nil { fmt.Fprintf(os.Stderr, "failed to insert: %v\n", err) os.Exit(1) } rowsAffected, _ := result.RowsAffected() lastInsertId, _ := result.LastInsertId() fmt.Printf("rows affected: %d, last insert id: %d\n", rowsAffected, lastInsertId) ``` ## Transactions You can use transactions to execute multiple statements atomically: ```go theme={null} tx, err := db.Begin() if err != nil { fmt.Fprintf(os.Stderr, "failed to begin transaction: %v\n", err) os.Exit(1) } _, err = tx.Exec("INSERT INTO users (name) VALUES (?)", "Kit") if err != nil { tx.Rollback() fmt.Fprintf(os.Stderr, "failed to insert: %v\n", err) os.Exit(1) } _, err = tx.Exec("INSERT INTO users (name) VALUES (?)", "Sam") if err != nil { tx.Rollback() fmt.Fprintf(os.Stderr, "failed to insert: %v\n", err) os.Exit(1) } if err := tx.Commit(); err != nil { fmt.Fprintf(os.Stderr, "failed to commit: %v\n", err) os.Exit(1) } ``` ## Prepared Statements For repeated queries, you can use prepared statements for better performance: ```go theme={null} stmt, err := db.Prepare("SELECT * FROM users WHERE id = ?") if err != nil { fmt.Fprintf(os.Stderr, "failed to prepare statement: %v\n", err) os.Exit(1) } defer stmt.Close() rows, err := stmt.Query(1) if err != nil { fmt.Fprintf(os.Stderr, "failed to execute query: %v\n", err) os.Exit(1) } defer rows.Close() ``` ## Context Support The libSQL client supports Go's context for timeout and cancellation: ```go theme={null} ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() rows, err := db.QueryContext(ctx, "SELECT * FROM users") if err != nil { fmt.Fprintf(os.Stderr, "failed to execute query: %v\n", err) os.Exit(1) } defer rows.Close() ``` # Bunny Magic Containers Source: https://bunny.net/docs/database/connect/magic-containers Connect your Magic Container apps to Bunny Database using environment variables You can connect [Magic Container](/docs/magic-containers) apps to your database by adding credentials as environment variables directly from the database dashboard. Navigate to **Dashboard > Edge Platform > Database > \[Select Database] > Access**. Click **Generate Tokens** to create new access credentials for your database. Generate Tokens Once the tokens are generated, click **Add Secrets to Magic Container App**. Add to Magic
Container Choose the Magic Container app you want to connect to your database. Select App The database URL and access token are now available as environment variables in your app. Use them to connect to your database: ```typescript theme={null} import { createClient } from "@libsql/client/web"; const client = createClient({ url: process.env.BUNNY_DATABASE_URL, authToken: process.env.BUNNY_DATABASE_AUTH_TOKEN, }); const result = await client.execute("SELECT * FROM users"); ``` # Rust Source: https://bunny.net/docs/database/connect/rust Get started with Bunny Database and Rust using the libSQL crate In this Rust quickstart you will learn how to: * Retrieve database credentials * Install the libSQL crate * Connect to a remote Bunny Database * Execute a query using SQL ## Quickstart You will need an existing database to continue. If you don't have one, [create one](/docs/database/quickstart). Navigate to **Dashboard > Edge Platform > Database > \[Select Database] > Access** to find your database URL and generate an access token. You should store these as environment variables to keep them secure. Install the libSQL crate: ```bash theme={null} cargo add libsql ``` Create a database connection with your database URL and auth token: ```rust theme={null} use libsql::Builder; let url = std::env::var("BUNNY_DATABASE_URL").expect("BUNNY_DATABASE_URL must be set"); let token = std::env::var("BUNNY_DATABASE_AUTH_TOKEN").expect("BUNNY_DATABASE_AUTH_TOKEN must be set"); let db = Builder::new_remote(url, token) .build() .await?; let conn = db.connect()?; ``` You can execute a SQL query against your database using `query()` for reads or `execute()` for writes: ```rust theme={null} let mut rows = conn.query("SELECT * FROM users", ()).await?; while let Some(row) = rows.next().await? { let id: i64 = row.get(0)?; let name: String = row.get(1)?; println!("User: {} - {}", id, name); } ``` If you need to use placeholders for values, you can do that: ```rust Positional theme={null} conn.query("SELECT * FROM users WHERE id = ?1", libsql::params![1]).await?; ``` ```rust Named theme={null} conn.query("SELECT * FROM users WHERE id = :id", libsql::named_params! { ":id": 1 }).await?; ``` ## Placeholders libSQL supports the use of positional and named placeholders within SQL statements: ```rust Positional theme={null} conn.query("SELECT * FROM users WHERE id = ?1", libsql::params![1]).await?; ``` ```rust Named theme={null} conn.query("SELECT * FROM users WHERE id = :id", libsql::named_params! { ":id": 1 }).await?; ``` libSQL supports the same named placeholder characters as SQLite — `:`, `@` and `$`. ## Executing Writes Use `execute()` for INSERT, UPDATE, and DELETE statements: ```rust theme={null} conn.execute("INSERT INTO users (name) VALUES (?1)", libsql::params!["Kit"]).await?; ``` You can also use named parameters: ```rust theme={null} conn.execute( "INSERT INTO users (name) VALUES (:name)", libsql::named_params! { ":name": "Kit" } ).await?; ``` ## Transactions You can use transactions to execute multiple statements atomically: ```rust theme={null} let tx = conn.transaction().await?; tx.execute("INSERT INTO users (name) VALUES (?1)", libsql::params!["Kit"]).await?; tx.execute("INSERT INTO users (name) VALUES (?1)", libsql::params!["Sam"]).await?; tx.commit().await?; ``` To roll back a transaction: ```rust theme={null} let tx = conn.transaction().await?; tx.execute("INSERT INTO users (name) VALUES (?1)", libsql::params!["Kit"]).await?; // Roll back the transaction tx.rollback().await?; ``` ## Batch Execution You can execute multiple SQL statements in a single call: ```rust theme={null} conn.execute_batch( "BEGIN; INSERT INTO users (name) VALUES ('Kit'); INSERT INTO users (name) VALUES ('Sam'); COMMIT;" ).await?; ``` # Bunny Edge Scripting Source: https://bunny.net/docs/database/connect/scripting Connect your Edge Scripts to Bunny Database using environment variables You can connect [Edge Scripts](/docs/scripting) to your database by adding credentials as environment variables directly from the database dashboard. Navigate to **Dashboard > Edge Platform > Database > \[Select Database] > Access**. Click **Generate Tokens** to create new access credentials for your database. Generate Tokens Once the tokens are generated, click **Add Secrets to Edge Script**. Add to
Script Choose the Edge Script you want to connect to your database. Select Script The database URL and access token are now available as environment variables in your script. Use them to connect to your database: ```typescript theme={null} import { createClient } from "@libsql/client/web"; import process from "node:process"; const client = createClient({ url: process.env.BUNNY_DATABASE_URL, authToken: process.env.BUNNY_DATABASE_AUTH_TOKEN, }); const result = await client.execute("SELECT * FROM users"); ``` # SQL API Source: https://bunny.net/docs/database/connect/sql-api Execute SQL queries over HTTP using the libSQL remote protocol The SQL API is currently in beta. APIs and behavior may change. The SQL API allows you to execute SQL queries directly over HTTP without using an SDK. This is useful for environments where an SDK isn't available, or when you need to make simple queries from any HTTP client. Bunny Database uses the [libSQL remote protocol](https://github.com/tursodatabase/libsql/blob/main/docs/HRANA_3_SPEC.md#hrana-over-http) (Hrana over HTTP) for its SQL API. ## Quickstart Your Database URL can be found in the Dashboard under **Edge Platform > Database > \[Select Database] > Access**. The HTTP endpoint follows this format: ``` https://[your-database-id].lite.bunnydb.net/v2/pipeline ``` Note the `/v2/pipeline` path — this is the endpoint that accepts SQL requests. You'll need an access token to authenticate requests. Generate one from the same Access page in the Dashboard, or see [Database Access](/docs/database/connect/authorization) for details. Send a POST request to the pipeline endpoint with your SQL statement: ```bash cURL theme={null} curl -X POST https://[your-database-id].lite.bunnydb.net/v2/pipeline \ -H "Authorization: Bearer your-access-token" \ -H "Content-Type: application/json" \ -d '{ "requests": [ { "type": "execute", "stmt": { "sql": "SELECT * FROM users" } }, { "type": "close" } ] }' ``` ```ts JavaScript theme={null} const url = "https://[your-database-id].lite.bunnydb.net/v2/pipeline"; const authToken = "your-access-token"; const response = await fetch(url, { method: "POST", headers: { Authorization: `Bearer ${authToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ requests: [ { type: "execute", stmt: { sql: "SELECT * FROM users" } }, { type: "close" }, ], }), }); const data = await response.json(); console.log(data); ``` ```python Python theme={null} import requests url = "https://[your-database-id].lite.bunnydb.net/v2/pipeline" auth_token = "your-access-token" response = requests.post( url, headers={ "Authorization": f"Bearer {auth_token}", "Content-Type": "application/json", }, json={ "requests": [ {"type": "execute", "stmt": {"sql": "SELECT * FROM users"}}, {"type": "close"}, ] }, ) print(response.json()) ``` ## Request format Each request to `/v2/pipeline` contains an array of requests to execute. A typical request includes an `execute` statement followed by a `close`: ```json theme={null} { "requests": [ { "type": "execute", "stmt": { "sql": "SELECT * FROM users" } }, { "type": "close" } ] } ``` ## Response format The response contains a `results` array with the outcome of each request: ```json theme={null} { "baton": null, "base_url": null, "results": [ { "type": "ok", "response": { "type": "execute", "result": { "cols": [ { "name": "id", "decltype": "INTEGER" }, { "name": "name", "decltype": "TEXT" } ], "rows": [ [ { "type": "integer", "value": "1" }, { "type": "text", "value": "Kit" } ] ], "affected_row_count": 0, "last_insert_rowid": null, "replication_index": "1" } } }, { "type": "ok", "response": { "type": "close" } } ] } ``` ## Parameter binding Use parameter binding to safely pass values to your queries. This helps prevent SQL injection and handles proper escaping. ### Positional parameters Use `?` placeholders and provide values in the `args` array: ```json theme={null} { "requests": [ { "type": "execute", "stmt": { "sql": "SELECT * FROM users WHERE id = ?", "args": [{ "type": "integer", "value": "1" }] } }, { "type": "close" } ] } ``` ### Named parameters Use `:name`, `$name`, or `@name` placeholders with `named_args`: ```json theme={null} { "requests": [ { "type": "execute", "stmt": { "sql": "SELECT * FROM users WHERE name = :name", "named_args": [ { "name": "name", "value": { "type": "text", "value": "Kit" } } ] } }, { "type": "close" } ] } ``` ## Value types The `type` field in parameter values must be one of: | Type | Description | | --------- | ---------------------------- | | `null` | NULL value | | `integer` | 64-bit signed integer | | `float` | 64-bit floating point | | `text` | UTF-8 string | | `blob` | Binary data (base64 encoded) | Values are passed as strings in JSON to avoid precision loss, since some JSON implementations treat all numbers as 64-bit floats. ## Multiple statements You can execute multiple statements in a single request: ```json theme={null} { "requests": [ { "type": "execute", "stmt": { "sql": "INSERT INTO users (name) VALUES ('Kit')" } }, { "type": "execute", "stmt": { "sql": "SELECT * FROM users" } }, { "type": "close" } ] } ``` Each statement executes in order, and the results array contains the response for each. ## Interactive sessions For most use cases, executing statements with a `close` request in a single HTTP call is sufficient. The API also supports interactive sessions using a `baton`, a token returned in responses that allows you to maintain state across multiple HTTP requests. This is useful for advanced scenarios like long-running transactions that span multiple roundtrips. If you have a use case that requires interactive sessions with batons, [contact us](https://bunny.net/contact) to discuss your requirements. # TypeScript Source: https://bunny.net/docs/database/connect/typescript Get started with Bunny Database and TypeScript using the libSQL client In this TypeScript quickstart you will learn how to: * Retrieve database credentials * Install the libSQL client * Connect to a remote Bunny Database * Execute a query using SQL ## Quickstart You will need an existing database to continue. If you don't have one, [create one](/docs/database/quickstart). Navigate to **Dashboard > Edge Platform > Database > \[Select Database] > Access** to find your database URL and generate an access token. You should store these as environment variables to keep them secure. Install the libSQL client package: ```bash npm theme={null} npm install @libsql/client ``` ```bash pnpm theme={null} pnpm add @libsql/client ``` ```bash yarn theme={null} yarn add @libsql/client ``` ```bash bun theme={null} bun add @libsql/client ``` Create a client instance with your database URL and auth token: ```typescript theme={null} import { createClient } from "@libsql/client/web"; const client = createClient({ url: process.env.BUNNY_DATABASE_URL, authToken: process.env.BUNNY_DATABASE_AUTH_TOKEN, }); ``` You can execute a SQL query against your database by calling `execute()`: ```typescript theme={null} await client.execute("SELECT * FROM users"); ``` If you need to use placeholders for values, you can do that: ```typescript Positional theme={null} await client.execute({ sql: "SELECT * FROM users WHERE id = ?", args: [1], }); ``` ```typescript Named theme={null} await client.execute({ sql: "INSERT INTO users VALUES (:name)", args: { name: "Kit" }, }); ``` ## Using with Bunny Edge Scripting You can connect [Edge Scripts](/docs/scripting) to your database by adding credentials as environment variables directly from the database dashboard. See [Edge Scripting](/docs/database/connect/scripting) for step-by-step instructions on connecting your script to Bunny Database. ```typescript theme={null} import { createClient } from "@libsql/client/web"; const client = createClient({ url: process.env.BUNNY_DATABASE_URL, authToken: process.env.BUNNY_DATABASE_AUTH_TOKEN, }); const result = await client.execute("SELECT * FROM users"); ``` When using Edge Scripting, your database credentials are automatically available as `BUNNY_DATABASE_URL` and `BUNNY_DATABASE_AUTH_TOKEN` environment variables after generating a token from the database dashboard. ## Using with Magic Containers You can connect [Magic Container](/docs/magic-containers) apps to your database by adding credentials as environment variables directly from the database dashboard. See [Magic Containers](/docs/database/connect/magic-containers) for step-by-step instructions on connecting your app to Bunny Database. ```typescript theme={null} import { createClient } from "@libsql/client/web"; const client = createClient({ url: process.env.BUNNY_DATABASE_URL, authToken: process.env.BUNNY_DATABASE_AUTH_TOKEN, }); const result = await client.execute("SELECT * FROM users"); ``` When using Magic Containers, your database credentials are automatically available as `BUNNY_DATABASE_URL` and `BUNNY_DATABASE_AUTH_TOKEN` environment variables after generating a token from the database dashboard. ## Response Each query method returns a `Promise`: | Property | Type | Description | | ----------------- | --------------------- | -------------------------------------------------------------------------------------- | | `rows` | `Array` | An array of Row objects containing the row values, empty for write operations | | `columns` | `Array` | An array of strings with the names of the columns in the order they appear in each Row | | `rowsAffected` | `number` | The number of rows affected by a write statement, `0` otherwise | | `lastInsertRowid` | `bigint \| undefined` | The ID of a newly inserted row, or `undefined` if there is none for the statement | ## Placeholders libSQL supports the use of positional and named placeholders within SQL statements: ```typescript Positional theme={null} const result = await client.execute({ sql: "SELECT * FROM users WHERE id = ?", args: [1], }); ``` ```typescript Named theme={null} const result = await client.execute({ sql: "INSERT INTO users VALUES (:name)", args: { name: "Kit" }, }); ``` libSQL supports the same named placeholder characters as SQLite — `:`, `@` and `$`. ## Batch Transactions A batch consists of multiple SQL statements executed sequentially within an implicit transaction. The backend handles the transaction: success commits all changes, while any failure results in a full rollback with no modifications. ```typescript theme={null} const result = await client.batch( [ { sql: "INSERT INTO users VALUES (?)", args: ["Kit"], }, { sql: "INSERT INTO users VALUES (?)", args: ["Sam"], }, ], "write", ); ``` ### Transaction Modes | Mode | SQLite command | Description | | ---------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `write` | `BEGIN IMMEDIATE` | The transaction may execute statements that read and write data. Write transactions executed on a replica are forwarded to the primary instance, and can't operate in parallel. | | `read` | `BEGIN TRANSACTION READONLY` | The transaction may only execute statements that read data (select). Read transactions can occur on replicas, and can operate in parallel with other read transactions. | | `deferred` | `BEGIN DEFERRED` | The transaction starts in read mode, then changes to write as soon as a write statement is executed. This mode change may fail if there is a write transaction currently executing on the primary. | ## Interactive Transactions Interactive transactions in SQLite ensure the consistency of a series of read and write operations within a transaction's scope. These transactions give you control over when to commit or roll back changes, isolating them from other client activity. | Method | Description | | ------------ | ------------------------------------------------------------------- | | `execute()` | Similar to `execute()` except within the context of the transaction | | `commit()` | Commits all write statements in the transaction | | `rollback()` | Rolls back the entire transaction | | `close()` | Immediately stops the transaction | ```typescript theme={null} const transaction = await client.transaction("write"); try { // Read the current balance const result = await transaction.execute({ sql: "SELECT balance FROM accounts WHERE id = ?", args: [1], }); const currentBalance = result.rows[0].balance as number; const newBalance = currentBalance - 100; // Validate and update based on the read value if (newBalance < 0) { throw new Error("Insufficient funds"); } await transaction.execute({ sql: "UPDATE accounts SET balance = ? WHERE id = ?", args: [newBalance, 1], }); await transaction.commit(); } catch (e) { await transaction.rollback(); } ``` Interactive transactions in libSQL lock the database for writing until committed or rolled back, with a 5-second timeout. They can impact performance on high-latency or busy databases. # Durability and Consistency Source: https://bunny.net/docs/database/durability-and-consistency Understand how Bunny Database handles writes, replication, and data durability Bunny Database uses a primary-replica architecture with durable storage to balance performance, consistency, and durability. ## Database types ### Primary The primary database accepts both reads and writes. All write operations are executed on the primary. ### Replica Replicas accept reads and writes, but all writes are proxied to the primary. If a replica is not up to date with the latest changes, read requests may also be proxied to the primary until fresh data is available. ## Write path All writes are executed on the primary database. Replicas forward write operations to the primary and do not execute writes locally. If the primary is changing during a write operation, the request may take longer to complete. A 60-second timeout applies, and the write should complete once the new primary is established. ## Commit semantics A write is considered committed when it is appended to the primary's WAL (Write-Ahead Log). At this point, the write is durable on the primary but has not yet been uploaded to storage. ## Durability Data is persisted to storage every 10 seconds or every 4096 frames (where a frame represents a single database page change), whichever comes first. Writes not yet included in the latest successfully uploaded WAL batch may be lost during a primary failover. The maximum data loss window is 10 seconds or 4096 frames. ### Snapshots Bunny Database creates full snapshots of your data at specific points: * **Hourly**: While the database is online, a vacuum operation runs every hour * **On idle**: When a database becomes inactive, a full vacuum and snapshot is created and uploaded to storage When restoring, the system downloads the latest snapshot and replays any subsequent WAL segments. ## Replica consistency The primary streams changes to all active replicas in real-time. Replica lag depends on network latency between regions. Replicas track their current version. If a replica knows it has version 3 but the primary has version 4, the replica will proxy requests to the primary until it receives the updated data. Read-your-writes consistency is not guaranteed when reading from replicas. If your application requires reading data immediately after writing, direct those reads to the primary or account for potential replication lag. ## Failover behavior ### Primary failure When the primary fails, a new primary is automatically promoted and syncs any missing data from storage. All replicas are notified and begin syncing from the new primary. During failover, the system attempts to persist pending data to storage. If this cannot complete in time, writes not yet uploaded may be lost. ### Replica failure If a replica fails, requests are automatically forwarded to another active replica. ### Storage failure If the underlying storage becomes unavailable, data remains persisted on the primary database while it is running. However, if both storage and the primary fail simultaneously, data is unavailable until storage recovers. # Bunny Database Source: https://bunny.net/docs/database/index Bunny Database is a globally distributed SQLite-compatible database. bunny.net Database Bunny Database is currently in [Public Preview](/docs/product-release-stages#stage-2-public-preview). Features and APIs may evolve during this period. Bunny Database is a fully managed relational database service built on libSQL, a fork of SQLite. It allows you to easily deploy simple, globally distributed SQL databases. The service automatically handles scaling and replication, and stays idle when inactive for a usage-based billing model. ## Key features * **SQLite compatibility**: Use standard SQL syntax and leverage existing SQLite knowledge, tools, and libraries * **Global replication**: Replicate your database across multiple regions for low-latency access worldwide * **Multiple connection options**: Connect via HTTP API, native SDKs, or popular ORMs * **Edge integration**: Works seamlessly with Edge Scripting and Magic Containers * **Fully managed**: No infrastructure to maintain with automatic scaling and replication * **Usage-based billing**: Stays idle when inactive, so you only pay for what you use ## Integrations Bunny Database integrates with other bunny.net developer platform services: Deploy any Docker workload without provisioning. Databases and containers are deployed to the same regions to minimize latency. Run lightweight serverless JavaScript/TypeScript logic close to your users. ## Getting started Create your first database and run queries in minutes Connect directly using the HTTP-based SQL API Connect using the libSQL client for JS & TypeScript Connect using the libSQL client for Go Connect using the libSQL crate for Rust Connect using the Bunny.LibSQL.Client for .NET # Limits Source: https://bunny.net/docs/database/limits Understand the usage limits and quotas for Bunny Database during the Public Preview phase. | Limit | Value | Description | | ----------------------- | ----- | ---------------------------------------- | | Max number of databases | 50 | Maximum number of databases per account. | | Max database size | 1 GB | Maximum size of a single database. | These limits are in place during the Public Preview phase and are designed to accommodate typical use cases under normal operating conditions. These limits can be raised upon request. If you need higher limits, please contact [support](https://support.bunny.net) to discuss your requirements. # Metrics Source: https://bunny.net/docs/database/metrics Monitor your database performance with real-time metrics and usage statistics Bunny Database provides detailed metrics to help you monitor performance, track usage, and optimize your queries. Access metrics from **Dashboard > Edge Platform > Database > \[Select Database] > Metrics**. Prefer the terminal? The [Bunny CLI](/docs/cli/quickstart) reports usage statistics for a database with `bunny db usage`: ```bash theme={null} bunny db usage # current month bunny db usage --period 7d # last 7 days bunny db usage --from 2026-01-01 --to 2026-01-31 ``` It shows rows read and written, query count, average latency, and storage. See [`bunny db`](/docs/cli/commands/db) for the full command reference. ## Date range Use the date picker to filter all metrics by a specific time period. Select from preset ranges or define a custom date range to analyze historical performance. Date Range Picker ## Available metrics ### Rows read and written Track the number of database rows read and written over time. This metric helps you understand your database workload and identify usage patterns. Rows Read and Written | Metric | Description | | ------------ | ------------------------------------------------------------------------ | | Rows Read | Total number of rows retrieved by `SELECT` queries | | Rows Written | Total number of rows affected by `INSERT`, `UPDATE`, `DELETE` operations | High read counts may indicate opportunities for caching or query optimization. Sudden spikes in writes may indicate bulk operations or potential issues worth investigating. ### Latency Monitor query response times across different percentiles to understand your database performance characteristics. Latency Metrics | Metric | Description | | ------- | ----------------------------------------------------------------------- | | Average | Mean response time across all queries | | P75 | 75th percentile latency (75% of queries complete faster than this time) | | P95 | 95th percentile latency (95% of queries complete faster than this time) | Average latency should remain consistent over time. Increases may indicate growing data volumes or query complexity. A large gap between P95 and average suggests some queries need optimization. Consider adding indexes or restructuring slow queries. ### Query count View the total number of queries executed against your database over time. Correlate query volume with application traffic to ensure expected behavior and identify unusual patterns. Query Count ### Database size Monitor your database storage consumption over time. This metric shows the total size of your database including all tables, indexes, and metadata. Track growth trends to plan for scaling and identify unexpected increases. Database Size # Quickstart Source: https://bunny.net/docs/database/quickstart Create a database and run your first query in minutes 1. From your Bunny dashboard, click **Add** in the left-hand sidebar and select **Database**. Add Database 2. Enter a unique name for your database. This name will be used to identify your database in the dashboard and connection URLs. Database Name 3. Choose how you want your database deployed: * **Automatic**: We will choose the optimal regions for your database based on your location and best performance. * **Single region**: Deploy your database to a single region without any replication. Best for development or applications with users in one geographic area. * **Manual**: Manually select your storage location along with primary and replication regions. Best for applications with specific latency or compliance requirements. Region Selection 4. Click **Add Database** to create your database. Add Database Button 5. Once created, copy your connection credentials: * **Database URL**: Your database endpoint * **Access Token**: Choose between **Full Access** (read/write) or **Read Only** depending on your needs Database Credentials Keep your access tokens secure. Never commit them to version control or expose them in client-side code. 6. **Optional**: if you have an existing Edge Script or Magic Container, you can add your database credentials directly from this page: 1. Click **Add Secrets to Edge Script** or **Add Secrets to Magic Container Apps** 2. Select the script or app you want to connect 3. The database URL and access token will be added as environment variables automatically Add Secrets Don't have the CLI installed? See the [CLI quickstart](/docs/cli/quickstart) to install and authenticate. Create a database interactively: ```bash theme={null} bunny db create ``` You'll be prompted for a name and region mode (automatic, single region, or manual). After creation, the CLI offers to: * **Link** the current directory to the database (writes `.bunny/database.json`) * **Generate** a full-access auth token * **Save** `BUNNY_DATABASE_URL` and `BUNNY_DATABASE_AUTH_TOKEN` to a `.env` file For a fully non-interactive run (e.g. in CI): ```bash theme={null} bunny db create --name mydb --primary FR ``` Keep your access tokens secure. Never commit them to version control or expose them in client-side code. See [`bunny db`](/docs/cli/commands/db) for the full command reference, including `bunny db shell` for an interactive SQL prompt. Install the official SDK for your language and connect to your database: Install the libSQL client: ```bash theme={null} npm install @libsql/client ``` Connect and run queries: ```typescript theme={null} import { createClient } from "@libsql/client/web"; const client = createClient({ url: "libsql://your-database-id.lite.bunnydb.net", authToken: "your-access-token", }); // Create a table await client.execute(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL ) `); // Insert data await client.execute({ sql: "INSERT INTO users (name, email) VALUES (?, ?)", args: ["Kit Hopkins", "kit@bunny.net"], }); // Query data const result = await client.execute("SELECT * FROM users"); console.log(result.rows); ``` Add the libSQL crate to your `Cargo.toml`: ```toml theme={null} [dependencies] libsql = "0.6" tokio = { version = "1", features = ["full"] } ``` Connect and run queries: ```rust theme={null} use libsql::Builder; #[tokio::main] async fn main() -> Result<(), libsql::Error> { let db = Builder::new_remote( "libsql://your-database-id.lite.bunnydb.net".to_string(), "your-access-token".to_string(), ) .build() .await?; let conn = db.connect()?; // Create a table conn.execute( "CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL )", (), ) .await?; // Insert data conn.execute( "INSERT INTO users (name, email) VALUES (?1, ?2)", ("Kit Hopkins", "kit@bunny.net"), ) .await?; // Query data let mut rows = conn.query("SELECT * FROM users", ()).await?; while let Some(row) = rows.next().await? { let id: i64 = row.get(0)?; let name: String = row.get(1)?; let email: String = row.get(2)?; println!("User: {} - {} ({})", id, name, email); } Ok(()) } ``` Install the libSQL driver: ```bash theme={null} go get github.com/tursodatabase/libsql-client-go/libsql ``` Connect and run queries: ```go theme={null} package main import ( "database/sql" "fmt" "log" _ "github.com/tursodatabase/libsql-client-go/libsql" ) func main() { url := "libsql://your-database-id.lite.bunnydb.net?authToken=your-access-token" db, err := sql.Open("libsql", url) if err != nil { log.Fatal(err) } defer db.Close() // Create a table _, err = db.Exec(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL ) `) if err != nil { log.Fatal(err) } // Insert data _, err = db.Exec( "INSERT INTO users (name, email) VALUES (?, ?)", "Kit Hopkins", "kit@bunny.net", ) if err != nil { log.Fatal(err) } // Query data rows, err := db.Query("SELECT * FROM users") if err != nil { log.Fatal(err) } defer rows.Close() for rows.Next() { var id int64 var name, email string rows.Scan(&id, &name, &email) fmt.Printf("User: %d - %s (%s)\n", id, name, email) } } ``` Install the libSQL client package: ```bash theme={null} dotnet add package Libsql.Client ``` Connect and run queries: ```csharp theme={null} using Libsql.Client; var client = DatabaseClient.Create(opts => { opts.Url = "libsql://your-database-id.lite.bunnydb.net"; opts.AuthToken = "your-access-token"; }); // Create a table await client.Execute(@" CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL ) "); // Insert data await client.Execute( "INSERT INTO users (name, email) VALUES (?, ?)", "Kit Hopkins", "kit@bunny.net" ); // Query data var result = await client.Execute("SELECT * FROM users"); foreach (var row in result.Rows) { Console.WriteLine($"User: {row["id"]} - {row["name"]} ({row["email"]})"); } ``` # Replication Source: https://bunny.net/docs/database/replication Learn how Bunny Database separates storage from compute and replicates data across regions for low-latency access. Bunny Database separates data storage (at rest) from data processing (in compute instances). This separation allows compute resources to be allocated dynamically in different regions while keeping data safely stored in its designated location. Configure regions from the Dashboard or the [Bunny CLI](/docs/cli/quickstart): Manage regions from **Dashboard > Edge Platform > Database > \[Select Database] > Regions**. List, add, and remove regions with the `bunny db regions` commands: ```bash theme={null} bunny db regions list # show primary and replica regions bunny db regions add --primary DE # add a primary region bunny db regions add --replicas UK,NY # add replica regions bunny db regions remove --replicas UK # remove a region ``` At least one primary region must remain. See [`bunny db`](/docs/cli/commands/db) for the full command reference. ## Storage location Data can be stored at rest in: * Toronto, CA (North America) * Frankfurt, DE (Europe) Storage Location If a database doesn't receive any requests, it will be moved to object storage and removed from all active regions to optimize costs. ## Enabled regions Enabled Regions ### Replication regions Replication regions function as dynamically provisioned read replicas, offering fast, low-latency data access while proxying write operations to the primary region. ### Primary regions The primary region handles write operations. You can select multiple regions, but only one is active at a time. The active primary region is automatically chosen based on latency. If a database becomes idle, the primary region may change when it's reactivated. # Changelog Source: https://bunny.net/docs/dns/changelog Latest updates and improvements to Bunny DNS. ## Manage DNS from the bunny CLI Bunny DNS can now be managed from the command line with the [bunny CLI](/docs/cli). Create zones and records, import and export BIND zone files, manage DNSSEC, statistics, and query logging, and scaffold, deploy, and attach [Scriptable DNS](/docs/dns/scriptable/introduction) scripts with type-safe editor autocomplete via [`@bunny.net/scriptable-dns-types`](https://github.com/BunnyWay/cli/tree/main/packages/scriptable-dns-types). The DNS docs now include CLI examples alongside the dashboard steps. [Learn more](/docs/cli/commands/dns) ## DNS Limits and Defaults New reference page documenting Bunny DNS default limits, including the maximum number of records per zone (5,000) and DNS zones per account (500). Limits can be raised upon request by contacting sales. [Learn more](/docs/dns/limits) # DNSSEC Source: https://bunny.net/docs/dns/dnssec Enable DNSSEC to protect your domain from DNS spoofing, cache poisoning, and man-in-the-middle attacks. ## What is DNSSEC? DNSSEC (Domain Name System Security Extensions) adds a layer of security to your domain by enabling cryptographic validation of DNS responses. This ensures that visitors to your website are protected from DNS spoofing, cache poisoning, and man-in-the-middle attacks. When DNSSEC is enabled on your domain, DNS resolvers can verify that the DNS records they receive come from an authoritative source and haven't been tampered with. ## How DNSSEC Works with Bunny DNS Our DNS service supports full DNSSEC signing. Once enabled, your DNS zones are signed using cryptographic signatures, and we provide the necessary DS (Delegation Signer) records for your domain registrar. **Signing Algorithm:** We use Algorithm 13 (ECDSA Curve P-256 with SHA-256) for an ideal balance of security and performance. **Key Management:** DNSSEC keys are securely managed and automatically rotated. **Denial of Existence:** We employ NSEC Black Lies, an advanced privacy-preserving method. This prevents zone enumeration while providing authenticated denial-of-existence responses. NSEC Black Lies avoid exposing actual zone data, ensuring that attempts to list your domain's DNS records are thwarted. ## Steps to Enable DNSSEC 1. Enable DNSSEC within your DNS zone under the Security tab. 2. Copy the DS Record provided after enabling. 3. Add the DS Record through your domain registrar (see setup guides below). Don't have the CLI installed? See the [CLI quickstart](/docs/cli/quickstart) to install and authenticate. 1. Enable DNSSEC on the zone. The command prints the DS record to register: ```bash theme={null} bunny dns zones dnssec enable example.com ``` 2. Add the DS Record through your domain registrar (see setup guides below). To turn DNSSEC off again, run `bunny dns zones dnssec disable example.com` (confirms unless `--force`). **That's it! Your domain will now serve DNSSEC-signed responses.** ## Registrar-Specific DNSSEC Setup Guides | Registrar | DNSSEC Setup Guide | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | GoDaddy | [GoDaddy DNSSEC setup](https://www.godaddy.com/en-uk/help/add-a-ds-record-23865) | | Namecheap | [Namecheap DNSSEC guide](https://www.namecheap.com/support/knowledgebase/article.aspx/9722/2232/managing-dnssec-for-domains-pointed-to-custom-dns/) | | Google Domains | [Google Domains DNSSEC guide](https://cloud.google.com/dns/docs/dnssec-config) | | Bluehost | [Bluehost DNSSEC information](https://cp.cn.bluehost.com/kb/answer/1909) | | Dynadot | [Dynadot DNSSEC guide](https://www.dynadot.com/community/help/question/set-DNSSEC) | | Name.com | [Name.com DNSSEC guide](https://www.name.com/support/articles/205439058-managing-dnssec) | | Porkbun | [Porkbun DNSSEC guide](https://kb.porkbun.com/article/93-how-to-install-dnssec) | ## Verify DNSSEC deployment To verify correct deployment of your DNSSEC-enabled zone, make sure that you placed the correct DS record in the parent zone. DNS resolution can fail if either of the following occurs: * The configuration is wrong, or you have mistyped it. * You have placed the incorrect DS record in the parent zone. To verify that you have the right configuration in place and to cross-check the DS record before placing it in the parent zone, use the following tools: * [DNSViz](https://dnsviz.net/) * [Verisign DNSSEC debugger](https://dnssec-analyzer.verisignlabs.com/) # Import and Export Source: https://bunny.net/docs/dns/import-export Import and export DNS records using BIND zone files. Bunny DNS supports importing and exporting DNS records using the universal BIND zone file format. This lets you migrate between DNS providers or backup your configuration. ## Import records To import DNS records from a BIND-compatible zone file: 1. Go to **DNS** and select your DNS Zone 2. Click **Import/Export** 3. Upload your zone file or paste the contents 4. Review the imported records 5. Click to confirm the import Don't have the CLI installed? See the [CLI quickstart](/docs/cli/quickstart) to install and authenticate. ```bash theme={null} bunny dns records import example.com ./zonefile.txt ``` See the [`bunny dns` command reference](/docs/cli/commands/dns) for the full list of commands and flags. ### Supported record types for import | Type | Supported | | ----- | --------- | | A | Yes | | AAAA | Yes | | CNAME | Yes | | MX | Yes | | TXT | Yes | | SRV | Yes | | CAA | Yes | | PTR | Yes | Record types not listed above are skipped during import. Bunny-specific records (Pull Zone, Redirect, Script) cannot be imported automatically. ## Export records To export your DNS records to a BIND-compatible zone file: 1. Go to **DNS** and select your DNS Zone 2. Click **Import/Export** 3. Click **Export Zone File** 4. Save the downloaded file ```bash theme={null} bunny dns records export example.com # print to stdout bunny dns records export example.com --file ./my.zone # write to a path bunny dns records export example.com --save # write to ./example.com.zone ``` ### Supported record types for export | Type | Supported | | ----- | --------- | | A | Yes | | AAAA | Yes | | CNAME | Yes | | MX | Yes | | TXT | Yes | | SRV | Yes | | CAA | Yes | | PTR | Yes | Bunny-specific records such as Pull Zone, Redirect, and Script records are not included in the export. ## Zone file format The exported BIND file follows the standard format: ```dns theme={null} ;A records myothersite.example.com. IN 5m A 1.1.2.2 maindomain.example.com. IN 5m A 1.2.3.4 ;MX records mailexchange.example.com. IN 5m MX 0 mxexchange.host.com. ;TXT records example.com. IN 1m TXT "_acme-challenge.example.com= D-52Wm4V7xoUpGax-F8FrPO45cQRcbRj-XoblaY4uYM" ``` # Bunny DNS Source: https://bunny.net/docs/dns/index Hop on an ultra-fast DNS platform designed to power the next generation of applications. By utilizing scriptable records, Bunny DNS helps you unlock the true power of DNS and turn complex issues into simple solutions. bunny.net DNS Bunny DNS is an authoritative DNS service that runs on a dual-stack anycast network, natively supporting both IPv4 and IPv6 for global performance and reliability. Powered by our nameservers, we've built a resilient foundation designed to handle anything the internet throws at it. ## DNS platform for next-gen apps Make intelligent routing decisions and build smart network services with just a few lines of code. Interconnect your applications and get complete control of your network behavior. * Load balancing * SEO optimization * Service discovery * Domain mapping * Multi-cloud solutions * Geographical routing ## Easy migration Migrating to Bunny DNS is simple. When adding a new DNS Zone, you can: * **Auto-scan existing records**: Bunny DNS automatically detects and imports your current DNS records. Review and edit them before switching. * **Upload zone files**: Import a BIND-compatible zone file to migrate all records at once. Both options let you verify everything before making the switch, so you can migrate with confidence. ## DNS management Manage your DNS zones with a full suite of tools: * **Records**: Add, edit, and delete DNS records with support for all standard types plus Bunny-specific records like Pull Zone and Script records. * **CDN Acceleration**: Route traffic through Bunny CDN with a single click for improved performance and caching. * **Record sets**: Group multiple records of the same type for load balancing, weighted routing, and automatic failover. * **Smart records**: Configure geographic or latency-based routing to direct users to the closest or fastest endpoint. * **Health monitoring**: Automatically remove offline endpoints from DNS responses. Zones, records, and Scriptable DNS scripts can all be managed from the dashboard or from the command line with the [bunny CLI](/docs/cli/commands/dns). ## Security We mitigate large-scale L3/L4 DDoS attacks right at the edge, and apply smart L7 protections with per-IP DNS rate limiting to keep your domains safe and responsive. Set up [DNSSEC](/docs/dns/dnssec) in just a few clicks and keep your records safe from spoofing and tampering. ## Scriptable DNS Bunny DNS is scriptable with [Edge Scripting](/docs/dns/scriptable/introduction). Write JavaScript to dynamically respond to DNS queries, enabling advanced use cases like: * Health-based routing with automatic failover * Weighted load balancing * Geographic routing based on client location * Custom logic for A/B testing or canary deployments Learn how to write your first scriptable DNS handler. # DNS Limits and Defaults Source: https://bunny.net/docs/dns/limits Overview of the default limits for Bunny DNS. | Limit | Value | Description | | --------------------- | ----- | ------------------------------------------------ | | Records per zone | 5,000 | Maximum number of DNS records in a single zone. | | DNS zones per account | 500 | Maximum number of DNS zones across your account. | These are default limits, designed to accommodate typical use cases under normal operating conditions. These limits can be raised upon request. If you need higher limits, please [contact our sales team](https://bunny.net/contact-sales/) with your use case and expected scale. # Logging Source: https://bunny.net/docs/dns/logging Enable and configure DNS query logging for your zone. DNS logging records raw access logs of all queries to your zone. This is useful for debugging, security analysis, and traffic monitoring. ## Enable logging To enable DNS logging: 1. Go to **DNS** and select your DNS Zone 2. Click **Logging** in the zone menu 3. Click **Settings** 4. Enable logging 5. Save your configuration Don't have the CLI installed? See the [CLI quickstart](/docs/cli/quickstart) to install and authenticate. ```bash theme={null} bunny dns zones logging enable example.com # Anonymize client IPs in the logs (strategy: onedigit or drop) bunny dns zones logging enable example.com --anonymize-ip --anonymization drop # Disable logging (confirms unless --force) bunny dns zones logging disable example.com ``` When enabled, the zone records and stores raw access logs of all queries for up to 3 days. ## View logs Once logging is enabled, the Logging page displays query logs with the following information: | Field | Description | | ------------------- | ---------------------------------------------------- | | **Query Type** | The DNS record type requested (A, AAAA, CNAME, etc.) | | **Hostname** | The queried hostname | | **Remote IP** | The IP address that made the query | | **EDNS IP** | The EDNS client subnet IP, if provided | | **Response Values** | The DNS response returned | AAAA queries are logged even for zones without AAAA records configured. See [AAAA Queries Without AAAA Records](/docs/dns/troubleshooting/aaaa-queries-in-logs) for an explanation. ## Privacy settings ### Anonymize log IPs Enabled by default, this setting anonymizes IP addresses in DNS logs to ensure compliance with GDPR privacy regulations. ### Log anonymization type Choose how IP addresses are anonymized: * **Remove one octet**: Masks the last segment of the IP address * **Drop IP completely**: Removes the IP address entirely from logs # Nameservers Source: https://bunny.net/docs/dns/nameservers Configure nameservers for your DNS Zone. After creating a DNS Zone, you need to update your domain's nameservers at your registrar to point to Bunny DNS. This page shows you where to find your nameserver details and how to configure custom nameservers. ## Default nameservers Bunny DNS uses the following nameservers: * `kiki.bunny.net` * `coco.bunny.net` To view your nameservers, go to **DNS**, select your DNS Zone, then click **Nameservers** in the zone menu. With the [bunny CLI](/docs/cli), run `bunny dns zones nameservers example.com` to show them (custom nameservers if enabled, otherwise the defaults above). ## Update nameservers at your registrar To activate Bunny DNS for your domain, update the nameserver records at your domain registrar: 1. Log in to your domain registrar 2. Find the domain management or DNS settings 3. Replace existing nameservers with `kiki.bunny.net` and `coco.bunny.net` 4. Save the changes You can verify the delegation has taken effect with the CLI, which does a live nameserver lookup: ```bash theme={null} bunny dns zones nameservers example.com ``` If the registrar already delegates to Bunny DNS it confirms; otherwise it shows the nameservers to set at your registrar, naming the registrar when it can detect it. DNS propagation can take up to 48 hours, though changes typically take effect within a few hours. ## Custom nameservers Custom nameservers let you use your own domain for nameservers instead of the default `bunny.net` domain. This is useful for white-labeling or branding purposes. To configure custom nameservers: 1. Go to **DNS** and select your DNS Zone 2. Click **Nameservers** 3. Enable **Custom nameservers** 4. Enter your custom nameserver hostnames 5. Click **Save Configuration** ### Glue records When using custom nameservers, you need to create glue records at your domain registrar. Glue records provide the IP addresses for your nameservers, preventing circular dependencies. The nameserver IP addresses are displayed in the configuration panel: | Nameserver | IPv4 | IPv6 | | ------------ | --------------- | ------------------- | | Nameserver 1 | `91.200.176.1` | `2400:52e0:fff0::1` | | Nameserver 2 | `109.104.147.1` | `2400:52e0:fff2::1` | Before changing nameservers, make sure the appropriate DNS or glue records have been set correctly on the domain. Misconfigured records can leave your [domain unreachable](/docs/dns/troubleshooting/domain-unavailable). ## SOA configuration The SOA (Start of Authority) record contains administrative information about your zone. You can configure the contact email address in the nameserver settings. | Field | Description | | --------- | ------------------------------------------------------------------------------- | | **Email** | The administrative contact email for the zone (default: `hostmaster@bunny.net`) | # Quickstart Source: https://bunny.net/docs/dns/quickstart Create and configure your first DNS Zone in minutes. This guide walks you through adding a DNS Zone to bunny.net and configuring your domain to use Bunny DNS. In the bunny.net dashboard, go to **DNS** and click **Add DNS Zone**, or select it from the **+ Add** sidebar launcher. Add DNS Zone Type your domain name (e.g., `mywebsite.com`) to start the setup process. Enter domain Now that you've entered your domain, you can import any existing DNS records. Scan or upload Select this option to have Bunny DNS scan and fetch your existing DNS records automatically. You can review and edit them before switching, or start from scratch. The scanning process can take several minutes depending on your domain's complexity. Drag and drop a DNS zone file (such as a BIND zone file), or click **Upload Zone File** to import all records at once. This is useful when migrating from providers that support zone file exports. [Learn more about importing zone files](/docs/dns/import-export) Click **Next** to continue. After scanning or importing, review your DNS records carefully: * Compare detected records with your current DNS provider * Add any missing records that weren't detected * Remove or modify records as needed * Check that critical records like MX (email) and CNAME entries are correct Review records Once you've verified your records, choose how to proceed: * **Confirm and add** creates the DNS Zone with your imported or configured records * **Add DNS Zone without records** creates an empty DNS Zone that you can populate manually Confirm and add Click your preferred option to create the zone. After creating your DNS Zone, update your domain's nameservers at your domain registrar to point to Bunny DNS: * `kiki.bunny.net` * `coco.bunny.net` The process varies by registrar, but typically involves logging in to your registrar's control panel and replacing the existing nameservers with the ones above. DNS propagation can take up to 48 hours, though changes typically take effect within a few hours. Don't have the CLI installed? See the [CLI quickstart](/docs/cli/quickstart) to install and authenticate. ```bash theme={null} bunny dns zones add example.com ``` After creating the zone, the CLI checks your domain's live registrar delegation. If the domain already points at Bunny DNS, it simply confirms. Otherwise it prints the nameservers to set, naming your registrar when it can detect it. Import an existing BIND zone file, or add records individually: ```bash theme={null} # Import a BIND zone file exported from your current provider bunny dns records import example.com ./zonefile.txt # Or add records one by one (use '@' for the zone apex) bunny dns records add example.com api A 198.51.100.1 bunny dns records add example.com '@' MX mail.example.com 10 # Or apply a preset record set for a common provider (email, verification, security) bunny dns records preset google-workspace example.com ``` Run `bunny dns records add` with no arguments for an interactive wizard, and `bunny dns records list example.com` to review the result. Update your domain's nameservers at your domain registrar to point to Bunny DNS: * `kiki.bunny.net` * `coco.bunny.net` Then verify the delegation has taken effect: ```bash theme={null} bunny dns zones nameservers example.com ``` This does a live nameserver lookup and confirms once your registrar delegates to Bunny DNS. DNS propagation can take up to 48 hours, though changes typically take effect within a few hours. See the [`bunny dns` command reference](/docs/cli/commands/dns) for the full list of zone, record, and script commands. ## Next steps Your DNS Zone is now active. From here you can: Add, edit, and configure DNS records for your domain. Protect your domain from spoofing and tampering. Write JavaScript to dynamically respond to DNS queries. Monitor DNS query traffic and patterns. # DNS Records Source: https://bunny.net/docs/dns/records Add, edit, and manage DNS records for your zone. DNS records map domain names to IP addresses and other resources. Bunny DNS supports all standard record types and provides advanced features like weighted load balancing, smart routing, and health monitoring through record sets. ## Add a record In the bunny.net dashboard, go to **DNS** and select your DNS Zone, then click **Add Record**. Fill in the record details: | Field | Description | | ------------ | ------------------------------------------------------------------- | | **Hostname** | The subdomain for this record. Leave empty for the root domain. | | **Type** | The record type (A, AAAA, CNAME, MX, TXT, etc.) | | **TTL** | Time to live. How long resolvers cache the record. | | **Value** | The record value (IP address, hostname, or text depending on type). | Expand **Advanced Settings** to configure additional options: | Setting | Description | | --------------------- | ------------------------------------------------------------------------------------------------------------- | | **Routing Weight** | Value between 0-100 that controls how often this record is returned relative to others in a record set. | | **Monitoring** | Enable health monitoring to automatically remove offline records from responses. | | **Smart Record Type** | Configure geographic or latency-based routing for A and AAAA records. | | **Record Enabled** | Toggle to disable a record without deleting it. Disabled records remain visible but don't respond to queries. | Click **Add Record** to save. Don't have the CLI installed? See the [CLI quickstart](/docs/cli/quickstart) to install and authenticate. Add records with `bunny dns records add`. Positional values follow the record type, and `'@'` targets the zone apex: ```bash theme={null} bunny dns records add example.com api A 198.51.100.1 bunny dns records add example.com '@' MX mail.example.com 10 bunny dns records add example.com '@' SRV 10 0 389 sip.example.com bunny dns records add example.com '@' CAA '0 issue "letsencrypt.org"' # Link a record to a Pull Zone or DNS script bunny dns records add example.com cdn PullZone --pull-zone 12345 bunny dns records add example.com fn Script --script 67890 ``` Omit the record type (or all arguments) to run an interactive wizard that prompts for the zone, type, and per-type values. Pass `--ttl` and `--comment` to set those fields. You can also apply a curated preset record set for a common provider or task (Google Workspace, Microsoft 365, DMARC, and more) in one step: ```bash theme={null} bunny dns records preset list bunny dns records preset google-workspace example.com ``` See the [`bunny dns` command reference](/docs/cli/commands/dns) for the full list of commands and flags. ## Edit a record To modify an existing record: 1. Locate the record in your DNS Zone 2. Click the **...** menu on the record row 3. Select **Edit** 4. Modify the record settings 5. Click **Save Record** Pass the record ID (from `bunny dns records list`), or omit it to pick the record interactively. Only the flags you set are changed: ```bash theme={null} bunny dns records list example.com bunny dns records update example.com 123 --value 198.51.100.2 bunny dns records update example.com 123 --ttl 3600 bunny dns records update example.com 123 --disabled ``` The record type cannot be changed when editing. Delete and recreate the record if you need a different type. ## Delete a record To remove a record: 1. Locate the record in your DNS Zone 2. Click the **...** menu on the record row 3. Select **Delete** 4. Click **Delete** to confirm Pass the record ID, or omit it to pick the record interactively. The CLI confirms before deleting unless you pass `--force`: ```bash theme={null} bunny dns records remove example.com 123 bunny dns records remove example.com 123 --force ``` Deleting a record takes effect immediately. Make sure the record is no longer needed before deleting. ## Quick edit TTL You can change a record's TTL directly from the record list without opening the full edit dialog. Click the **TTL** value and select a new option: * 15m * 30m * 1h * 5h * 12h * 1D ## CDN Acceleration CDN Acceleration routes traffic through Bunny CDN for improved performance and caching. Click the **CDN Acceleration** field in the record list to toggle it on or off. When enabled: * TTL automatically changes to **Auto** * Click the settings icon to configure the connected Pull Zone * Bunny CDN handles caching, optimization, and DDoS protection ## Record sets A record set groups multiple DNS records of the same type that share the same name. Record sets enable advanced features like load balancing and failover. ### A and AAAA record sets Record sets for A (IPv4) and AAAA (IPv6) records support: * **Weighting**: Assign different weights to control how often each record is returned * **Smart routing**: Route traffic based on geographic location or latency * **Health monitoring**: Automatically remove unhealthy endpoints from responses These features make A and AAAA record sets ideal for global load balancing and CDN configurations. ### Other record sets For record types other than A and AAAA (such as TXT, CNAME, MX), all records in the set are returned together when queried. ## Smart records A and AAAA records can be configured as Smart Records for dynamic routing based on user location. ### Geographic routing Routes queries based on the end user's geographical location. The record closest to the user is returned. Location is determined using: * Bunny DNS resolver location * Query remote IP * EDNS0 client subnet IP ### Latency routing Routes queries based on estimated latency to bunny.net datacenter regions. Select the region closest to your server for accurate matching. Latency routing may be more accurate than geographic routing since physical distance doesn't always correlate with network latency. ## Load balancing Bunny DNS supports DNS-based load balancing through weighted record sets and health monitoring. ### Weights Every A and AAAA record has a **Routing Weight** (0-100) that controls how often it's returned relative to other records in the set. For example, with two records where one has weight 100 and another has weight 50: * First record returned approximately 66% of the time * Second record returned approximately 33% of the time If all records have equal weights, up to 3 records are randomly returned from the set. ### Health monitoring Bunny DNS provides basic built-in monitoring for any A, AAAA, or CNAME record. When monitoring is enabled, each IP is tested every 30 seconds from a selection of 3 regions around the world. For a record to be detected as online, the monitored response must be successful. The 30-second monitoring interval cannot currently be changed. #### Monitoring types Two monitoring types are available: | Type | Description | | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Ping** | Sends 4 ICMP ping packets to the monitored IP during each test from a region. At least one packet must return successfully within 2500ms for the test to pass. | | **HTTP** | Sends HTTP requests to the configured IP on port 80. The request must complete within 10 seconds and return a non-5xx status code. Any other outcome (including a 5xx response) counts as a timeout. | #### What happens when a record is offline The behavior when a record is detected as offline depends on the record set: * **Single-value records** continue to be returned regardless of health status. * **Record sets** (multiple records sharing the same type and name) remove offline records from routing automatically. * If **all records** in a set are offline, filtering is disabled to prevent false positives and all records continue to be returned. Bunny DNS does not currently send alerts when an outage is detected. Alerting is planned for a future release. ## Wildcard records A wildcard record (for example, `*.example.com`) is used to synthesize responses for any subdomain that does not have an explicit record of any type defined. Bunny DNS intentionally deviates from [RFC 4592](https://datatracker.ietf.org/doc/html/rfc4592) when a wildcard interacts with an Empty Non-Terminal (ENT). An ENT is a name that has no records of its own but has descendants that do (for example, `b.example.com` is an ENT if only `a.b.example.com` exists). Per the RFC, queries for an ENT should return a NODATA response and the wildcard must not be expanded. Bunny DNS instead actively populates the response with the wildcard value, so queries for an ENT receive the wildcard record rather than an empty answer. ## Supported record types ### Standard records | Type | Description | | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **A** | Maps a domain to an IPv4 address | | **AAAA** | Maps a domain to an IPv6 address | | **CNAME** | Creates an alias pointing to another domain | | **MX** | Specifies mail servers for the domain | | **TXT** | Stores text data (SPF, DKIM, verification records) | | **SRV** | Defines service locations (hostname, port, priority, weight) | | **CAA** | Specifies which certificate authorities can issue certificates | | **PTR** | Reverse DNS lookup | | **NS** | Delegates a subdomain to other nameservers | | **HTTPS** | HTTPS service binding record. Indicates that a service is available over HTTPS and can include connection parameters such as supported protocols and port numbers. | | **SVCB** | General-purpose service binding record. Provides information about available services for a domain, including protocol and connection parameters. | | **TLSA** | Associates a TLS certificate with a domain. Used with DANE to specify which certificate or CA should be trusted for a domain. | Bunny DNS supports CNAME flattening on both **CNAME** and **Pull Zone (PZ)** records. When either is used at the zone apex (root domain), Bunny DNS automatically flattens it to A/AAAA records, so you can point a root domain directly at a Pull Zone. ### Bunny-specific records | Type (UI label) | Description | | ------------------ | ---------------------------------------------------------------- | | **PZ** (Pull Zone) | Links a domain to a CDN Pull Zone for automatic CDN acceleration | | **RDR** (Redirect) | URL redirect record. Redirects requests to another URL | | **SCR** (Script) | Scriptable DNS responses using DNS script | # Helper Objects Source: https://bunny.net/docs/dns/scriptable/helper-objects The Scriptable DNS comes with a list of helper objects to help with various tasks needed for dynamic routing. This page contains the documentation for the helper objects. ## Monitoring The monitoring helper allows you to check and monitor the uptime status of an IP. This is useful for uptime checking when returning DNS records. It takes a single IP parameter that is then monitored in the background. The first time an IP is called, it is added into a background monitoring service while synchronously waiting for the initial latency. Once the IP is called again from the same server, the result is returned immediately. ### Functions #### Monitoring.getStatus(IP: string): MonitoringStatus Returns the current status of the IP. ### MonitoringStatus The monitoring status object is returned by the `Monitoring.getStatus` helper and contains the monitoring information of a specific IP. | Field | Type | Description | | ---------- | ------- | ----------------------------------------------------------- | | `isOnline` | boolean | The current status of the IP (true: online, false: offline) | | `latency` | integer | The last measured latency from the DNS server to this IP | ```javascript theme={null} /* Example usage for Monitoring.getStatus Returns the A record for 222.222.222.222 if the IP is online, otherwise return 111.111.111.111 */ export default function handleQuery(query) { if (Monitoring.getStatus("222.222.222.222").isOnline == true) { return new ARecord("222.222.222.222", 30); } return new ARecord("111.111.111.111", 30); } ``` ## GeoDatabase GeoDatabase is a wrapper around a GeoDNS library. It allows you to easily look up the geo location of an IP address. ### Functions #### GeoDatabase.resolve(IP: string): GeoLocation Returns the [GeoLocation](/docs/dns/scriptable/introduction#geolocation) result for the specified IP based on a GeoDNS database. ```javascript theme={null} /* Example usage for GeoDatabase.resolve Resolves the geo location for the IP 142.251.10.102 and returns the country code as a TXT record */ export default function handleQuery(query) { var location = GeoDatabase.resolve("142.251.10.102"); return new TxtRecord(location.country, 30); } ``` ## GeoDistance GeoDistance contains helper methods to help you calculate the distance between two geographical points. ### Functions #### GeoDistance.calculate(lat1: double, lon1: double, lat2: double, lon2: double): double Returns the geographical distance between two geographical locations based on latitude and longitude. #### GeoDistance.calculate(loc1: GeoLocation, loc2: GeoLocation): double Returns the geographical distance between two GeoLocation objects. #### GeoDistance.calculate(server: Server, location: GeoLocation): double Returns the geographical distance between a Server object and a GeoLocation object. ```javascript theme={null} /* Example usage for GeoDistance.calculate Calculates the distance between two points on the map and returns them as a TxtRecord value */ export default function handleQuery(query) { var distance = GeoDistance.calculate( 40.73061, -73.935242, // New York 50.110924, 8.682127, // Frankfurt ); return new TxtRecord( "Distance between New York and Frankfurt: " + distance, 30, ); } ``` ## RoutingEngine RoutingEngine helps with dynamic geographic routing, weight calculation, and round robin logic based on a list of servers. ### Functions #### RoutingEngine.getWeightedRandom(servers: Array\, onlineOnly: bool = false, applyWeight: bool = true): Server Returns one server from an array of servers based on a round robin weighted random principle. Additionally, servers can be filtered by online servers only. To activate weights, the applyWeight parameter should be set to true. ```javascript theme={null} /* Example usage for RoutingEngine.getWeightedRandom Returns one of the servers in a round robin fashion with the weights applied */ export default function handleQuery(query) { var servers = new Array( new Server("89.187.162.249", 40.69, -74.18, 100), new Server("89.187.162.249", 40.69, -74.18, 100), new Server("89.187.162.249", 40.69, -74.18, 75), new Server("89.187.162.249", 40.69, -74.18, 50), ); return RoutingEngine.getWeightedRandom( servers, true, // Skip offline servers ); } ``` #### RoutingEngine.getClosestServer(servers: Array\, location: GeoLocation, onlineOnly: bool = false, applyWeight: bool = true): Server Returns one server from an array of servers based on a round robin weighted random principle that is closest to the given GeoLocation parameter. Additionally, servers can be filtered by online servers only. To activate weights, the applyWeight parameter should be set to true. ```javascript theme={null} /* Example usage for RoutingEngine.getClosestServer Returns the geographically closest server to the client that sent the query */ export default function handleQuery(query) { var servers = new Array( new Server("89.187.162.249", 40.69, -74.18), new Server("89.187.162.248", 52.31, 4.76), new Server("89.187.162.247", -37.67, 144.85), new Server("89.187.162.246", 33.94, -118.41), ); return RoutingEngine.getClosestServer( servers, query.request.geoLocation, true, // Skip offline servers ); } ``` ## Server The Server object is used to pass or return server information to various other helper methods and does not contain any functionality by itself. ### Constructor ```typescript theme={null} constructor(ip: string, latitude: double = 0, longitude: double = 0, weight: int = 100, online: bool = true) ``` ### Fields | Field | Type | Description | | ----------- | ------ | --------------------------------------------------------------- | | `ip` | string | The IP of the server | | `latitude` | double | The geographical latitude of the server's location | | `longitude` | double | The geographical longitude of the server's location | | `weight` | int | The routing weight of the server in a weighted routing scenario | # Introduction Source: https://bunny.net/docs/dns/scriptable/introduction The Scriptable DNS allows you to dynamically respond to DNS queries using JavaScript scripts. The following pages contain sample documentation to help you get started. ## Quickstart Create a DNS script, test it, publish it, and attach it to a hostname in your zone. In the bunny.net dashboard, go to [**DNS Scripts**](https://dash.bunny.net/dns/scripts) and click **Add DNS Script**. The Code editor opens with a starter script that answers every query with a TXT record: ```javascript theme={null} /* Handle the DNS query! */ export default function handleQuery(event) { return new TxtRecord("Hello world!"); } ``` Edit the script to return the response you need, such as an `ARecord` for IP-based routing. See the [entry function](#entry-function) below for the query details available to your script. Use the fields below the editor to simulate a query: set the **Type**, **Hostname**, **Client IP**, **EDNS IP**, and **Location**, then click **Run**. The **Response** panel shows the DNS response your script returned, so you can try different request parameters and evaluate how your script behaves. Output from `console.log` appears in the **Console** tab. Click **Save** to store your changes, then **Publish** to make the script live. A script only answers queries once a record points at it: 1. Go to **DNS** and select your DNS Zone 2. Click **Add DNS Record** 3. Enter a **Hostname** 4. Select **SCR** as the **Type** 5. Select the script you just published in the **Script** dropdown 6. Click **Add Record** Queries for that hostname are now answered by your script. Don't have the CLI installed? See the [CLI quickstart](/docs/cli/quickstart) to install and authenticate. ```bash theme={null} bunny dns scripts init hello-dns ``` This writes a `handleQuery` entry file, a `tsconfig.json`, and a `package.json` with the [type definitions](#type-safety) preconfigured, and links the directory to the script. Pass `--example` to start from a template (`empty`, `geo`, `closest`, `weighted`, `failover`, `pullzone`). Edit the entry file to return the response you need. See the [entry function](#entry-function) below for the query details available to your script. Upload and publish the linked script's entry file: ```bash theme={null} bunny dns scripts deploy ``` A script only answers queries once a record points at it. Add a `SCRIPT` record to a zone: ```bash theme={null} bunny dns scripts attach example.com api ``` The command confirms before writing the record. Verify it by querying the record type your script answers: ```bash theme={null} dig api.example.com A ``` See the [`bunny dns` command reference](/docs/cli/commands/dns) for the full list of script commands. ## Entry function The Scriptable DNS pipeline executes through a statically defined function **handleQuery**. To return a response to the query, you can return one of the DNS response objects as documented in the [Query Response Object Types](/docs/dns/scriptable/query-response-object-types). ```javascript theme={null} export default function handleQuery(query) { return new ARecord("111.111.111.111", 30); } ``` `console.log` is intended for use within the Script Editor only. Please remove all logging statements before saving or publishing, as they may cause your script to fail at runtime. ## Query object The entry function is passed a DnsRequest object parameter called **query** that contains the information about the request, such as the hostname, country, remote IP, etc. ### DnsRequest | Field | Type | Description | | --------- | -------- | --------------------------------------------------------------- | | `request` | DnsQuery | The DNS query request that contains the details about the query | ### DnsQuery | Field | Type | Description | | ------------- | ----------- | ---------------------------------------------------------------------------- | | `hostname` | string | The hostname that is being queried | | `clientIP` | string | The IP of the remote client that sent the DNS query | | `queryType` | string | The query question type (A, AAAA, TXT) | | `ednsIP` | string | The EDNS0 IP of the DNS query that was attached by the client | | `geoLocation` | GeoLocation | The geo location of the client | | `serverZone` | string | The server zone of the DNS server that received the query (DE, UK, SG, etc.) | ### GeoLocation | Field | Type | Description | | ----------- | ------ | ------------------------------------------------------ | | `latitude` | double | The latitude location of the client | | `longitude` | double | The longitude location of the client | | `country` | string | The detected two letter ISO country code of the client | | `asn` | long | The detected ASN number of the client | ## Type safety The Scriptable DNS runtime injects its globals (such as `ARecord` and the other [response object types](/docs/dns/scriptable/query-response-object-types)) directly into your script, and does not support `import`. The [`@bunny.net/scriptable-dns-types`](https://github.com/BunnyWay/cli/tree/main/packages/scriptable-dns-types) package provides ambient TypeScript declarations for these globals, giving you editor autocomplete and optional type checking without any runtime imports. Install it as a development dependency: ```bash theme={null} npm install --save-dev @bunny.net/scriptable-dns-types ``` Then reference the types from within your script file with a triple-slash directive: ```javascript theme={null} /// /** @param {DnsRequest} query */ export default function handleQuery(query) { if (query.request.geoLocation.country === "DE") { return new ARecord("203.0.113.20", 30); } return new ARecord("203.0.113.10", 30); } ``` Or enable them project-wide through `tsconfig.json`: ```json theme={null} { "compilerOptions": { "allowJs": true, "checkJs": true, "noEmit": true, "types": ["@bunny.net/scriptable-dns-types"] } } ``` # Query Response Object Types Source: https://bunny.net/docs/dns/scriptable/query-response-object-types The handleQuery entry point function supports multiple object return types to return different types of answers. This page contains the various types of answers the function can return to respond to queries. Each response object usually consists of a constructor that receives a string representation of the value and a TTL value parameter that determines how long the response client should cache the object. Objects can be returned as a single object or as an array of objects depending on the desired output. ## Supported response objects * [ARecord](#arecord) * [AaaaRecord](#aaaarecord) * [CnameRecord](#cname-record) * [PullZoneRecord](#pullzonerecord) ## Example response Below is an example code snippet to illustrate how to return a dynamic DNS response to a DNS query. If the request is coming from Germany, then 222.222.222.222 is returned, otherwise, 111.111.111.111 is returned. ```javascript theme={null} export default function handleQuery(query) { if (query.request.geoLocation.country == "DE") { return new ARecord("222.222.222.222", 30); } return new ARecord("111.111.111.111", 30); } ``` ## ARecord The ARecord represents the A type DNS answer to return A records. ```typescript theme={null} declare class ARecord { constructor(ip: string, ttl: number = 30); /** * The IP of the A record */ ip: string; /** * The TTL of the answer */ ttl: number; } ``` ## AaaaRecord The AaaaRecord represents the AAAA type DNS answer to return AAAA records. ```typescript theme={null} declare class AaaaRecord { constructor(ip: string, ttl: number = 30); /** * The IP of the AAAA record */ ip: string; /** * The TTL of the answer */ ttl: number; } ``` ## Cname Record The CnameRecord represents the CNAME type DNS answer to return CNAME records. ```typescript theme={null} declare class CnameRecord { constructor(hostname: string, ttl: number = 30); /** * The hostname of the CNAME record */ hostname: string; /** * The TTL of the answer */ ttl: number; } ``` ## PullZoneRecord The PullZoneRecord is used to map the response to a Pull Zone response, meaning the DNS system will automatically map the response to the appropriate A or AAAA records for this specific pull zone. ```typescript theme={null} declare class PullZoneRecord { constructor(pullzone: string); /** * The name of the pull zone */ pullzone: string; } ``` # Statistics Source: https://bunny.net/docs/dns/statistics View DNS query statistics for your zone. The Statistics page shows DNS query metrics for your zone, helping you understand traffic patterns and monitor usage. ## View statistics To access your DNS statistics, go to **DNS**, select your DNS Zone, then click **Statistics** in the zone menu. Don't have the CLI installed? See the [CLI quickstart](/docs/cli/quickstart) to install and authenticate. View query statistics from the command line. Text mode draws a bar chart, and the range defaults to the last 30 days: ```bash theme={null} bunny dns zones stats example.com bunny dns zones stats example.com --from 2026-05-01 --to 2026-05-31 bunny dns zones stats example.com --output json ``` ## Queries served The **Queries served** chart displays the total number of DNS queries handled by your zone over time. Use this to monitor overall traffic volume and identify patterns or spikes in activity. ## Queries by type The **Queries by type** chart breaks down queries by record type (A, AAAA, CNAME, MX, TXT, etc.), showing which record types receive the most traffic. ## Filter by timeframe Use the date picker in the top right to filter statistics by timeframe: * Last 14 days * Last 30 days * Specific month * Custom date range Select your preferred range to analyze traffic patterns over different periods. # AAAA Queries Without AAAA Records Source: https://bunny.net/docs/dns/troubleshooting/aaaa-queries-in-logs Why AAAA queries appear in DNS logs even when no AAAA records are configured. If you've only configured A records, you may still see a large number of AAAA queries in your [DNS logs](/docs/dns/logging). This is expected behavior. ## Why this happens The global shortage of IPv4 addresses has driven a large-scale migration to IPv6 networks. Most global infrastructure currently runs IPv4 or a dual stack of IPv4 and IPv6, but the migration to IPv6 is ongoing. As a result, many clients, browsers, and applications on IPv6-enabled networks now prioritize IPv6 lookups. When resolving a domain, they first issue an **AAAA** query to look for an IPv6 address. If no AAAA record is configured, they fall back to an **A** query for an IPv4 address. Bunny DNS logs these AAAA queries with an empty answer. The queries are harmless. ## Recommendation Configure both A and AAAA records for your domain when possible. This avoids the fallback round-trip and improves resolution time, which can improve overall performance for your website or application. # Can't Activate CDN Acceleration Source: https://bunny.net/docs/dns/troubleshooting/cdn-acceleration-not-activating Resolve errors when enabling CDN Acceleration on a DNS record. If you receive an error when trying to enable [CDN Acceleration](/docs/dns/records#cdn-acceleration) on a DNS record, this is usually caused by an existing Pull Zone that is already mapped to the hostname you're trying to accelerate. Hostnames are a unique map between a Pull Zone and its configuration, so only a single instance of a hostname can exist in the system at a time. ## Option 1: Remove the existing hostname Open the Pull Zone that already has the hostname configured and delete the hostname from it. You can then enable CDN Acceleration on the DNS record. ## Option 2: Use a PZ (Pull Zone) DNS record Instead of CDN Acceleration, use a **PZ** DNS record to point your domain or subdomain directly at the existing Pull Zone. PZ records are a Bunny-specific record type designed for this exact case. See [Bunny-specific records](/docs/dns/records#bunny-specific-records). For CDN Acceleration to work, your domain's nameservers must point to `kiki.bunny.net` and `coco.bunny.net` (or your custom nameservers if configured manually). # CDN Acceleration SSL Issues Source: https://bunny.net/docs/dns/troubleshooting/cdn-acceleration-ssl Troubleshoot SSL certificate issues on DNS records with CDN Acceleration enabled. When you enable [CDN Acceleration](/docs/dns/records#cdn-acceleration) on Bunny DNS, bunny.net automatically registers your domain with Bunny CDN, enables a secure proxy to your origin, and configures an SSL certificate for the accelerated hostname. If you see an SSL error after enabling CDN Acceleration, one of the following is usually the cause. ## DNS configuration issues Bunny.net uses Let's Encrypt to issue SSL certificates. Let's Encrypt validates ownership by contacting our system before issuing a certificate, so if your domain is not yet pointing its nameservers to bunny.net when the certificate is requested, issuance will fail. To resolve this: 1. Confirm your domain is pointing to bunny.net nameservers (typically `kiki.bunny.net` and `coco.bunny.net`). 2. Confirm the DNS record is correctly configured. 3. Wait for the nameserver change to propagate. This can take up to 24 hours. Keep CDN Acceleration disabled until the nameserver change has fully propagated. If you enabled CDN Acceleration before propagation completed, you can manually request the certificate from the connected Pull Zone's settings. ## Configuration delay Once a DNS record is accelerated, bunny.net immediately configures the HTTP domain across our global network and begins issuing the SSL certificate. The process usually completes within seconds but can occasionally take a few minutes. During this window, your domain remains reachable but may not respond correctly to HTTPS requests. The certificate will be issued after a series of retries, so wait a few minutes before investigating further. ## Still stuck? If you've followed the steps above and your accelerated domain is still unreachable over HTTPS, reach out to our Super Bunnies for further help. # Domain Unavailable on the Internet Source: https://bunny.net/docs/dns/troubleshooting/domain-unavailable Common reasons a domain is unreachable, including ERR_NAME_NOT_RESOLVED errors. If your domain isn't reachable on the internet (for example, your browser returns `ERR_NAME_NOT_RESOLVED`), one of the following is usually the cause. ## Nameservers are not configured correctly at your registrar To activate Bunny DNS, your domain must be configured at your registrar with the [nameservers](/docs/dns/nameservers) shown in the DNS panel for your zone. If the registrar isn't pointing at these nameservers, resolvers can't route queries to Bunny DNS. Some registrars take up to 24 hours to apply nameserver changes. Monitor the nameserver records at your registrar after making changes. ## Missing glue records for custom nameservers If you've configured [custom nameservers](/docs/dns/nameservers#custom-nameservers) in the bunny.net control panel that point back to your own domain, you must configure glue records at your registrar. Without them, resolvers can't find the DNS network responsible for the domain. The custom nameserver hostnames must also have A records that point to the same IPs as the glue records. ## DNSSEC was not disabled before transferring from another DNS service If you migrated from a DNS service that had DNSSEC enabled and didn't disable it before the transfer, the chain of trust breaks and resolvers reject Bunny DNS responses. To fix this: 1. Disable DNSSEC on your previous DNS service. 2. Re-enable [DNSSEC](/docs/dns/dnssec) on Bunny DNS. 3. Add the new DNSSEC records to your registrar if required. ## An exception in your DNS script If you're using [Scriptable DNS](/docs/dns/scriptable/introduction), an exception thrown by the script prevents Bunny DNS from returning records. Check your script for runtime errors. ## Missing A, AAAA, or CNAME records For a domain to be reachable, it needs the appropriate DNS records. To reach `example.com`, for instance, you need an A record pointing to an IPv4 address (such as `185.188.10.122`), an AAAA record for IPv6, or a CNAME pointing to another resolvable hostname. ## Typo in the domain name When troubleshooting, double-check that the queried domain matches the DNS zone or record exactly. A single character difference will cause resolution to fail. ## Debugging tools Bunny's [DNS Lookup tool](https://tools.bunny.net/dns-lookup) can perform global NS lookups to check propagation status and verify record responses from different regions. # RDR Record SSL Certificate Issues Source: https://bunny.net/docs/dns/troubleshooting/rdr-record-ssl Troubleshoot invalid SSL certificate errors on Redirect (RDR) DNS records. After creating a Redirect (RDR) DNS record, you may find that the redirected domain isn't serving a valid SSL certificate. The following are the common causes. ## The certificate is still being issued If you've just created the record, the certificate may still be issuing. Activation takes anywhere from a few seconds to a few minutes — wait before investigating further. ## The record was created before Bunny DNS was active If the RDR record was created before your domain was fully pointing to Bunny DNS, the certificate authority couldn't validate the domain and the certificate failed to issue. If your domain is now fully pointing to Bunny DNS, delete and recreate the Redirect record. The certificate should issue within a few minutes. ## CAA records are blocking Let's Encrypt If your domain has a custom CAA record that doesn't authorize Let's Encrypt, certificate issuance will fail. In this case, use a Pull Zone with a redirect Edge Rule instead and provide your own certificate for the domain. ## Other Let's Encrypt issues Bunny.net uses Let's Encrypt to issue SSL certificates. If none of the above applies, the issue is likely on the Let's Encrypt validation side. Run a check for your domain with the [Let's Debug tool](https://letsdebug.net/) to identify the problem. If Let's Debug doesn't find an issue, reach out to support from the control panel. # Platform Domains Source: https://bunny.net/docs/domains The domains and base URLs used across bunny.net for deployed apps, CDN delivery, and platform APIs. Use this page to look up the domains bunny.net assigns to your resources and the base URLs of each public API. ## Application URLs When you deploy code on bunny.net, you get a unique URL on the `bunny.run` domain so the app is reachable straight away. ### Magic Containers Adding a CDN endpoint to a Magic Containers app generates a URL on `bunny.run`. You can find it on the **Endpoints** tab. ```bash theme={null} mc-.bunny.run ``` To learn more, see [Endpoints](/docs/magic-containers/endpoints). ### Edge Scripting Publishing a script generates a URL on `bunny.run`, backed by an automatically provisioned pull zone. The URL appears on the script overview after the first publish. ```bash theme={null} -.bunny.run ``` The URL is set when the script is created and does not change if you later rename the script. To learn more, see [Deployments](/docs/scripting/deployments). ## CDN URLs Each pull zone you create gets a hostname on the `b-cdn.net` domain. ```bash theme={null} .b-cdn.net ``` To use your own hostname instead, see [Custom Hostnames](/docs/cdn/custom-hostname). ## Database URLs Each database you create gets a connection URL on the `lite.bunnydb.net` domain. ```bash theme={null} libsql://-.lite.bunnydb.net ``` To learn more, see [Connecting to a database](/docs/database/connect/authorization). ## Platform APIs The public APIs are served from the following base URLs. For request and response details, see the [API Reference](/docs/api-reference). | API | Base URL | | ---------------- | --------------------------------------- | | Core Platform | `https://api.bunny.net` | | Magic Containers | `https://api.bunny.net/mc` | | Edge Scripting | `https://api.bunny.net/compute` | | Shield | `https://api.bunny.net/shield` | | Storage | `https://{region}.storage.bunnycdn.com` | | Stream | `https://video.bunnycdn.com` | | CDN Logging | `https://logging.bunnycdn.com` | OpenAPI specifications are available on the [OpenAPI Specifications](/docs/openapi) page. ## Dashboard The bunny.net dashboard is at `dash.bunny.net`. # FAQs Source: https://bunny.net/docs/faq Frequently asked questions about bunny.net trials, billing, and accounts. ### What happens if I use up my trial balance before 14 days? If you exhaust your trial balance before the 14-day trial period ends, your trial will effectively end. To resume services, you can recharge your account with additional funds. Once you top up, your account will switch to a paid user status. ### Do I need to add a payment card to use the trial? No, adding a payment card is optional. You'll still get \$20 in trial credits without it. However, adding a card boosts your trial balance to \$50 and unlocks additional features and products. ### If I add billing information, will bunny.net charge me automatically? No, bunny.net will only charge you automatically if you enable Automatic Recharge. Adding billing information alone does not trigger automatic charges. ### Why can't I use a prepaid or virtual card? To ensure fair use of our free trial, we require a valid payment card that can be verified. Unfortunately, prepaid cards, gift cards, and some virtual cards are not accepted. ### Will I be charged after adding my payment card? No, we won't charge your card during the trial. A small authorization (typically \$0 or \$1) might be made to verify your card, but this will be reversed. ### What happens to my trial credits when the 14-day trial expires? When your trial period ends, any remaining trial credits are removed from your account. Unused trial credits are not carried over or converted to paid credits. To continue using bunny.net after your trial, add funds to your account. ### Can I end my trial early? Yes, you can switch to a paid account anytime before your trial ends by contacting our support team. Keep in mind that any unused trial credits will expire when you end the trial. ### How are charges calculated during the trial? Charges are calculated according to our standard pricing for each product or service. You can view detailed usage and billing information in your account dashboard. ### Can I get a refund? bunny.net operates on a prepaid model, so payments are generally non-refundable. We make exceptions in certain cases, such as a payment made by mistake, or a payment made within one month of account creation where the balance hasn't been used. If you believe you're eligible, [contact support](https://bunny.net/contact/) and we'll review your request. # Developer Hub Source: https://bunny.net/docs/index Everything from quickstarts to advanced guides, you will find it here. Sign up for free, and start building with bunny.net Programmatically manage account resources and actions Manage bunny.net resources from your terminal Official plugins and tools for your favorite platforms ## Explore bunny.net Build, optimize, and scale faster with bunny.net Accelerate and protect your content globally Video streaming and delivery platform Global object storage Automatic image and web optimization Deploy any app anywhere with Docker Deploy serverless code at the edge Serverless SQLite over HTTP Stay protected and online no matter what Unlock the true power of DNS with an ultra-fast scriptable platform *** ## Need help? Get super fast support from industry experts Share projects, discuss ideas, and get community support # Configuration Source: https://bunny.net/docs/integrations/cpanel/configuration Configure the bunny.net cPanel plugin settings including API keys, nameservers, and logging. The bunny.net cPanel plugin uses a configuration file located at `/etc/bunnynet.conf`. This file is created automatically during setup, but you can edit it to customize the plugin behavior. ## Configuration file ```bash theme={null} /etc/bunnynet.conf ``` After making changes to the configuration file, restart the daemon: ```bash theme={null} systemctl restart bunnynet-cpanel-daemon ``` ## Required settings | Key | Description | | :----------------- | :------------------------------------------------------------------------------------------------------------- | | `BUNNYNET_API_KEY` | Your bunny.net account API key. Available in [dash.bunny.net](https://dash.bunny.net). | | `WHM_API_KEY` | The WHM API token. Automatically generated during setup. Managed in **WHM > Development > Manage API Tokens**. | ## Optional settings | Key | Description | Default | | :------------------ | :------------------------------------------- | :---------------------- | | `NS1` | Primary nameserver | `kiki.bunny.net` | | `NS2` | Secondary nameserver | `coco.bunny.net` | | `SOA_EMAIL` | SOA email address | `hostmaster@bunny.net` | | `DOH_RESOLVER` | DNS over HTTPS resolver | `https://dns.quad9.net` | | `LOG_LEVEL` | Log level (`debug`, `info`, `warn`, `error`) | `warn` | | `URL_PURGE_WORKERS` | Number of workers for URL cache purging | `2` | ## DNS cluster settings The DNS cluster configuration is available in **WHM > Clusters > DNS Cluster**. In addition to the API key and DNS role, you can enable: * **Sync all DNS zones** — Automatically synchronize all existing DNS zones to the cluster. Enable this if you want all current zones to be pushed to bunny.net DNS when setting up the cluster. DNS Cluster configuration ## Hosting packages You can configure bunny.net features per hosting package in **WHM > Packages > Edit a Package**. The following options are available under the **bunny.net** section: * **Max bunny.net websites** — Limit the number of websites that can use bunny.net per account * **Bunny Optimizer** — Enable or disable image optimization for the package * **Bunny Shield** — Enable or disable DDoS protection and WAF for the package Make sure to set **Max bunny.net websites** to at least `1`. Setting it to `0` will prevent customers on that package from enabling bunny.net for any of their websites. Hosting package configuration ## Hiding the plugin for certain packages If you want to hide the bunny.net plugin from customers on specific packages, you can use WHM's Feature Manager to create a feature list that excludes it. In WHM, go to **Packages > Feature Manager**. Create a new feature list or edit an existing one. In the feature list, uncheck **bunny.net** to hide the plugin from cPanel for customers using this feature list. Go to **Packages > Edit a Package**, select the package you want to restrict, and set the **Feature List** to the one you created. Customers on packages with this feature list will no longer see the bunny.net plugin in their cPanel. # Custom Nameservers Source: https://bunny.net/docs/integrations/cpanel/custom-nameservers Use your own branded nameservers with the bunny.net cPanel plugin. By default, the bunny.net cPanel plugin uses `kiki.bunny.net` and `coco.bunny.net` as nameservers. You can replace these with your own branded nameservers (e.g., `ns1.example.com`). ## Set up custom nameservers Add the following records to your domain's DNS zone, pointing your nameservers to bunny.net: ``` ns1.example.com. IN A 91.200.176.1 ns1.example.com. IN AAAA 2400:52e0:fff0::1 ns2.example.com. IN A 109.104.147.1 ns2.example.com. IN AAAA 2400:52e0:fff2::1 ``` Edit `/etc/bunnynet.conf` and add your custom nameservers: ``` NS1=ns1.example.com NS2=ns2.example.com SOA_EMAIL=hostmaster@example.com ``` Apply the changes by restarting the daemon: ```bash theme={null} systemctl restart bunnynet-cpanel-daemon ``` The plugin will now display your custom nameservers instead of the default bunny.net ones. # cPanel Plugin Source: https://bunny.net/docs/integrations/cpanel/index Integrate bunny.net services into your cPanel/WHM server for easy CDN, DNS, and optimization management. The official bunny.net cPanel plugin lets hosting providers enable DNS, CDN, Shield, and Optimizer directly from WHM. Install the plugin on your server, connect your bunny.net account, and offer bunny.net services to your customers with minimal configuration. The plugin repository is protected. To get access, [contact our sales team](https://bunny.net/contact-sales/). ## What's included * **Bunny DNS** — Automatic DNS cluster integration with WHM * **Bunny CDN** — CDN acceleration for hosted websites * **Bunny Optimizer** — Image compression and optimization * **Bunny Shield** — DDoS protection and WAF * **Per-package controls** — Configure bunny.net features per hosting package The plugin does not currently integrate with billing systems like WHMCS. Optimizer and Shield can still be offered to your customers, but upselling them directly through the cPanel plugin is not possible. To offer these services, include them in your hosting packages upfront. ## WHM setup Plugin configuration file, DNS cluster settings, and hosting package controls. Use your own branded nameservers instead of the default bunny.net ones. # Quickstart Source: https://bunny.net/docs/integrations/cpanel/quickstart Install and configure the bunny.net cPanel plugin on your WHM server. This guide walks you through installing and setting up the bunny.net cPanel plugin on your server. ## Prerequisites * cPanel/WHM server with root access * A bunny.net account with an API key * Access to the plugin repository (requires approval) The plugin repository is protected. To get access, [contact our sales team](https://bunny.net/contact-sales/). ## Install the plugin Install the plugin using either APT or RPM depending on your server's package manager. Configure the APT repository: ```bash theme={null} cat < /etc/apt/auth.conf.d/bunnynet-cpanel.conf machine https://cpanel-repo.bunny.net login username-goes-here password password-goes-here EOF ``` ```bash theme={null} cat < /etc/apt/sources.list.d/bunnynet-cpanel.sources Enabled: yes Types: deb URIs: https://cpanel-repo.bunny.net/deb Suites: stable Components: main Architectures: amd64 Signed-By: -----BEGIN PGP PUBLIC KEY BLOCK----- . mQINBGh3oPsBEADPYVnsIf4W10dG90KtpeUciVROjJB+/cEzBgU1nHPGcM1o043R 4LYfbTkDN3kYQwp0SVBn8BeNyYHIzXjothRV9w41J6dzA074GcFoyNE0JsihdX/t 0ZMO0VunkcSYE3nQEq5sw5pweb6DB0nqYSGd8uG+0baIjoOJmDTTDysyLvSp8YLd 4NRK48AC7gCTOrYsDm1EqJFO64OPn27DdfB0mte/LNIxKBrUkX2omw0IPv89lIN8 0ckeMW9PgNRHuUFLttNWProw3qSAkqu29R4hwH374qJmooO0NP8Jh41KmG5uefrR PnWM29+CW1EEbvqgh7un7kB9NQdDGfMhTryfXsoYDjkqfYlM5+KThMsgWFJILPdR NyB/UvyyuL40XFydKGiEFUa/OjuuPjub9Zdhec28tpHrg73yFyr9AtjySdpSiED3 vg/gBHTnzmKgkRJH61Vo6/rqcGCk3ZwsARbQn1Mg+AwQuYS8MQfSDzon3jZ8poAU V2ChYtdT2w0vBlZ2aaVrlv9e+2hcLZq5qyZzYB4exfhdm0kWElAYlig+rfFQbUcz E26gpfbNMqWK+/dR3HYmqHmt2PZFha/OvG7Au5mSGnocH4vfO+ih+dBmPvXUYzDC X4BJ83/qQS8OeFBlvRe1bh/Wom+SuHC8U6VmcTdhq7TDYkuIyeu884hQlwARAQAB tDFidW5ueS5uZXQgY1BhbmVsIFBsdWdpbiA8Y3BhbmVsLXBsdWdpbkBidW5ueS5u ZXQ+iQJOBBMBCgA4FiEELDzERi4ZAA/HE+ZlMRADpKDgpC8FAmh3oPsCGy8FCwkI BwIGFQoJCAsCBBYCAwECHgECF4AACgkQMRADpKDgpC93WRAAlo0uWrrFgzfCI3KL ubrva9jBeuOdJ2FQFpKDIglUN7hCRZGF8+zfdbDizTORqoKuI+IGdpcW4Xlv3eik 9j7gQ4AMK+4xsnkpnzobCb40RlZE3wCaEHhMzAOko3o75M+5vYXRu9Io+2LLXzr9 Bgh5RQQ7wVmG9CAhkBwGhD+PNfZIEZjqnobLDA9Ts7uN4hSAlpgQeLRfae3E5Xuu CkRkMZYDVJc3O/uKsMLziw4p0Ck7URjHS0C/5c0qLtZYfupTW9rnVqmsFz3enTni IIfBPcyP95RtmVCuKttqHRHvcgokWqd6y3Ts1ExCKPP1aVeohUQtc+UwXy+J/Xc2 wi/kQH7BGQZp5AWtaFQ6ORYek/Avtjeo1mySHmAAhNAMKJd3nzeBXnX5IrpAy6So /tcVL3Y5g4dLeEMJBBHzZqVKPe4LNVynV+k1zsCG6MCuof5KfGKLlVG7GhkeV5bg yLDg7CRXU7Hi3tibkHJdkADA+EGomjmALl3I06oDM91Xsiqi98/xkDGj2Wu6wHlf X6HTi4xWsN2d0aMi0kLyY693xAJqdmHCf8L9TjTHptKCbSxgzyy6bfF80w9nVPag uAnXW7FRIZ0WsOArKm0bZdCvvoddH0/aDTaDkLa+xhoflKpbBpkFbufc1bNb2uke 5U00QjAZa8bU1qsdyvOteL48lmk= =/1ex -----END PGP PUBLIC KEY BLOCK----- EOF ``` Install the packages: ```bash theme={null} apt install bunnynet-cpanel bunnynet-cpanel-daemon ``` Configure the RPM repository: ```bash theme={null} cat < /etc/yum.repos.d/bunnynet-cpanel.repo [bunnynet-cpanel] name=bunny.net cPanel Plugin baseurl=https://cpanel-repo.bunny.net/rpm gpgcheck=1 gpgkey=https://cpanel-repo.bunny.net/gpg.key username=username-goes-here password=password-goes-here EOF ``` Install the packages: ```bash theme={null} dnf install bunnynet-cpanel bunnynet-cpanel-daemon ``` ## Set up the plugin After installation, open WHM and navigate to the **bunny.net cPanel plugin** page. The plugin requires a DNS cluster to be configured. Click **Setup DNS cluster** to open the DNS Cluster configuration page. The DNS Role must be set to **write-only**. bunny.net cPanel plugin requirements On the DNS Cluster page, select **cPanel** as the Backend Type and click **Configure**. DNS Cluster setup Enter your **bunny.net API key** and set the **DNS Role** to **Standalone**. You can also enable **Sync all DNS zones** to automatically synchronize all existing DNS zones to the cluster. You can find your API key in the [bunny.net dashboard](https://dash.bunny.net). DNS Cluster configuration Back on the plugin page, enter your **bunny.net API key** and click **Setup**. bunny.net API key entry The setup is complete. The plugin will display your API keys and the available pricing zones. The bunny.net API key also needs to be updated on the DNS cluster. If you see a warning about this, update it in **Clusters > DNS Cluster**. Setup complete After setup, you can configure bunny.net features per hosting package under **Packages > Edit a Package**. # Authentication Source: https://bunny.net/docs/integrations/terraform/authentication Learn how to authenticate with the bunny.net Terraform provider using your API key. The bunny.net Terraform provider supports two authentication methods. Team member API keys are not supported. You must use your account's main API key. ## Using the provider configuration Set the `api_key` attribute directly in your provider configuration: ```hcl theme={null} provider "bunnynet" { api_key = "your-api-key" } ``` ## Using environment variables Alternatively, set the `BUNNYNET_API_KEY` environment variable: ```bash theme={null} export BUNNYNET_API_KEY="your-api-key" ``` Then configure the provider without the `api_key` attribute: ```hcl theme={null} provider "bunnynet" {} ``` ## Provider configuration options | Attribute | Description | Default | | ------------------- | ------------------------------ | ----------------------------- | | `api_key` | Your bunny.net API key | `BUNNYNET_API_KEY` env var | | `api_url` | Primary API endpoint | `https://api.bunny.net` | | `stream_api_url` | Video streaming API endpoint | `https://video.bunnycdn.com` | | `container_api_url` | Container service API endpoint | `https://api-mc.opsbunny.net` | You can find your API key in the [bunny.net dashboard](https://dash.bunny.net/account/api-key). # Data Sources Source: https://bunny.net/docs/integrations/terraform/data-sources Overview of all data sources available in the bunny.net Terraform provider. Data sources allow you to fetch information about existing bunny.net resources for use in your Terraform configuration. For detailed configuration options and examples, see the [official provider documentation](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs). ## Pull Zone | Data Source | Description | | --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | | [bunnynet\_pullzone](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/data-sources/pullzone) | Fetch an existing pull zone | | [bunnynet\_pullzone\_access\_lists](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/data-sources/pullzone_access_lists) | Fetch access lists for a pull zone | ## DNS | Data Source | Description | | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------- | | [bunnynet\_dns\_zone](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/data-sources/dns_zone) | Fetch an existing DNS zone | | [bunnynet\_dns\_record](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/data-sources/dns_record) | Fetch an existing DNS record | ## Compute | Data Source | Description | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | | [bunnynet\_compute\_container\_app\_container](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/data-sources/compute_container_app_container) | Fetch container details | | [bunnynet\_compute\_container\_app\_container\_endpoint](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/data-sources/compute_container_app_container_endpoint) | Fetch container endpoint details | | [bunnynet\_compute\_container\_imageregistry](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/data-sources/compute_container_imageregistry) | Fetch image registry details | ## Utility | Data Source | Description | | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------- | | [bunnynet\_region](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/data-sources/region) | Fetch available regions | | [bunnynet\_video\_language](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/data-sources/video_language) | Fetch supported video languages | # Managing Pullzones for Edge Scripting Source: https://bunny.net/docs/integrations/terraform/edgescript-pullzone Learn how to manage the association between Edge Scripts and Pullzones via Terraform Edge Scripts are required to be attached to a Pullzone in order to be executed. Here's how you can manage this association via Terraform. ## Terraform-managed Scripts If you already have a Pullzone managed via Terraform and you want to attach an Edge Script to it, you'll need the following attributes on your resource: ```hcl theme={null} resource "bunnynet_pullzone" "my-app" { name = "my-app" origin { type = "ComputeScript" url = "https://bunnycdn.com" script = bunnynet_compute_script.my-script.id } routing { filters = ["scripting"] } } ``` ```hcl theme={null} resource "bunnynet_pullzone" "my-app" { name = "my-app" origin { type = "OriginUrl" url = "https://my-origin.example.com" middleware_script = bunnynet_compute_script.my-script.id } routing { filters = ["scripting"] } } ``` ## Edge Script created via Dashboard Edge Scripts created via the bunny.net Dashboard already are attached to a Pullzone, so you need to import it into your Terraform configuration: 1. Define the resource ```hcl theme={null} resource "bunnynet_pullzone" "my-app" { name = "my-app" origin { type = "ComputeScript" script = bunnynet_compute_script.my-script.id url = "https://bunnycdn.com" } routing { filters = ["scripting"] } } ``` 2. Import the pullzone Run `terraform import bunnynet_pullozne.my-app PZID`, where `PZID` is the ID of the Pullzone created alongside the Edge Script. # Importing stream library Source: https://bunny.net/docs/integrations/terraform/importing-stream-library Learn how to import an existing Stream library and its associated resources into Terraform. When you create a Stream library, bunny.net automatically provisions a Pull Zone and Storage Zone. To manage these resources with Terraform, you need to import them into your state. Create or update your Terraform configuration with the stream library resource: ```hcl theme={null} resource "bunnynet_stream_library" "library" { name = "my-library" } ``` Run apply to create or sync the library: ```bash theme={null} terraform apply ``` Define the associated Pull Zone and Storage Zone resources, along with import blocks: ```hcl theme={null} resource "bunnynet_pullzone" "pullzone" { name = "my-library-pullzone" origin { type = "StorageZone" storagezone = bunnynet_stream_library.library.storage_zone } routing { tier = "Volume" } } resource "bunnynet_storage_zone" "storage" { name = "my-library-storage" region = "DE" zone_tier = "Standard" } import { to = bunnynet_pullzone.pullzone id = bunnynet_stream_library.library.pullzone } import { to = bunnynet_storage_zone.storage id = bunnynet_stream_library.library.storage_zone } ``` Run apply to import the resources into your Terraform state: ```bash theme={null} terraform apply ``` The Pull Zone and Storage Zone are now managed by Terraform. Create or update your Terraform configuration with the stream library resource: ```hcl theme={null} resource "bunnynet_stream_library" "library" { name = "my-library" } ``` Run apply to create or sync the library: ```bash theme={null} terraform apply ``` Define the associated Pull Zone and Storage Zone resources, with outputs to retrieve their IDs: ```hcl theme={null} resource "bunnynet_pullzone" "pullzone" { name = "my-library-pullzone" origin { type = "StorageZone" storagezone = bunnynet_stream_library.library.storage_zone } routing { tier = "Volume" } } resource "bunnynet_storage_zone" "storage" { name = "my-library-storage" region = "DE" zone_tier = "Standard" } output "pullzone_id" { value = bunnynet_stream_library.library.pullzone } output "storage_zone_id" { value = bunnynet_stream_library.library.storage_zone } ``` Run plan to retrieve the Pull Zone and Storage Zone IDs from the outputs: ```bash theme={null} terraform plan ``` Note the IDs shown in the output. Use the IDs to import each resource into your Terraform state: ```bash theme={null} terraform import bunnynet_pullzone.pullzone terraform import bunnynet_storage_zone.storage ``` Replace `` and `` with the actual IDs from the previous step. # Documentation Source: https://bunny.net/docs/integrations/terraform/index The bunny.net Terraform provider allows users to manage Bunny.net resources using Terraform's infrastructure as code (IaC) capabilities. This provider enables the automation and configuration of Bunny.net's content delivery network (CDN), edge storage, and DNS services, providing a consistent and scalable approach to infrastructure management. # Key Features * **CDN Zone Management**: Create, update, and delete CDN zones, including caching, edge rules, and security configurations. * **Edge Storage Management**: Provision and manage Bunny.net edge storage, including storage zones and replication settings. * **DNS Management**: Manage DNS records associated with Bunny.net, ensuring seamless integration with other DNS services supported by Terraform. * **Security and Optimization**: Configure security features such as DDoS protection and performance optimization settings. * **Stream**: Manage video libraries and collections. * **Scripting**: Deploy and manage edge scripts for serverless compute. * **Magic Containers**: Deploy and manage containerized applications at the edge. ## Resources For more detailed configuration examples and advanced usage, please visit our [bunny.net terraform documentation](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs). # Quickstart Source: https://bunny.net/docs/integrations/terraform/quickstart Get started with the bunny.net Terraform provider to manage your infrastructure as code. # What you'll need Before you begin, make sure you have: * A [bunny.net account](https://dash.bunny.net/auth/register) * [Terraform](https://developer.hashicorp.com/terraform/install) installed on your machine * Your [bunny.net API key](https://dash.bunny.net/account/api-key) ## Quickstart Create a new directory and add the provider configuration: ```bash theme={null} mkdir my-terraform-project cd my-terraform-project ``` Create a `provider.tf` file with the following content: ```hcl theme={null} terraform { required_providers { bunnynet = { source = "BunnyWay/bunnynet" } } } provider "bunnynet" { api_key = "your-api-key" } ``` Add the bunny.net provider to your existing `provider.tf`: ```hcl theme={null} terraform { required_providers { bunnynet = { source = "BunnyWay/bunnynet" } # ... your other providers } } provider "bunnynet" { api_key = "your-api-key" } ``` Clone the official template repository: ```bash theme={null} git clone https://github.com/BunnyWay/terraform-provider-bunnynet-template.git my-terraform-project cd my-terraform-project ``` Update the `api_key` in the configuration file with your API key. You can also use the `BUNNYNET_API_KEY` environment variable instead of hardcoding your API key. See [Authentication](/docs/integrations/terraform/authentication) for more options. Run the init command to download the bunny.net provider: ```bash theme={null} terraform init ``` Create a `main.tf` file with your infrastructure. This example creates a Storage Zone with an index file and a Pull Zone: ```hcl theme={null} resource "bunnynet_storage_zone" "my_storage" { name = "my-project-name" zone_tier = "Edge" region = "DE" } resource "bunnynet_storage_file" "index" { zone = bunnynet_storage_zone.my_storage.id path = "index.html" content = "

Hello world!

Deployed with Terraform.

" } resource "bunnynet_pullzone" "my_cdn" { name = bunnynet_storage_zone.my_storage.name origin { type = "StorageZone" storagezone = bunnynet_storage_zone.my_storage.id } routing { tier = "Standard" } } ``` Replace `my-project-name` with your desired project name.
Preview the changes Terraform will make: ```bash theme={null} terraform plan ``` Apply the configuration to create your resources: ```bash theme={null} terraform apply ``` Type `yes` when prompted to confirm. ``` bunnynet_storage_zone.my_storage: Creating... bunnynet_storage_zone.my_storage: Creation complete after 3s bunnynet_pullzone.my_cdn: Creating... bunnynet_pullzone.my_cdn: Creation complete after 0s bunnynet_storage_file.index: Creating... bunnynet_storage_file.index: Creation complete after 0s Apply complete! Resources: 3 added, 0 changed, 0 destroyed. ``` Open the [bunny.net dashboard](https://dash.bunny.net) to see your new resources. Your content is now being served through the bunny.net CDN.
## Next steps Explore all available Terraform resources Query existing bunny.net resources Configure provider authentication options Official Terraform Registry documentation # Resources Source: https://bunny.net/docs/integrations/terraform/resources Overview of all resources available in the bunny.net Terraform provider. The bunny.net Terraform provider includes resources for managing CDN, storage, DNS, streaming, and developer platform services. For detailed configuration options and examples, see the [official provider documentation](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs). ## Pull Zone Manage CDN pull zones, hostnames, edge rules, and optimization settings — [learn more](/docs/cdn). | Resource | Description | | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------- | | [bunnynet\_pullzone](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/resources/pullzone) | CDN pull zone | | [bunnynet\_pullzone\_hostname](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/resources/pullzone_hostname) | Custom hostname for a pull zone | | [bunnynet\_pullzone\_edgerule](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/resources/pullzone_edgerule) | Edge rule for request/response manipulation | | [bunnynet\_pullzone\_optimizer\_class](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/resources/pullzone_optimizer_class) | Image optimizer class | | [bunnynet\_pullzone\_ratelimit\_rule](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/resources/pullzone_ratelimit_rule) | Rate limiting rule | | [bunnynet\_pullzone\_waf\_rule](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/resources/pullzone_waf_rule) | Web Application Firewall rule | | [bunnynet\_pullzone\_shield](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/resources/pullzone_shield) | Origin shield configuration | | [bunnynet\_pullzone\_access\_list](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/resources/pullzone_access_list) | Access control list | ## Storage Manage edge storage zones and files — [learn more](/docs/storage). | Resource | Description | | ----------------------------------------------------------------------------------------------------------------------- | ---------------------- | | [bunnynet\_storage\_zone](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/resources/storage_zone) | Edge storage zone | | [bunnynet\_storage\_file](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/resources/storage_file) | File in a storage zone | ## DNS Manage DNS zones and records — [learn more](/docs/dns). | Resource | Description | | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | | [bunnynet\_dns\_zone](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/resources/dns_zone) | DNS zone | | [bunnynet\_dns\_record](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/resources/dns_record) | DNS record | | [bunnynet\_dns\_script](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/resources/dns_script) | DNS script | | [bunnynet\_dns\_script\_variable](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/resources/dns_script_variable) | DNS script variable | ## Stream Manage video libraries, collections, and videos — [learn more](/docs/stream). | Resource | Description | | --------------------------------------------------------------------------------------------------------------------------------- | ---------------- | | [bunnynet\_stream\_library](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/resources/stream_library) | Video library | | [bunnynet\_stream\_collection](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/resources/stream_collection) | Video collection | | [bunnynet\_stream\_video](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/resources/stream_video) | Video | ## Scripting Manage edge scripts for serverless compute at the edge — [learn more](/docs/scripting). | Resource | Description | | ---------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | | [bunnynet\_compute\_script](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/resources/compute_script) | Edge compute script | | [bunnynet\_compute\_script\_variable](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/resources/compute_script_variable) | Script environment variable | | [bunnynet\_compute\_script\_secret](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/resources/compute_script_secret) | Script secret | ## Magic Containers Manage containerized applications deployed to the edge — [learn more](/docs/magic-containers). | Resource | Description | | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | | [bunnynet\_compute\_container\_app](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/resources/compute_container_app) | Container application | | [bunnynet\_compute\_container\_imageregistry](https://registry.terraform.io/providers/BunnyWay/bunnynet/latest/docs/resources/compute_container_imageregistry) | Container image registry | # Enable Bunny DNS and CDN Acceleration Source: https://bunny.net/docs/integrations/wordpress/dns Migrate your domain to Bunny DNS and enable CDN Acceleration. This is a prerequisite for the Bunny Offloader and Bunny Shield features in the WordPress plugin. Several features of the bunny.net WordPress plugin, including **Bunny Offloader** and **Bunny Shield**, require that your domain uses Bunny DNS with CDN Acceleration enabled. Traffic must flow through the bunny.net CDN proxy so the plugin can offload media to Storage and so Shield can inspect and mitigate requests at the edge. This guide walks through migrating your domain to Bunny DNS, enabling CDN Acceleration, and verifying that everything is wired up correctly. Once complete, head to [Enable Offloader](/docs/integrations/wordpress/offloader) or [Enable Shield](/docs/integrations/wordpress/shield) to turn on the features themselves. These features are only supported when using the official bunny.net plugin and managing DNS through bunny.net. They are not compatible with third-party plugins or external DNS providers. ## Prerequisites * A [bunny.net account](https://dash.bunny.net) * Access to manage your domain's DNS settings ## Step 1: Migrate your domain to Bunny DNS Skip this step if your domain is already using Bunny DNS. Log in to bunny.net and go to **Delivery → DNS → Add DNS Zone**. Enter your domain name and click **Add DNS Zone**. Follow the setup instructions, then click **Okay, I'm done**. Click **Add Record**, enter the required details, and continue adding records as needed. Confirm your DNS settings have propagated using the [bunny DNS Lookup Tool](https://tools.bunny.net/dns-lookup). Look for NS records pointing to `kiki.bunny.net` or `coco.bunny.net`. When creating a DNS zone, it must be based on the root domain (e.g., `domain.com`). Creating a zone for `www.domain.com` or `something.domain.com` is not valid. When migrating your DNS, make sure to transfer **all** existing DNS records, not just the website-related ones. Failing to do so can result in critical services (like email) going offline. ## Step 2: Enable CDN Acceleration in the plugin If your domain was already using Bunny DNS when you installed the plugin, the "Enable CDN Acceleration" option may not appear. Skip to Step 3 to check if it's already enabled. If not, you can reset the plugin to start the configuration from scratch. In the bunny.net dashboard, go to the **DNS** section. Click the button in the **CDN Proxy** (or CDN acceleration) column next to each DNS record. CDN Proxy toggle In your WordPress Admin Panel, navigate to **bunny.net → Offloader** or **bunny.net → Shield**. Follow the on-screen prompts and click **Enable CDN Acceleration** (or **Enable Bunny DNS** from the Shield tab). You'll receive an orange confirmation message once configured. Enable CDN Acceleration Enabling CDN acceleration disconnects your current Pull Zone and creates a new one. Make sure to copy any essential settings (Edge Rules, Cache settings, Optimizer configurations) from the old zone to the new one. The old Pull Zone can be safely deleted after migration. ## Step 3: Confirm CDN Acceleration is working Verify your setup using any of these methods: * In the bunny.net **DNS** section, a green **CDN Proxy** icon next to your DNS record indicates proper CDN integration. Green CDN Proxy
icon * Inspect response headers from your site. Look for: * `CDN-ProxyVer: 1.04` * `CDN-RequestPullSuccess: True` * `CDN-RequestPullCode: 200` * In your WordPress Admin Panel, go to **bunny.net → About**. Under **Technical Information**, check the **Request Headers** section. If you see both `Cdn-RequestId` and `Via: BunnyCDN`, everything is working. Request Headers showing CDN
confirmation ## Next steps Offload media files to Bunny Storage for scalable, globally replicated delivery. Turn on WAF and DDoS protection with Bunny Shield. # WordPress Plugin Source: https://bunny.net/docs/integrations/wordpress/index Speed up your WordPress site with the official bunny.net plugin for easy CDN integration, content offloading, and optimization. The official [bunny.net WordPress plugin](https://wordpress.org/plugins/bunnycdn/) integrates bunny.net's delivery optimization services into your WordPress site. Install the plugin, connect your account, and start delivering content faster with just a few clicks. ## What's included * **Bunny CDN** — Rewrites static content links with CDN URLs to improve loading times * **Bunny Shield** — Protects your site with a Web Application Firewall (WAF) and DDoS mitigation * **Bunny Optimizer** — Compresses files and images to reduce file size * **Bunny Offloader** — Transfers media files to Bunny Storage with multi-region replication * **Bunny Stream** — Upload and embed videos with a dedicated block and shortcode * **Bunny Fonts** — GDPR-compliant fonts hosted within the EU, with no tracking Plugin version lifecycle, PHP, and WordPress version compatibility. ## Guides Migrate your domain to Bunny DNS and turn on CDN Acceleration — a prerequisite for Offloader and Shield. Offload media files to Bunny Storage for scalable, globally replicated delivery. Turn on WAF and DDoS protection with Bunny Shield. Move files from Bunny Storage back to your WordPress media library. Embed Bunny Stream videos using the Stream Video block or shortcode. ## Third-party plugins Set up bunny.net CDN with the WP Rocket caching plugin. Set up bunny.net CDN with the W3 Total Cache plugin. # Migrate Media Files from Bunny Storage Source: https://bunny.net/docs/integrations/wordpress/move-files-to-wordpress Move media files that were previously offloaded to a Bunny Storage Zone back to your WordPress server In some cases, you may want to migrate media files that were previously offloaded to a Bunny Storage Zone back to your WordPress server. This could be to simplify management, regain file ownership, or consolidate your hosting setup. Follow this step-by-step guide to safely complete the process without affecting your site's functionality. Before proceeding: Always create a complete backup of your WordPress files and database. This ensures you can restore your site if needed. ## Step 1: Copy Files from Bunny Storage 1. In your WordPress Admin, go to the **bunny.net → Offloader** tab. 2. Note the **Storage Zone** listed; this is where your offloaded files are currently stored. 3. Download or transfer the files from the Storage Zone to your WordPress server. You can do this via FTP or by downloading from the Bunny.net Storage dashboard. 4. Ensure you preserve the original directory structure so WordPress can correctly locate the files. Content offloading ## Step 2: Update the WordPress Database Once files are back on your server, you'll need to inform WordPress that these files are no longer offloaded. Run the following SQL query (using phpMyAdmin or your preferred database tool) to remove the offload markers: ```sql theme={null} DELETE FROM wp_postmeta WHERE meta_key = '_bunnycdn_offloaded'; ``` This removes the `_bunnycdn_offloaded` meta entries that tell WordPress the file is stored externally. The name of the `wp_postmeta` table may vary depending on the table prefix used in your WordPress installation. While the `postmeta` suffix remains the same, the prefix (e.g., `wp`, `wp3c_`) may differ, resulting in names like `wp3c_postmeta`. To confirm your prefix, check your database using phpMyAdmin, WP-CLI, or by listing the tables directly. Make sure you have a database backup before running any queries. ## Verify Your Changes **If you're no longer using Bunny DNS:** Verify that your media files load correctly. If everything works, you can delete the Storage Zone to stop recurring charges. **If you're still using Bunny DNS:** Your files may still be served from the Storage Zone. To test if they now load from your server: * Disable the Edge Rule called **WordPress Content Offloading**. * Purge the cache for your Pull Zone. Edge Rules Purge Once verified, you can safely remove the files from the Storage Zone. # Enable Offloader Source: https://bunny.net/docs/integrations/wordpress/offloader Turn on Bunny Offloader in the bunny.net WordPress plugin to automatically transfer media files to Bunny Storage. **Bunny Offloader** automatically transfers media files to Bunny Storage, enabling multi-region replication, near-unlimited scalability, and up to 5x faster performance compared to traditional delivery methods. Offloader requires Bunny DNS with CDN Acceleration enabled. If you haven't set this up yet, follow [Enable Bunny DNS and CDN Acceleration](/docs/integrations/wordpress/dns) first. ## Enable the Offloader In your WordPress Admin Panel, navigate to **bunny.net → Offloader**. Toggle on **Content Offloading**, adjust settings to your preference, and click **Save Settings**. Content Offloading settings You can monitor offloaded content through the Offloader dashboard. # Quickstart Source: https://bunny.net/docs/integrations/wordpress/quickstart Install and configure the bunny.net WordPress plugin to speed up your website with CDN acceleration. This guide walks you through installing and configuring the bunny.net WordPress plugin on your site. ## Prerequisites * WordPress 6.7 or higher * PHP 8.1 or higher The plugin will not appear in the WordPress plugin directory on earlier versions of WordPress or PHP. ## Install the plugin Log in to your WordPress admin panel. Click **Plugins** in the sidebar, then click **Add New Plugin**. In the search box, type **BunnyCDN**. Find the BunnyCDN plugin in the results and click **Install Now**. BunnyCDN plugin search results After installation completes, click **Activate**. ## Configure the plugin In the WordPress sidebar, select **bunny.net** and click **Login / Create Account**. bunny.net plugin menu Create a new bunny.net account or log in with your existing credentials. After logging in, select **Integration Wizard**. Integration Wizard Enter the URL of your WordPress website and click **Confirm URL**. The wizard uses this URL to configure your CDN integration. Confirm URL If your account already has a Pull Zone for this URL, you'll be prompted to use the existing one or create a new one. Setup complete CDN acceleration is enabled by default once the setup completes. Your website will immediately start serving static assets through the CDN. # Enable Bunny Shield Source: https://bunny.net/docs/integrations/wordpress/shield Turn on Bunny Shield in the bunny.net WordPress plugin to protect your site with WAF and DDoS mitigation. **Bunny Shield** protects your site from attacks, malicious traffic, and service disruption using a built-in Web Application Firewall (WAF) and DDoS mitigation. Shield requires Bunny DNS with CDN Acceleration enabled. If you haven't set this up yet, follow [Enable Bunny DNS and CDN Acceleration](/docs/integrations/wordpress/dns) first. ## Enable Bunny Shield In your WordPress Admin Panel, navigate to **bunny.net → Shield**. If DNS and CDN Acceleration are not yet configured, the Shield tab prompts you to enable them first. The **Enable Bunny DNS** button takes you through the same setup covered in [Enable Bunny DNS and CDN Acceleration](/docs/integrations/wordpress/dns). Bunny Shield tab in the WordPress plugin Once acceleration is active, the Shield tab exposes **WAF** and **DDoS** settings, which you can configure to protect your site. # Bunny Stream Videos Source: https://bunny.net/docs/integrations/wordpress/stream Embed Bunny Stream videos in WordPress using the Stream Video block or shortcode. The bunny.net WordPress plugin includes built-in support for [Bunny Stream](/docs/stream), allowing you to upload and embed videos directly from the WordPress editor. ## Stream Video block The plugin adds a **bunny.net Stream Video** block to the WordPress block editor. Use it to upload new videos or embed existing ones from your Stream library. In the block editor, click the **+** inserter and search for **bunny.net Stream Video**. Choose an existing video from your Stream library or upload a new one directly from the block. Use the block settings panel to adjust playback options like autoplay, muting, and looping. ## Shortcode For classic editor pages, WooCommerce product descriptions, or anywhere blocks aren't supported, use the `bunnycdn_stream_video` shortcode. ```text theme={null} [bunnycdn_stream_video library=LIBRARY_ID id="VIDEO_ID"] ``` | Parameter | Required | Description | | ------------ | -------- | --------------------------------------------- | | `library` | Yes | Your Stream library ID (numeric) | | `id` | Yes | The video ID (UUID format) | | `responsive` | No | Set to `true` for responsive sizing (default) | ### Example ```text theme={null} [bunnycdn_stream_video library=197133 id="dc48a09e-d9bb-420a-83d7-72dc2304c034" responsive=true] ``` ### WooCommerce To embed a video in a WooCommerce product description, paste the shortcode directly into the product's description field: ```text theme={null} [bunnycdn_stream_video library=197133 id="dc48a09e-d9bb-420a-83d7-72dc2304c034" responsive=true] ``` You can find your library ID and video ID in the [Bunny Stream dashboard](https://dash.bunny.net/stream). ## Supported parameters All [Stream embed parameters](/docs/stream/embedding#supported-parameters) are supported as shortcode attributes. Common options include: | Parameter | Values | Description | | ----------- | ---------------------------- | --------------------------- | | `autoplay` | `true`, `false` | Start playing automatically | | `muted` | `true`, `false` | Start in mute mode | | `loop` | `true`, `false` | Replay after the video ends | | `preload` | `true`, `false` | Pre-download video files | | `showSpeed` | `true`, `false` | Show playback speed control | | `captions` | caption short-code | Default captions language | | `t` | `Xs`, `1h20m45s`, `hh:mm:ss` | Video start time | For the full list, see [Embedding videos](/docs/stream/embedding#supported-parameters). # Support Policy Source: https://bunny.net/docs/integrations/wordpress/support-policy Support policy for the bunny.net WordPress plugin covering plugin versions, PHP, and WordPress compatibility. This policy is designed around a six-month maintenance window. If you install the plugin using supported versions of its dependencies today, you're guaranteed support for at least the next six months. ## Plugin The plugin's support lifecycle is based on minor version releases. The current minor version of the plugin will receive full support until the release of the next minor version. The version immediately preceding the current release will continue to receive security and severe bug fixes for six months after the new release. After this window, that version will no longer receive updates or patches. Although patch releases may still be issued for the previous minor version, it is important to note that WordPress does not offer automatic updates for older plugin branches. As a result, you will need to manually update your plugin to receive these fixes. | Plugin version | Initial release date | Supported until | | -------------- | -------------------- | ---------------- | | 3.0.x | 2026-03-12 | Today + 6 Months | | 2.3.x | 2025-05-07 | 2026-09-12 | | 2.2.x | 2024-04-03 | 2025-11-07 | | 2.1.x | 2024-03-20 | 2024-09-20 | | 2.0.x | 2024-03-14 | 2024-09-14 | ## PHP The plugin is supported on all PHP versions currently in either active support or security support, as defined by [The PHP Group](https://www.php.net/supported-versions). In addition, the plugin provides security issue support for PHP versions included under Ubuntu's LTS standard security maintenance. Details about Ubuntu's maintenance timelines are available [here](https://ubuntu.com/about/release-cycle). However, a new plugin minor version will not support a PHP version under the Ubuntu LTS scheme if that PHP version is scheduled to expire less than six months after the plugin's release date. For example, if a plugin version is released in December 2026, it will not support PHP 8.1, even if PHP 8.1 is included in Ubuntu 22.04 LTS. This is because Ubuntu's support for PHP 8.1 ends in April 2027, which is less than six months after the plugin release. While bug reports related to pre-release PHP versions (e.g., PHP 8.6) are welcome, the plugin does not provide full support for such versions until they are officially released. Compatibility with major PHP releases cannot be guaranteed until official PHP changelogs are finalized. | PHP Version | Support for 2.2.x | Support for 2.3.x | Support for 3.0.x | | ------------- | ----------------- | ----------------- | ----------------- | | 8.5 | No | Yes | Yes | | 8.4 | Yes | Yes | Yes | | 8.3 | Yes | Yes | Yes | | 8.2 | Yes | Yes | Yes | | 8.1 | Yes | Yes | Yes | | 8.0 | Yes | No | No | | 7.4 | Yes | No | No | | 7.3 and below | No | No | No | ## WordPress Each minor version supports WordPress versions released up to six months before the plugin's own release. This ensures compatibility with reasonably recent WordPress updates without requiring immediate upgrades. For example, if a plugin version is released in May 2025, it will support all WordPress versions released from November 2024 onward. Older WordPress versions, even if they remain technically functional, are not officially supported beyond this six-month backward compatibility window. | WordPress Version | WP Release Date | Support for 2.2.x | Support for 2.3.x | Support for 3.0.x | | ----------------- | --------------- | ----------------- | ----------------- | ----------------- | | 6.9 | 2025-12-02 | Yes | Yes | Yes | | 6.8 | 2025-04-15 | Yes | Yes | No | | 6.7 | 2024-11-12 | Yes | Yes | No | | 6.6 | 2024-07-16 | Yes | No | No | | 6.5 | 2024-04-02 | Yes | No | No | | 6.4 and below | 2023-11-07 | No | No | No | # CDN Acceleration Not Enabled Source: https://bunny.net/docs/integrations/wordpress/troubleshooting-acceleration-not-enabled Fix the "CDN Acceleration Is Not Enabled" error in the bunny.net WordPress plugin. If the bunny.net WordPress plugin shows an error indicating CDN acceleration isn't detected, this guide covers the most common causes and solutions. CDN Acceleration Not Enabled error ## Common causes ### Direct origin access You may be accessing your WordPress admin panel without routing traffic through bunny.net. This can happen if: * DNS settings have not yet propagated * Your computer has a stale DNS cache entry * A static IP for your domain is defined in your computer's hosts file ### CDN Acceleration not properly configured Your DNS settings may be incomplete or misconfigured. DNS records that aren't properly set up for CDN acceleration will prevent detection. ### Web server altering headers If your site is behind a reverse proxy (e.g., Cloudflare, NGINX, or a load balancer), it may strip or modify the headers that bunny.net uses to confirm CDN usage. ## Solutions ### Clear your DNS cache ```bash theme={null} ipconfig /flushdns ``` ```bash theme={null} sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder ``` Restart the `nscd` or `systemd-resolved` service. Then restart your browser or clear its cache. ### Verify Bunny DNS and CDN settings In the bunny.net dashboard, go to the **DNS** section. Make sure the DNS record pointing to your site has the green **CDN Proxy** icon enabled. If not, edit the DNS record and enable CDN Acceleration. Green CDN Proxy icon in DNS settings ## Verifying CDN Acceleration You can confirm your setup is working using any of these methods: * A green **CDN Proxy** icon next to your DNS record in the bunny.net dashboard * Response headers from your site containing: * `CDN-ProxyVer: 1.04` * `CDN-RequestPullSuccess: True` * `CDN-RequestPullCode: 200` * In your WordPress Admin Panel under **bunny.net → About → Technical Information → Request Headers**, both `Cdn-RequestId` and `Via: BunnyCDN` should appear # WordPress Plugin Conflicts Source: https://bunny.net/docs/integrations/wordpress/troubleshooting-plugin-conflicts Fix compatibility issues between the bunny.net WordPress plugin and other plugins. Some WordPress theme builder and page editor plugins can cause issues when used together with the bunny.net plugin (Divi, Elementor, etc.). The plugin includes a **Disable for Admin** setting to help resolve these conflicts. ## Disable CDN for admin users Open the bunny.net plugin settings in your WordPress admin, click **CDN**, and enable the **Disable for Admin** setting at the bottom of the panel. This prevents the plugin from rewriting URLs while you're logged in as an admin. Disable for Admin setting ## CDN Acceleration and plugin conflicts If you have CDN Acceleration enabled and cannot use the "Disable for Admin" option, here are fixes for common plugins: ### Divi Builder Divi's layout editor can have backend conflicts with how the CDN handles WordPress API requests. Switch to the **default editor** to resolve this. ### Elementor Layout and page update issues with Elementor are typically caching related. Add a cache bypass Edge Rule (0 cache time) for `*/elementor/*` in your Pull Zone settings. Edge Rules configuration ### Other plugins Many plugins rely on REST API requests through WordPress and can encounter compatibility issues. Most commonly, the `X-HTTP-Method-Override` header is involved. bunny.net does not forward this header by default for security reasons. If a plugin requires `PUT`, `POST`, or `DELETE` methods via this header, you can create an Edge Rule to handle it. Check the plugin's documentation for specific requirements. # WordPress Static URLs Source: https://bunny.net/docs/integrations/wordpress/troubleshooting-static-urls Fix issues where not all WordPress static assets are being served through the CDN. In some cases, even after configuring the bunny.net plugin, some static resource URLs may not be served through the CDN. Below are the most common causes and how to resolve them. ## Page caching If your WordPress site uses a static page caching plugin (such as LiteSpeed Cache or WP Fastest Cache), it may be holding an older cached version of your pages from before the CDN plugin was activated. After activating the plugin for the first time, purge the cache of any caching plugins to ensure the new URLs take effect. This also applies to server-level caching configured by your web host. ## Conflicting CDN integration plugin Many caching plugins include their own CDN configuration, which can conflict with the bunny.net plugin. For example, if both the bunny.net plugin and WP Rocket CDN integration are enabled with different asset settings, this can cause unexpected behavior. There should always be only one CDN integration enabled at a time. ## Incorrect URL detection The plugin detects your site URL based on your WordPress settings. If no URLs are being rewritten, check that the URL in the **Advanced Settings** of the plugin matches the actual URL of your website. ## CSS and JavaScript files Some static resources are loaded from within CSS files or dynamically by JavaScript. The plugin cannot modify those files to replace URLs. In these cases, check your CSS and JavaScript files for any hardcoded URLs that bypass the CDN. ## Dynamically generated scripts or lazy loading Some WordPress plugins load images dynamically from scripts generated on the fly. The plugin cannot access these URLs as they are typically encoded within JavaScript. Check whether the plugin in question offers CDN configuration or allows you to specify which URLs to load. ## HTTP/2 Push If your server is configured to push resources to the browser via HTTP/2 Push, those resources may be delivered before the browser requests the CDN version. Disable HTTP/2 Push for resources that are already served from the CDN. # W3 Total Cache Source: https://bunny.net/docs/integrations/wordpress/w3-total-cache Set up bunny.net CDN with WordPress using the W3 Total Cache plugin. This guide walks you through configuring bunny.net CDN on your WordPress site using W3 Total Cache. Log in to your [bunny.net dashboard](https://dash.bunny.net), create a new Pull Zone, and set the origin URL to your WordPress site. For more details, see [How to create your first Pull Zone](/docs/cdn/quickstart). In your WordPress admin, go to **Plugins → Add New**. Search for **W3 Total Cache**, click **Install Now**, then **Activate**. Go to **Performance → General Settings** and scroll down to the **CDN** section. Set the CDN Type to **Generic Mirror**, check the **Enable** checkbox, and click **Save all settings**. W3 Total Cache General Settings CDN section Go to **Performance → CDN** in the sidebar. Scroll down to the configuration section and enter your Pull Zone hostname in the **Replace site's hostname with** field. Click **Save all settings**. W3 Total Cache CDN hostname configuration Your WordPress site is now serving static assets through bunny.net CDN. # WP Rocket Source: https://bunny.net/docs/integrations/wordpress/wp-rocket Set up bunny.net CDN with WordPress using the WP Rocket caching plugin. This guide walks you through configuring bunny.net CDN on your WordPress site using WP Rocket. Log in to your [bunny.net dashboard](https://dash.bunny.net), create a new Pull Zone, and set the origin URL to your WordPress site. For more details, see [How to create your first Pull Zone](/docs/cdn/quickstart). Purchase and download the WP Rocket plugin from their website. In your WordPress admin, go to **Plugins → Add New**, upload the ZIP file, and activate the plugin. Click the **WP Rocket** option in your WordPress admin bar, then go to **CDN Settings**. Enter the full hostname of your Pull Zone (including `https://`) and save your changes. WP Rocket CDN Settings Open your site in an Incognito/Private Browsing window. In the browser's Developer Tools, open the **Sources** panel. You should see static assets being served from your CDN hostname. WP Rocket forces a no-cache instruction (`max-age=0`) on HTML files to ensure those pages are treated as dynamic. HTML pages will not be cached on the CDN unless you configure this explicitly via WP Rocket settings or an Edge Rule on your bunny.net Pull Zone. # API Reference Source: https://bunny.net/docs/magic-containers/api-reference Complete API reference for deploying and managing Magic Containers programmatically. # App Metadata Source: https://bunny.net/docs/magic-containers/app-metadata bunny.net's platform automatically injects specific metadata into the deployed containers. This metadata is provided in the form of environment variables, which are crucial for applications running on these containers. This critical information aids in managing and operating applications more dynamically and responsively within cloud environments. # What is app metadata? The Magic Containers feature automatically injects metadata into each container. The following Environment Variables are configured: * `BUNNYNET_MC_APPID`: Unique identifier for the application. * `BUNNYNET_MC_PODID`: Identifier for the pod within which the container runs. * `BUNNYNET_MC_REGION`: Geographic location data where the container is deployed. * `BUNNYNET_MC_PUBLIC_ENDPOINTS`: Publicly accessible endpoints of the pod, which are essential for inter-service communication. * `BUNNYNET_MC_PODIP`: Internal IP address of the host, useful for intra-network communications within the cloud environment. * `BUNNYNET_MC_HOSTIP`: IP address of the host machine on which the container is running. * `BUNNYNET_MC_ZONE`: A more specific designation within the region. These environment variables are injected into the containers, providing dynamic data that can be accessed programmatically within the application. The simplicity of access to this data is facilitated through environment variables, which can be easily queried by the application running within the container. # Benefits of app metadata App metadata carries several practical benefits, making it an indispensable feature for modern cloud-native applications: * **Enhanced Logging**: Metadata allows for more detailed logs. For instance, including the application ID or pod ID in log entries can help in troubleshooting and understanding application behavior across different environments. * **Dynamic Configuration**: Metadata like region or public endpoints can be used by the application to adjust its behavior or configuration dynamically. This is particularly useful in geo-distributed deployments where applications need to serve region-specific content or adhere to region-specific regulations. * **Service Discovery**: For microservices architectures, public endpoints metadata allows services to register themselves or discover other services dynamically, facilitating smoother inter-service communication and scalability. * **Operational Efficiency**: Knowing the internal IP and host details can help in network planning and management, ensuring that resources are optimally utilized and performance bottlenecks are minimized. # Autoscaling Source: https://bunny.net/docs/magic-containers/autoscaling Autoscaling is a powerful feature in Magic Containers that allows you to automatically adjust the number of replicas running in each region based on the CPU usage of your containers. This ensures optimal resource utilization and responsiveness for your applications. # How autoscaling works Magic Containers employ a reinforcement learning-based autoscaling mechanism that continuously monitors the CPU usage of your containers. The goal is to maintain CPU usage within an optimal threshold span (optimal CPU usage span). Here's how it works: * **Optimal threshold**: The autoscaler sets an optimal threshold span for CPU usage. * **Downscaling**: If the CPU usage falls below the minimal threshold value, the autoscaler will initiate downscaling. This means it will reduce the number of replicas in that region, as fewer replicas are needed when the CPU usage is lower. * **Upscaling**: Conversely, if the CPU usage exceeds the maximum threshold value, the autoscaler will initiate upscaling. This involves adding replicas to bring the CPU usage closer to the optimal threshold. # Configuring autoscaling Autoscaling limits can be updated or changed even after the application is deployed. To configure autoscaling for your Magic Containers, follow these steps: 1. Log in to your bunny.net account. 2. Navigate to the Magic Containers section and select the app from the dropdown menu that you want to modify. 3. Click Regions and Scaling. 4. Here you can configure the maximum and minimum number of running replicas per region (the same setting applies to all the regions). **Autoscaling** in Magic Containers allows you to set minimum and maximum limits for the number of replicas that can run in each region. It's important to note that these limits apply uniformly across all regions associated with your container: * **Minimum replicas**: Specify the minimum number of replicas that should be running in each region. Autoscaling will not downscale below this limit. * **Maximum replicas**: Set the maximum number of replicas that can run in each region. Autoscaling will not upscale beyond this limit (maximum is 10 on a standard account and 3 on a [trial account](/docs/magic-containers/limits#trial-accounts), however custom limit can be agreed through support request). # Changelog Source: https://bunny.net/docs/magic-containers/changelog Latest updates and improvements to Magic Containers. ## Quick Deploy A streamlined deployment flow that lets you go from zero to deployed in seconds, with everything presented in a single form. [Learn more](/docs/magic-containers/quick-deploy) ## Templates Deploy pre-built application templates with just a few clicks. Each template includes a ready-to-run container image, and some include a sidecar database. [Learn more](/docs/magic-containers/templates) ## Graceful Shutdown New applications now have a default grace period of 30 seconds (up from 1 second), giving your applications more time to clean up resources and complete in-flight requests during rolling updates and scaling events. [Learn more](/docs/magic-containers/graceful-shutdown) # Configuration Source: https://bunny.net/docs/magic-containers/configuration Configure your container's environment variables, health checks, endpoints, and runtime settings. Once you have a [deployed container](/docs/magic-containers/quickstart), you can modify its configuration by going to **Magic Containers**, selecting your container, then clicking **Container Settings**. ## General settings Click **Edit** to modify environment variables, health checks, endpoints, and resource allocation. After making changes, click **Update Container** to apply the new configuration. ## Advanced settings Click the **Advanced** tab to configure runtime behavior. The following settings are available: * **Startup command**: A custom command that executes when the container is launched, giving you control over the initial behavior. * **Container Arguments**: Arguments added to the container's entry point when starting the image. Useful for passing runtime parameters. * **Working Dir**: The working directory for the container runtime. Configure this for applications that rely on specific file paths. # Delete App Source: https://bunny.net/docs/magic-containers/delete Permanently remove a container and its configuration. Deleting an app is irreversible. Ensure you want to permanently remove the app before confirming. Go to **Magic Containers** and select the container you want to delete. Click the menu icon (three dots) on the right side of the screen and click **Delete App**. Enter the name of the app to confirm, then click **Confirm**. # Deploy Source: https://bunny.net/docs/magic-containers/deploy Choose from Magic, Single region, or Advanced deployment options. Magic Containers offers three deployment options: Magic deployment for effortless global provisioning, Single region for fast deployment without auto-scaling, and Advanced for full control over regions and scaling based on user activity. Before deploying, you'll need a [private container registry](/docs/magic-containers/image-registries) configured. # Magic deployment **Magic deployment** is the simplest way to get your app up and running. It leverages the power of Magic Containers AI to handle global provisioning efficiently and cost-effectively. Magic Containers AI will automatically analyze your app's requirements, determine the optimal deployment regions, and provision your app globally in just a few clicks. This approach ensures the best possible performance while minimizing costs. # Single region deployment **Single region deployment** gives you full control over provisioning settings, allowing you to deploy to the closest single region without the worry of any scaling. This is ideal for scenarios where you are wanting fast deployment and no auto-scaling. Login to [bunny.net dashboard](https://dash.bunny.net/auth/login?pk_buttonlocation=menu). Select **Magic Containers** and click the **Add App** button. Select the **Single region deployment** option, define the name of your app, and click **Next Step**. After selecting Single region deployment, configure your **App region**. You will be presented with your closest region preselected and a list of all available regions. After configuring the **App region** settings, click **Next Step.** Click **Add Container**. If you want your app to be available on the internet, you need to set up edge endpoints. Go to the **Endpoints** tab and click **Add New Endpoint**. * You need to name the endpoint (it must have a unique name). * Decide how you want to expose the app, either using CDN or Anycast. * Define the container port, which is the port on which the application is listening inside the container. * Specify whether your application inside the container uses SSL for origin (only CDN setting). Click **Add endpoint**, and then click **Add Container**. Review your settings. If everything looks good, click **Next Step**. Click **Confirm and Create**. You will be navigated to the overview screen of your app. The processing button should turn green, indicating that your app is being deployed. Your application is now deployed and can be accessed by clicking here: The provisioner will take your region settings into account and deploy your app accordingly. # Advanced deployment **Advanced deployment** gives you full control over provisioning settings, allowing you to customize the deployment regions and react to user activity. This level of customization is ideal for scenarios where you have specific geographical requirements or want to optimize your deployment strategy based on user behavior. Login to [bunny.net dashboard](https://dash.bunny.net/auth/login?pk_buttonlocation=menu). Select **Magic Containers** and click the **Add App** button. Select the **Advanced deployment** option, define the name of your app, and click **Next Step**. You will be presented with a list of all available regions. For each region, you can set whether it is a base region or a standard region: * **Base region**: Regions where your app will always be deployed. You must select at least one base region. Standard accounts have no maximum, while trial accounts are limited to three regions. See [Limits](/docs/magic-containers/limits#trial-accounts). * **Enabled region**: The provisioning system will actively monitor user locations and behavior. If it identifies active users in a specific region, it will dynamically deploy the app to accommodate the user traffic. Conversely, if there is no user activity in a region, the app will not be deployed in that area. Based on your settings, the provisioner will react to user activity as follows: * If the **Enabled Regions** and **Base Regions** are equal, the provisioner will use Static Provisioning. In this mode, the regions deployed over time will remain the same. * If the sets are different, the provisioner will use **Auto Provisioning**. Regions are chosen based on end-user activity. If users are active in a region, the app will be deployed there. If there are no active users in a region, the app won't be deployed. After configuring the region settings, click **Next Step.** Set the minimum and maximum number of instances (maximum is 10 per region on a standard account, 3 on a [trial account](/docs/magic-containers/limits#trial-accounts)), and click **Add New Container**. Select the provisioning type. If you want your app to be available on the internet, you need to set up edge endpoints. Go to the **Endpoints** tab and click **Add New Endpoint**. * You need to name the endpoint (it must have a unique name). * Decide how you want to expose the app, either using CDN or Anycast. * Define the container port, which is the port on which the application is listening inside the container. * Specify whether your application inside the container uses SSL for origin (only CDN setting). Click **Add endpoint**, and then click **Add Container**. Review your settings. If everything looks good, click **Next Step**. Click **Confirm and Create**. You will be navigated to the overview screen of your app. The processing button should turn green, indicating that your app is being deployed. Your application is now deployed and can be accessed by clicking here: The provisioner will take your region settings into account and deploy your app accordingly. # Deploy with GitHub Actions Source: https://bunny.net/docs/magic-containers/deploy-with-github-actions Automate container image updates on Magic Containers with GitHub Actions. Automate your deployments by integrating Magic Containers with GitHub Actions. When you push changes to your repository, your workflow can build a Docker image, push it to a container registry, and trigger a rolling update on Magic Containers. ## Prerequisites * An application already deployed on the Magic Containers platform * A GitHub repository containing a Dockerfile or valid build context * Container registry credentials (use `GITHUB_TOKEN` for GitHub Container Registry, or personal access tokens for DockerHub) ## Quickstart Add a step to your GitHub Actions workflow with the following inputs: `app_id`, `api_key`, `container`, and `image_tag`. This triggers a rolling update whenever a new image is built and pushed. ```yml theme={null} name: Update container image when pushing to main on: push: branches: - "main" jobs: build: name: Build and deploy runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: docker login uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: docker build and push uses: docker/build-push-action@v5 with: context: . push: true tags: ghcr.io/${{ github.repository }}:${{ github.sha }} - name: Update container image on Magic Containers uses: BunnyWay/actions/container-update-image@main with: app_id: ${{ vars.APP_ID }} api_key: ${{ secrets.BUNNYNET_API_KEY }} container: app image_tag: "${{ github.sha }}" ``` ## Inputs | Input | Required | Description | | ----------- | -------- | -------------------------------------------------------------------- | | `app_id` | Yes | The App ID for your Magic Containers application | | `api_key` | Yes | The API Key for your Bunny account (sub-user accounts not supported) | | `container` | Yes | The name of the container within the application | | `image_tag` | Yes | The new image tag | # Blue/Green & A/B Deployments Source: https://bunny.net/docs/magic-containers/deployment-strategies Run multiple versions of your app at the same time and control how traffic is routed between them. Magic Containers lets you run more than one version of an app simultaneously. That unlocks three well-known release strategies. Here's the short version of each, and how it maps to Magic Containers: * **[Blue/green](#bluegreen-deployment)** keeps two environments side by side, the current one (blue) and the new one (green), then switches *all* traffic over in a single cutover. You get a clean release with no in-between state, and rollback is just pointing traffic back at blue. * **[A/B](#ab-deployment)** runs both versions at once and splits traffic between them by percentage, keeping each user pinned to the same version. It's how you put a new version in front of a slice of real users and compare how it performs. * **[Canary](#canary-deployment)** releases the new version to a narrow, targeted slice first, such as a single country, watches it under real traffic, then widens the audience as your confidence grows. Pick blue/green when you want a clean release with an instant rollback path. Pick A/B when you want to expose a new version to a fraction of real traffic. Pick canary when you want to validate a new version on a targeted slice before rolling it out everywhere. For everything else, a standard [rolling update](/docs/magic-containers/rolling-updates) is enough. | | Blue/Green | A/B | Canary | | ------------- | --------------------------------------------- | --------------------------------------- | --------------------------------------- | | Traffic | 100% to one version at a time | Split by percentage (e.g. 90/10) | Ramps from a targeted slice to 100% | | Goal | Safe release + instant rollback | Test a version on a slice of users | Validate a version on a targeted slice | | Routing | Pull zone origin points at the active version | Edge Script picks a version per request | Edge Script picks a version per request | | Configured in | Terraform / dashboard | Edge Scripting (+ two apps) | Edge Scripting (+ two apps) | ## Why not just use rolling updates? A [rolling update](/docs/magic-containers/rolling-updates) gradually replaces instances of a single app with the new image. During the rollout, both the old and new versions serve traffic at the same time, and across many regions a partial or stalled rollout can leave versions co-existing longer than you'd like. Blue/green sidesteps this by keeping the new version completely separate until you flip every request to it at once. There's no in-between state, and rolling back is just pointing traffic at the previous version again. Rolling updates are the right default for most apps. Reach for blue/green when a release must be atomic, such as a schema change where both versions need to serve the same traffic safely. ## Blue/green deployment The idea: deploy the new version as a **separate app** while the current version keeps serving, then switch a pull zone's origin from the old endpoint to the new one. Create a second Magic Containers app (the "green" version) with the new image. Leave the current "blue" app running and serving production traffic. Use a pull zone with a `ComputeContainer` origin that targets the currently active app's container and endpoint. When the new version is ready, switch the origin to the green app's container and endpoint, then apply. All traffic moves at once. To revert, point the origin back at the blue app. Because it's still running, rollback is immediate. ### Terraform example This pull zone routes to one container version at a time. To cut over, comment out the active block, uncomment the other, and apply: ```hcl main.tf theme={null} theme={null} resource "bunnynet_compute_container_app" "blue" { name = "my-app-blue" ... container { name = "app" image_tag = "2.3.14" ... endpoint { name = "app" type = "CDN" cdn { origin_ssl = false } port { container = 8080 } } } } resource "bunnynet_compute_container_app" "green" { name = "my-app-green" ... container { name = "app" image_tag = "3.0.0" ... endpoint { name = "app" type = "CDN" cdn { origin_ssl = false } port { container = 8080 } } } } resource "bunnynet_pullzone" "prod" { name = "my-app-prod" origin { type = "ComputeContainer" # Blue container_app_id = bunnynet_compute_container_app.blue.id container_endpoint_id = bunnynet_compute_container_app.blue.container[0].endpoint[0].id # Green # container_app_id = bunnynet_compute_container_app.green.id # container_endpoint_id = bunnynet_compute_container_app.green.container[0].endpoint[0].id } routing { tier = "Standard" } } resource "bunnynet_pullzone_hostname" "prod" { pullzone = bunnynet_pullzone.prod.id name = "my-app.example.net" tls_enabled = true force_ssl = false } ``` For **stateful** workloads such as databases, both versions read and write the same data, so the two versions must be compatible with a shared schema. The version-selection logic typically lives in your application or database layer. ## A/B deployment A/B routing sends a percentage of traffic to each version while keeping individual users pinned to one version (stickiness). [Edge Scripting](/docs/scripting) handles the weighted split, running in front of two separate apps and choosing a version per request. The approach: 1. Deploy **two Magic Containers apps**: version A and version B. 2. Add a [standalone Edge Script](/docs/scripting/standalone/overview) that derives a stable key per visitor (client IP works well for stickiness). 3. Hash the key into a bucket and route to app A or app B based on your target percentage. ```typescript theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; const VARIANTS = { a: "https://app-a.bunny.run", b: "https://app-b.bunny.run", }; const B_PERCENTAGE = 10; // send 10% of visitors to version B // Polynomial string hash (Java String.hashCode style), mapped into a 0-99 bucket function bucket(key: string): "a" | "b" { let h = 0; for (let i = 0; i < key.length; i++) { h = (h * 31 + key.charCodeAt(i)) >>> 0; } return h % 100 < B_PERCENTAGE ? "b" : "a"; } BunnySDK.net.http.serve(async (request: Request): Promise => { const ip = request.headers.get("x-forwarded-for") ?? "0.0.0.0"; const variant = bucket(ip); const target = new URL(request.url); const origin = new URL(VARIANTS[variant]); target.protocol = origin.protocol; target.host = origin.host; return fetch(new Request(target, request)); }); ``` Because the same IP always hashes to the same bucket, a given visitor consistently sees the same version. Adjust `B_PERCENTAGE` to change the split, and update the variant URLs to your two apps' endpoints. For a standalone reference, see [Weighted traffic splitting](/docs/scripting/standalone/examples/weighted-routing). Use a key that's stable for the session you want to pin. Client IP is the simplest. To weight by something else (a cookie, a header, a user ID), hash that value instead. ## Canary deployment A canary release sends the new version to a narrow, targeted audience first, lets you watch it under real traffic, then widens the audience as your confidence grows. Country makes a natural targeting key: ship to one or two markets, confirm the metrics look healthy, then add more. This uses the same two-app, [standalone Edge Script](/docs/scripting/standalone/overview) setup as A/B, routing on the visitor's country instead of a percentage. bunny.net adds the [`CDN-RequestCountryCode`](//cdn/cdn-acceleration#http-headers) header to every request, so the script can read it directly. The approach: 1. Deploy **two Magic Containers apps**: the stable version and the canary version. 2. List the countries that should receive the canary. 3. Route those countries to the canary app and everything else to stable. ```typescript theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; const STABLE = "https://app-stable.bunny.run"; const CANARY = "https://app-canary.bunny.run"; // Countries that receive the canary version first const CANARY_COUNTRIES = new Set(["NZ", "FI"]); BunnySDK.net.http.serve(async (request: Request): Promise => { const country = request.headers.get("CDN-RequestCountryCode") ?? ""; const variant = CANARY_COUNTRIES.has(country) ? CANARY : STABLE; const target = new URL(request.url); const origin = new URL(variant); target.protocol = origin.protocol; target.host = origin.host; return fetch(new Request(target, request)); }); ``` To widen the rollout, add countries to `CANARY_COUNTRIES` and redeploy the script. To roll back, empty the set so all traffic returns to the stable app. For a standalone reference, see [Route by country](/docs/scripting/standalone/examples/geo-routing). Combine this with the A/B bucket to ramp inside a country. Route a country to the canary, then send a growing percentage of its visitors to the new version as you gain confidence. # Endpoints Source: https://bunny.net/docs/magic-containers/endpoints Expose your container to the internet using CDN or Anycast endpoints. Once you have a [deployed container](/docs/magic-containers/quickstart), you can expose it to the internet using two endpoint types: **CDN** for HTTP(S) traffic or **Anycast** for direct IP access. ## CDN CDN endpoints route HTTP(S) traffic through bunny.net's edge network, improving performance and reducing latency based on user location. For more information about CDN, see our [CDN documentation](https://bunny.net/academy/cdn/what-is-a-cdn-content-delivery-network/). To create a CDN endpoint: 1. Go to **Magic Containers** and select your container. 2. Click **Endpoints**, then **Add New Endpoint**. 3. Select **CDN** as the type. 4. Configure the endpoint: * **Name**: A unique name for this endpoint. * **Container Port**: The port your application listens on. * **SSL for origin**: Enable if your application uses SSL internally. It's recommended to run one process per container. If running multiple processes, ensure they use different ports. 5. Click **Add Endpoint**. ### Sticky sessions Sticky sessions ensure all requests from a client are routed to the same server instance, maintaining session state across requests. To enable sticky sessions: 1. In the endpoint configuration, select **Sticky Session**. 2. Choose an identifier (headers like `X-Forwarded-For` or `User-Agent`, or cookies like `SessionID`). 3. Click **Add Endpoint**. ## Anycast Anycast endpoints map your container to an Anycast IP address, routing requests to the nearest node for improved performance. To create an Anycast endpoint: 1. Go to **Magic Containers** and select your container. 2. Click **Endpoints**, then **Add New Endpoint**. 3. Select **Anycast** as the type. 4. Configure the endpoint: * **Name**: A unique name for this endpoint. * **Container Port**: The port your application listens on inside the container. * **Exposed Port**: The port available on the Anycast IP. 5. Click **Add Endpoint**. # Environment Variables Source: https://bunny.net/docs/magic-containers/environment-variables Configure your container's runtime settings using environment variables. Environment variables allow you to provide dynamic configuration options to your container without hardcoding values in your code. ## Automatic detection When you select a registry image, Magic Containers scans the image metadata to identify environment variables the container might use. If detected, the variables are displayed with their names and any default values. Automatic scanning is currently only supported for public Docker Hub images. Click **Go To Environment Variables** to add all detected variables with a single click. ## Manual configuration If no variables are detected, or you need to add additional ones, you can configure them manually: 1. Go to **Magic Containers** and select your container. 2. Click **Container Settings**, then **Edit**. 3. Select the **Environment Variables** tab and click **+ Add New Variable**. 4. Enter the variable name and value, then click **Update Container**. # FAQs Source: https://bunny.net/docs/magic-containers/faqs Frequently asked questions about Magic Containers. **Is a detailed breakdown of my costs available?** Currently, no detailed cost breakdown is provided for Magic Containers. **How accurate is the monthly estimated costs?** The estimate is updated in near real-time, offering a highly accurate summary. **How often is this value updated?** The summary section is refreshed up to the last minute, ensuring you always see the latest information. **How are spikes on startup handled?** There is no special handling for startup spikes at this time. If you have concerns, we recommend reaching out to support to discuss possible options. **Will I be charged for having a scaled down application?** Yes. Even a minimal configuration (one region with a single instance) will accrue charges. **Is container ingress charged on apps with no endpoints?** No. Ingress is free if there are no endpoints. However, any outbound data (egress) will still incur charges. **My application requires persistence of data. How can I use Magic Containers in this case?** Magic Containers supports data persistence through [Persistent Volumes](/docs/magic-containers/persistent-volumes), which allow your containers to retain data across restarts and deployments. If you need a managed database, you can use [Bunny Database](/docs/database), a fully managed database service that integrates seamlessly with Magic Containers. # Graceful Shutdown Source: https://bunny.net/docs/magic-containers/graceful-shutdown Understand how Magic Containers handles container shutdown with SIGTERM signals and grace periods. When Magic Containers needs to stop a container, whether during a rolling update, scaling down, or app deletion, it follows a graceful shutdown process to give your application time to clean up resources and complete in-flight requests. ## How it works The shutdown process follows these steps: 1. **SIGTERM signal** - Magic Containers sends a `SIGTERM` signal to your container's main process (PID 1), notifying it to begin a graceful shutdown. 2. **Grace period** - Your application has a configurable grace period (default 30 seconds for new applications) to handle the signal, close connections, flush data, and exit cleanly. 3. **SIGKILL signal** - If the process is still running after the grace period, Magic Containers sends a `SIGKILL` signal to forcefully terminate the container. The 30-second grace period matches Kubernetes' default termination behavior, allowing applications designed for Kubernetes to work seamlessly on Magic Containers. Applications created before February 1st, 2026 have a 1-second grace period. Applications created after this date have a 30-second grace period. To check or update your application's grace period, use the `terminationGracePeriodSeconds` field via the API (`PUT /apps/APP_ID/`). ## Handling SIGTERM in your application To ensure a clean shutdown, your application should: * **Listen for SIGTERM** - Register a signal handler to catch the termination signal. * **Stop accepting new requests** - Prevent new work from starting during shutdown. * **Complete in-flight work** - Finish processing active requests or transactions. * **Close connections** - Gracefully close database connections, file handles, and network sockets. * **Flush data** - Write any buffered data to persistent storage. * **Exit cleanly** - Terminate the process with a zero exit code. ## Tips * **Keep shutdown fast** - While you have 30 seconds, aim to shut down as quickly as possible. Faster shutdowns reduce deployment times during rolling updates. * **Use health checks** - Configure readiness health checks so traffic stops flowing to your container before the shutdown signal is sent. * **Test your shutdown logic** - Send `SIGTERM` to your container locally to verify it handles the signal correctly and exits within the grace period. # Astro Source: https://bunny.net/docs/magic-containers/guides/astro Deploy an Astro application to Magic Containers This guide walks you through building and deploying an Astro application to Magic Containers with GitHub Container Registry. You'll need: * A GitHub account for source code and container registry Don't have a bunny.net account yet? [Sign up](https://dash.bunny.net/auth/register) and enable Magic Containers to get started. ## Create the Astro app Create a new Astro project: ```bash theme={null} npm create astro@latest app-astro cd app-astro ``` ### Add the Node adapter Astro requires an adapter for server-side rendering. Install the Node adapter: ```bash theme={null} npx astro add node ``` This updates your `astro.config.mjs` to include the adapter: ```javascript astro.config.mjs theme={null} import { defineConfig } from "astro/config"; import node from "@astrojs/node"; export default defineConfig({ output: "server", adapter: node({ mode: "standalone", }), }); ``` ### Create an API route Create a simple endpoint to test the deployment: ```typescript src/pages/index.json.ts theme={null} import type { APIRoute } from "astro"; export const GET: APIRoute = () => { return new Response(JSON.stringify({ message: "Hello from Bunny 🐰" })); }; ``` ## Run locally Start the development server: ```bash theme={null} npm run dev ``` Visit [http://localhost:4321](http://localhost:4321) in your browser, or test the API route: ```bash theme={null} curl http://localhost:4321/index.json ``` ## Create the Dockerfile ```dockerfile Dockerfile theme={null} FROM node:22-alpine AS base WORKDIR /app COPY package*.json ./ FROM base AS deps RUN npm install FROM deps AS build COPY . . RUN npm run build FROM base AS runtime COPY package*.json ./ RUN npm install --omit=dev COPY --from=build /app/dist ./dist ENV HOST=0.0.0.0 ENV PORT=80 EXPOSE 80 CMD ["node", "./dist/server/entry.mjs"] ``` ## Build and push to GitHub Container Registry Create `.github/workflows/build.yml` to automatically build and push on every commit to `main`: ```yaml .github/workflows/build.yml theme={null} name: Build and Push on: push: branches: [main] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build-and-push: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v5 with: context: . push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} - name: Update container image on Magic Containers uses: BunnyWay/actions/container-update-image@main with: app_id: ${{ vars.APP_ID }} api_key: ${{ secrets.BUNNYNET_API_KEY }} container: app image_tag: "${{ github.sha }}" ``` Push your code to trigger the workflow: ```bash theme={null} git init git add . git commit -m "Initial commit" git remote add origin https://github.com/YOUR_USERNAME/app-astro.git git push -u origin main ``` Build and push manually from your local machine. Go to [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens) and create a token with `write:packages` scope. ```bash theme={null} export CR_PAT=your_personal_access_token echo $CR_PAT | docker login ghcr.io -u YOUR_USERNAME --password-stdin ``` ```bash theme={null} docker build --platform linux/amd64 -t ghcr.io/YOUR_USERNAME/app-astro:latest . ``` Magic Containers only supports images built for the **linux/amd64** architecture. The `--platform` flag ensures compatibility regardless of your local machine's architecture. ```bash theme={null} docker push ghcr.io/YOUR_USERNAME/app-astro:latest ``` If your package is private, set the visibility to **Public** in GitHub or [configure Magic Containers with registry credentials](/docs/magic-containers/image-registries). ## Deploy to Magic Containers In the bunny.net dashboard, go to **Magic Containers** and click **Add App**. Enter a name and select your deployment option. Click **Add Container**, then configure: | Field | Value | | -------- | ---------------------------------------------------------------------------- | | Registry | GitHub Container Registry | | Image | `YOUR_USERNAME/{imageName}` | | Tag | `latest` for Docker CLI, or the commit SHA from your GitHub Actions workflow | Go to the **Endpoints** tab, click **Add New Endpoint**, and set the container port to `80`. Click **Add Container**, then **Next Step**, and **Confirm and Create**. For more details, see the [quickstart guide](/docs/magic-containers/quickstart). ## Test your app Visit your container URL in the browser to see your Astro site, or test the API route: ```bash theme={null} curl https://mc-xxx.bunny.run/index.json ``` ```json Response theme={null} { "message": "Hello from Bunny 🐰" } ``` You can [add a custom hostname](/docs/magic-containers/endpoints) from the **Endpoints** section in your app settings. ## Connect a database You can connect your app to [Bunny Database](/docs/database) directly from the dashboard: 1. Go to **Database > \[Your Database] > Access** 2. Click **Generate Tokens** 3. Click **Add Secrets to Magic Container App** 4. Select your app The `BUNNY_DATABASE_URL` and `BUNNY_DATABASE_AUTH_TOKEN` environment variables are now available in your app: ```typescript src/pages/api/users.ts theme={null} import type { APIRoute } from "astro"; import { createClient } from "@libsql/client/web"; const client = createClient({ url: import.meta.env.BUNNY_DATABASE_URL, authToken: import.meta.env.BUNNY_DATABASE_AUTH_TOKEN, }); export const GET: APIRoute = async () => { const result = await client.execute("SELECT * FROM users"); return new Response(JSON.stringify(result.rows), { status: 200, headers: { "Content-Type": "application/json", }, }); }; ``` See the [TypeScript SDK documentation](/docs/database/connect/typescript) for more details. ## Next steps 1. [Automate deploys with GitHub Actions](/docs/magic-containers/deploy-with-github-actions) 2. [Add a custom hostname](/docs/magic-containers/endpoints) 3. [Add a persistent volume](/docs/magic-containers/persistent-volumes) # ClickHouse Source: https://bunny.net/docs/magic-containers/guides/clickhouse Deploy ClickHouse to Magic Containers This guide walks you through deploying ClickHouse to Magic Containers, either as a standalone container or as part of a [multi-container](/docs/magic-containers/multi-container) app alongside your application. Don't have a bunny.net account yet? [Sign up](https://dash.bunny.net/auth/register) and enable Magic Containers to get started. ## Quickstart Go to the [bunny.net dashboard](https://dash.bunny.net), select **Magic Containers**, and click **Add App**. Select **Single region deployment**. Databases should use single region deployment with a single instance. Each pod gets its own dedicated volume with no data replication — this applies both across regions and within the same region. Scaling to multiple pods or regions would result in separate, isolated databases each with their own data. Click **Add Container** and configure the image: * **Registry**: Docker Hub * **Image**: `clickhouse/clickhouse-server` * **Tag**: `latest` Magic Containers will automatically detect the required endpoint and environment variables for the image. Configure the environment variables as needed: * `CLICKHOUSE_DB` = `app` * `CLICKHOUSE_USER` = `app` * `CLICKHOUSE_PASSWORD` = a strong password * `CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT` = `1` In the **Volumes** section of the container settings, add two volumes: * **Name**: `clickhouse-data`, **Mount path**: `/var/lib/clickhouse` * **Name**: `clickhouse-logs`, **Mount path**: `/var/log/clickhouse-server` This ensures your database files and logs persist across restarts and redeployments. Without volumes, all data is lost when the container stops. See [persistent volumes](/docs/magic-containers/persistent-volumes) for more details on volume behavior and pricing. Review your settings and click **Confirm and Create**. Always set a strong `CLICKHOUSE_PASSWORD`, even if ClickHouse is not exposed externally. Other containers in the same pod can access the network, and a password protects against accidental or unauthorized access. ## Environment variables The official ClickHouse image supports these environment variables: | Variable | Description | Default | | -------------------------------------- | ----------------------------------- | --------- | | `CLICKHOUSE_DB` | Default database created on start | `default` | | `CLICKHOUSE_USER` | Username to create | `default` | | `CLICKHOUSE_PASSWORD` | Password for the user | - | | `CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT` | Enable SQL-driven access management | `0` | ## Connect from your app In a [multi-container](/docs/magic-containers/multi-container) setup, your app and ClickHouse share the same localhost network. ClickHouse exposes two interfaces: * **HTTP**: port `8123` * **Native**: port `9000` ``` http://app:YOUR_PASSWORD@127.0.0.1:8123/?database=app ``` ```javascript theme={null} import { createClient } from "@clickhouse/client"; const client = createClient({ url: "http://127.0.0.1:8123", username: "app", password: process.env.CLICKHOUSE_PASSWORD, database: "app", }); const result = await client.query({ query: "SELECT now()" }); ``` ```go theme={null} import "github.com/ClickHouse/clickhouse-go/v2" conn, err := clickhouse.Open(&clickhouse.Options{ Addr: []string{"127.0.0.1:9000"}, Auth: clickhouse.Auth{ Database: "app", Username: "app", Password: os.Getenv("CLICKHOUSE_PASSWORD"), }, }) if err != nil { log.Fatal(err) } defer conn.Close() ``` ```python theme={null} import clickhouse_connect client = clickhouse_connect.get_client( host="127.0.0.1", port=8123, username="app", password=os.environ["CLICKHOUSE_PASSWORD"], database="app", ) result = client.query("SELECT now()") ``` ## Multi-container example A typical setup pairs ClickHouse with your application. When configuring the app, add two containers: ### App container * **Image**: your app image (e.g. `ghcr.io//my-app:latest`) * **Endpoint**: the port your app listens on * **Environment variables**: * `CLICKHOUSE_PASSWORD` = a strong password ### ClickHouse container * **Image**: `clickhouse/clickhouse-server:latest` * **Volumes**: `/var/lib/clickhouse` and `/var/log/clickhouse-server` * **Environment variables**: * `CLICKHOUSE_DB` = `app` * `CLICKHOUSE_USER` = `app` * `CLICKHOUSE_PASSWORD` = a strong password * `CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT` = `1` Both containers share the same localhost network, so your app connects to ClickHouse at `127.0.0.1:8123` (HTTP) or `127.0.0.1:9000` (native). See [multi-container apps](/docs/magic-containers/multi-container) for more details. ## External access To connect to ClickHouse from outside Magic Containers (e.g. from your local terminal), add either a [CDN or Anycast endpoint](/docs/magic-containers/endpoints). CDN endpoints work for ClickHouse's HTTP interface only; Anycast endpoints support both HTTP and the native protocol. ### Anycast endpoint 1. Go to your app's **Endpoints** tab and click **Add New Endpoint** 2. Select **Anycast** as the type 3. Set **Container Port** to `8123` 4. Set **Exposed Port** to `8123` 5. Click **Add Endpoint** Then connect using the Anycast IP and the **exposed port**: ```bash theme={null} curl "http://:/?query=SELECT+1&user=app&password=YOUR_PASSWORD" ``` Or using the ClickHouse client: ```bash theme={null} clickhouse-client --host --port --user app --password YOUR_PASSWORD ``` The exposed port and container port may differ. When connecting externally, always use the exposed port shown in your endpoint configuration. The `clickhouse-client` uses the native protocol (port `9000`). If you want to use the native client externally, add a second Anycast endpoint for port `9000`. ### CDN endpoint A CDN endpoint exposes ClickHouse's HTTP interface through bunny.net's edge network at a `b-cdn.net` hostname, giving you HTTPS out of the box. 1. Go to your app's **Endpoints** tab and click **Add New Endpoint** 2. Select **CDN** as the type 3. Set **Container Port** to `8123` 4. Click **Add Endpoint** Then connect using the assigned hostname. Replace `username` and `password` with your `CLICKHOUSE_USER` and `CLICKHOUSE_PASSWORD`, and `database` with your `CLICKHOUSE_DB`. ```bash theme={null} curl -u 'username:password' \ 'https://mc-abc123xyz0.b-cdn.net/?query=SELECT+1' ``` ```bash theme={null} curl -u 'username:password' \ 'https://mc-abc123xyz0.b-cdn.net/?database=database' \ --data-binary " CREATE TABLE IF NOT EXISTS events ( ts DateTime, user_id UInt64, event String, value Float64 ) ENGINE = MergeTree() ORDER BY (ts, user_id) " ``` ```bash theme={null} curl -u 'username:password' \ 'https://mc-abc123xyz0.b-cdn.net/?database=database&query=INSERT+INTO+events+FORMAT+JSONEachRow' \ --data-binary ' {"ts":"2026-04-20 10:00:00","user_id":1,"event":"view","value":1.0} {"ts":"2026-04-20 10:01:00","user_id":2,"event":"click","value":2.5} {"ts":"2026-04-20 10:02:00","user_id":1,"event":"view","value":1.0} ' ``` ```bash theme={null} curl -u 'username:password' \ 'https://mc-abc123xyz0.b-cdn.net/?database=database' \ --data-binary " SELECT event, count() AS n, sum(value) AS total FROM events GROUP BY event ORDER BY n DESC FORMAT PrettyCompact " ``` CDN endpoints are publicly reachable with no authentication at the edge — anyone who knows the hostname can reach ClickHouse, and only the database's basic auth stands in the way. Always set a strong `CLICKHOUSE_PASSWORD` (never leave it as `clickhouse/clickhouse`), especially with `CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1` enabled. CDN endpoints only expose the HTTP interface (port `8123`). The native protocol (port `9000`) and clients that depend on it — including `clickhouse-client` without `--secure` and most BI tools' native drivers — won't work. Use HTTP-based drivers, or add an Anycast endpoint for port `9000`. Exposing your database to the internet means anyone with the credentials can connect. Use a strong password and consider removing the endpoint when external access is no longer needed. # Laravel + MariaDB Source: https://bunny.net/docs/magic-containers/guides/laravel Deploy a Laravel application with MariaDB to Magic Containers This guide walks you through building and deploying a Laravel application with MariaDB to Magic Containers with GitHub Container Registry. You'll need: * A GitHub account for source code and container registry * A bunny.net account with Magic Containers enabled ## Create the Laravel app Create a new Laravel project: ```bash theme={null} composer create-project laravel/laravel app-laravel cd app-laravel ``` Update your `.env` to use MariaDB: ```env .env theme={null} DB_CONNECTION=mariadb DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=laravel DB_USERNAME=laravel DB_PASSWORD=laravel ``` Use `127.0.0.1` instead of `localhost` for `DB_HOST`. Magic Containers share a localhost network between containers, but PHP/PDO interprets `localhost` as a Unix socket connection which will fail. Using `127.0.0.1` forces a TCP connection. ## Create the Nginx config Create `docker/nginx.conf`: ```nginx docker/nginx.conf theme={null} server { listen 80; server_name _; root /var/www/html/public; add_header X-Frame-Options "SAMEORIGIN"; add_header X-Content-Type-Options "nosniff"; index index.php; charset utf-8; location / { try_files $uri $uri/ /index.php?$query_string; } location = /favicon.ico { access_log off; log_not_found off; } location = /robots.txt { access_log off; log_not_found off; } error_page 404 /index.php; location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; include fastcgi_params; fastcgi_hide_header X-Powered-By; } location ~ /\.(?!well-known).* { deny all; } } ``` ## Create the supervisord config The app container runs both PHP-FPM and Nginx using supervisord. Create `docker/supervisord.conf`: ```ini docker/supervisord.conf theme={null} [supervisord] nodaemon=true logfile=/dev/stdout logfile_maxbytes=0 [program:php-fpm] command=php-fpm -F stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 [program:nginx] command=nginx -g "daemon off;" stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 ``` ## Create the entrypoint script Create `docker/entrypoint.sh`: ```bash docker/entrypoint.sh theme={null} #!/bin/sh cd /var/www/html touch .env php artisan config:cache php artisan route:cache php artisan view:cache echo "Waiting for database..." until php artisan db:monitor --databases=mariadb > /dev/null 2>&1; do sleep 1 done echo "Database is ready." php artisan migrate --force exec supervisord -c /etc/supervisord.conf ``` The entrypoint creates an empty `.env` file so Laravel reads configuration from the container's environment variables instead of a file. Config caching happens at startup (not at build time) so it picks up the runtime environment. The script also waits for MariaDB to be ready before running migrations. ## Create the Dockerfile ```dockerfile Dockerfile theme={null} FROM php:8.4-fpm-alpine RUN apk add --no-cache nginx supervisor curl \ && docker-php-ext-install pdo pdo_mysql COPY --from=composer:2 /usr/bin/composer /usr/bin/composer WORKDIR /var/www/html COPY composer.json composer.lock ./ RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist COPY . . RUN composer dump-autoload --optimize RUN chown -R www-data:www-data storage bootstrap/cache COPY docker/nginx.conf /etc/nginx/http.d/default.conf COPY docker/supervisord.conf /etc/supervisord.conf COPY docker/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh EXPOSE 80 CMD ["/entrypoint.sh"] ``` Do not run `php artisan config:cache` in the Dockerfile. Caching config at build time bakes in the build environment's values, which won't match the runtime environment variables set in Magic Containers. The entrypoint script handles config caching at startup instead. Create a `.dockerignore` to keep the `.env` file out of the image: ```text .dockerignore theme={null} .env .env.example .git node_modules vendor storage/logs/* tests .github ``` ## Create the docker-compose file Create `docker-compose.yml` for local development: ```yaml docker-compose.yml theme={null} services: app: build: . ports: - "8000:80" environment: - DB_CONNECTION=mariadb - DB_HOST=db - DB_PORT=3306 - DB_DATABASE=laravel - DB_USERNAME=laravel - DB_PASSWORD=laravel depends_on: db: condition: service_healthy db: image: mariadb:11 environment: MARIADB_DATABASE: laravel MARIADB_USER: laravel MARIADB_PASSWORD: laravel MARIADB_ROOT_PASSWORD: root volumes: - dbdata:/var/lib/mysql healthcheck: test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] interval: 5s timeout: 5s retries: 5 volumes: dbdata: ``` The `docker-compose.yml` uses `DB_HOST=db` (the service name) for local development. In Magic Containers, the `bunny.json` sets `DB_HOST=127.0.0.1` since containers share the same localhost network. ## Run locally ```bash theme={null} docker compose up --build ``` Visit [http://localhost:8000](http://localhost:8000). ## Generate an app key Generate a key to use in production: ```bash theme={null} php artisan key:generate --show ``` Keep this value for the next step. ## Build and push to GitHub Container Registry Create `.github/workflows/deploy.yml` to automatically build and push on every commit to `main`: ```yaml .github/workflows/build.yml theme={null} name: Build and Push on: push: branches: [main] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build-and-push: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v5 with: context: . push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} - name: Update container image on Magic Containers uses: BunnyWay/actions/container-update-image@main with: app_id: ${{ vars.APP_ID }} api_key: ${{ secrets.BUNNYNET_API_KEY }} container: app image_tag: "${{ github.sha }}" ``` Push your code to trigger the workflow: ```bash theme={null} git init git add . git commit -m "Initial commit" git remote add origin https://github.com/YOUR_USERNAME/app-laravel.git git push -u origin main ``` Build and push manually from your local machine. Go to [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens) and create a token with `write:packages` scope. ```bash theme={null} export CR_PAT=your_personal_access_token echo $CR_PAT | docker login ghcr.io -u YOUR_USERNAME --password-stdin ``` ```bash theme={null} docker build --platform linux/amd64 -t ghcr.io/YOUR_USERNAME/app-laravel:latest . ``` Magic Containers only supports images built for the **linux/amd64** architecture. The `--platform` flag ensures compatibility regardless of your local machine's architecture. ```bash theme={null} docker push ghcr.io/YOUR_USERNAME/app-laravel:latest ``` If your package is private, set the visibility to **Public** in GitHub or [configure Magic Containers with registry credentials](/docs/magic-containers/image-registries). ## Deploy to Magic Containers In the bunny.net dashboard, go to **Magic Containers** and click **Add App**. Enter a name and select your deployment option. Click **Add Container**, then configure: | Field | Value | | -------- | ---------------------------------------------------------------------------- | | Registry | GitHub Container Registry | | Image | `YOUR_USERNAME/{imageName}` | | Tag | `latest` for Docker CLI, or the commit SHA from your GitHub Actions workflow | Go to the **Endpoints** tab, click **Add New Endpoint**, and set the container port to `80`. Click **Add Container**, then **Next Step**, and **Confirm and Create**. For more details, see the [quickstart guide](/docs/magic-containers/quickstart). When configuring the app, add two containers: ### App container * **Image**: `ghcr.io//app-laravel:latest` * **Endpoint**: port `80` * **Environment variables**: * `APP_ENV` = `production` * `APP_DEBUG` = `false` * `APP_KEY` = the key from `php artisan key:generate --show` * `DB_CONNECTION` = `mariadb` * `DB_HOST` = `127.0.0.1` * `DB_PORT` = `3306` * `DB_DATABASE` = `laravel` * `DB_USERNAME` = `laravel` * `DB_PASSWORD` = `laravel` ### Database container * **Image**: `mariadb:11` * **Volume**: `/var/lib/mysql` * **Environment variables**: * `MARIADB_DATABASE` = `laravel` * `MARIADB_USER` = `laravel` * `MARIADB_PASSWORD` = `laravel` * `MARIADB_ROOT_PASSWORD` = `root` ## Test your app ```bash theme={null} curl https://mc-xxx.bunny.run ``` You can [add a custom hostname](/docs/magic-containers/endpoints) from the **Endpoints** section in your app settings. ## Continuous deployment The workflow automatically deploys to Magic Containers on every push to `main`. Configure the following in your repository settings: * **Variable** `APP_ID` - your Magic Containers app ID * **Secret** `BUNNYNET_API_KEY` - your bunny.net API key ## Key differences for Magic Containers When deploying Laravel to Magic Containers, there are a few important things to keep in mind: | Topic | What to do | Why | | -------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `DB_HOST` | Use `127.0.0.1`, not `localhost` | PHP/PDO treats `localhost` as a Unix socket. Magic Containers share a localhost network, so TCP via `127.0.0.1` is required. | | Config caching | Cache at startup, not build time | `php artisan config:cache` in the Dockerfile bakes in build-time values. Cache in the entrypoint so runtime env vars are used. | | `.env` file | Exclude from Docker image | Add `.env` to `.dockerignore` and `touch .env` in the entrypoint. This ensures Laravel reads from container environment variables. | | Migrations | Run at startup | The entrypoint waits for MariaDB, then runs `php artisan migrate --force` so the database is always up to date. | | App key | Set via environment variable | Generate with `php artisan key:generate --show` and set `APP_KEY` in the container environment. | ## Next steps 1. [Automate deploys with GitHub Actions](/docs/magic-containers/deploy-with-github-actions) 2. [Add a custom hostname](/docs/magic-containers/endpoints) 3. [Add a persistent volume](/docs/magic-containers/persistent-volumes) # MariaDB Source: https://bunny.net/docs/magic-containers/guides/mariadb Deploy MariaDB to Magic Containers This guide walks you through deploying MariaDB to Magic Containers, either as a standalone container or as part of a [multi-container](/docs/magic-containers/multi-container) app alongside your application. Don't have a bunny.net account yet? [Sign up](https://dash.bunny.net/auth/register) and enable Magic Containers to get started. ## Quickstart Go to the [bunny.net dashboard](https://dash.bunny.net), select **Magic Containers**, and click **Add App**. Select **Single region deployment**. Databases should use single region deployment with a single instance. Each pod gets its own dedicated volume with no data replication — this applies both across regions and within the same region. Scaling to multiple pods or regions would result in separate, isolated databases each with their own data. Click **Add Container** and configure the image: * **Registry**: Docker Hub * **Image**: `library/mariadb` * **Tag**: `11` Magic Containers will automatically detect the required endpoint and environment variables for the image. Configure the environment variables as needed: * `MARIADB_ROOT_PASSWORD` = a strong root password * `MARIADB_USER` = `app` * `MARIADB_PASSWORD` = a strong password * `MARIADB_DATABASE` = `app` In the **Volumes** section of the container settings, add a volume: * **Name**: `mariadb-data` * **Mount path**: `/var/lib/mysql` This ensures your database files persist across restarts and redeployments. Without a volume, all data is lost when the container stops. See [persistent volumes](/docs/magic-containers/persistent-volumes) for more details on volume behavior and pricing. Review your settings and click **Confirm and Create**. Always set strong passwords for `MARIADB_ROOT_PASSWORD` and `MARIADB_PASSWORD`, even if the database is not exposed externally. Other containers in the same pod can access the network, and a password protects against accidental or unauthorized access. ## Environment variables The official MariaDB image supports these environment variables: | Variable | Description | Default | | ----------------------- | --------------------------------------- | ------- | | `MARIADB_ROOT_PASSWORD` | Root user password (required) | - | | `MARIADB_USER` | Additional user to create | - | | `MARIADB_PASSWORD` | Password for the additional user | - | | `MARIADB_DATABASE` | Default database created on first start | - | ## Connect from your app In a [multi-container](/docs/magic-containers/multi-container) setup, your app and MariaDB share the same localhost network. Connect using `127.0.0.1` and the default port `3306`. ``` mysql://app:YOUR_PASSWORD@127.0.0.1:3306/app ``` ```javascript theme={null} import mysql from "mysql2/promise"; const connection = await mysql.createConnection( process.env.DATABASE_URL ); const [rows] = await connection.execute("SELECT NOW()"); ``` ```go theme={null} import ( "database/sql" "os" _ "github.com/go-sql-driver/mysql" ) db, err := sql.Open("mysql", os.Getenv("DATABASE_URL")) if err != nil { log.Fatal(err) } defer db.Close() ``` ```python theme={null} import MySQLdb import os conn = MySQLdb.connect( host="127.0.0.1", user="app", passwd=os.environ["MARIADB_PASSWORD"], db="app", ) cur = conn.cursor() cur.execute("SELECT NOW()") ``` ```php theme={null} query('SELECT NOW()')->fetch(); ``` Use `127.0.0.1` instead of `localhost` for the database host. Some clients (including PHP/PDO) interpret `localhost` as a Unix socket connection, which will fail in a container environment. Using `127.0.0.1` forces a TCP connection. ## Multi-container example A typical setup pairs MariaDB with your application. When configuring the app, add two containers: ### App container * **Image**: your app image (e.g. `ghcr.io//my-app:latest`) * **Endpoint**: the port your app listens on * **Environment variables**: * `DATABASE_URL` = `mysql://app:YOUR_PASSWORD@127.0.0.1:3306/app` ### MariaDB container * **Image**: `library/mariadb:11` * **Volume**: mount path `/var/lib/mysql` * **Environment variables**: * `MARIADB_ROOT_PASSWORD` = a strong root password * `MARIADB_USER` = `app` * `MARIADB_PASSWORD` = a strong password * `MARIADB_DATABASE` = `app` Both containers share the same localhost network, so your app connects to MariaDB at `127.0.0.1:3306`. See [multi-container apps](/docs/magic-containers/multi-container) for more details. ## External access To connect to MariaDB from outside Magic Containers (e.g. from your local terminal), add an [Anycast endpoint](/docs/magic-containers/endpoints): 1. Go to your app's **Endpoints** tab and click **Add New Endpoint** 2. Select **Anycast** as the type 3. Set **Container Port** to `3306` 4. Set **Exposed Port** to `3306` 5. Click **Add Endpoint** Then connect using the Anycast IP and the **exposed port**: ```bash theme={null} mariadb -h -P -u app -p --database app ``` The exposed port and container port may differ. When connecting externally, always use the exposed port shown in your endpoint configuration. Exposing your database to the internet means anyone with the credentials can connect. Use a strong password and consider removing the Anycast endpoint when external access is no longer needed. # Next.js Source: https://bunny.net/docs/magic-containers/guides/nextjs Deploy a Next.js application to Magic Containers This guide walks you through building and deploying a Next.js application to Magic Containers with GitHub Container Registry. You'll need: * A GitHub account for source code and container registry Don't have a bunny.net account yet? [Sign up](https://dash.bunny.net/auth/register) and enable Magic Containers to get started. ## Create the Next.js app Create a new Next.js project: ```bash theme={null} npx create-next-app@latest app-nextjs cd app-nextjs ``` ### Configure standalone output Update your `next.config.ts` to enable standalone output mode for Docker: ```typescript next.config.ts theme={null} import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "standalone", }; export default nextConfig; ``` ### Create an API route Create a simple API route to test the deployment: ```typescript app/api/route.ts theme={null} import { NextResponse } from "next/server"; export async function GET() { return NextResponse.json({ message: "Hello from Bunny 🐰" }); } ``` ## Run locally Start the development server: ```bash theme={null} npm run dev ``` Visit [http://localhost:3000](http://localhost:3000) in your browser, or test the API route: ```bash theme={null} curl http://localhost:3000/api ``` ## Create the Dockerfile ```dockerfile Dockerfile theme={null} FROM node:22-alpine AS base FROM base AS deps WORKDIR /app COPY package*.json ./ RUN npm ci FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build FROM base AS runner WORKDIR /app ENV NODE_ENV=production COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static RUN if [ -d "/app/public" ]; then cp -r /app/public ./public; fi ENV PORT=80 EXPOSE 80 CMD ["node", "server.js"] ``` ## Build and push to GitHub Container Registry Create `.github/workflows/build.yml` to automatically build and push on every commit to `main`: ```yaml .github/workflows/build.yml theme={null} name: Build and Push on: push: branches: [main] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build-and-push: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v5 with: context: . push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} - name: Update container image on Magic Containers uses: BunnyWay/actions/container-update-image@main with: app_id: ${{ vars.APP_ID }} api_key: ${{ secrets.BUNNYNET_API_KEY }} container: app image_tag: "${{ github.sha }}" ``` Push your code to trigger the workflow: ```bash theme={null} git init git add . git commit -m "Initial commit" git remote add origin https://github.com/YOUR_USERNAME/app-nextjs.git git push -u origin main ``` Build and push manually from your local machine. Go to [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens) and create a token with `write:packages` scope. ```bash theme={null} export CR_PAT=your_personal_access_token echo $CR_PAT | docker login ghcr.io -u YOUR_USERNAME --password-stdin ``` ```bash theme={null} docker build --platform linux/amd64 -t ghcr.io/YOUR_USERNAME/app-nextjs:latest . ``` Magic Containers only supports images built for the **linux/amd64** architecture. The `--platform` flag ensures compatibility regardless of your local machine's architecture. ```bash theme={null} docker push ghcr.io/YOUR_USERNAME/app-nextjs:latest ``` If your package is private, set the visibility to **Public** in GitHub or [configure Magic Containers with registry credentials](/docs/magic-containers/image-registries). ## Deploy to Magic Containers In the bunny.net dashboard, go to **Magic Containers** and click **Add App**. Enter a name and select your deployment option. Click **Add Container**, then configure: | Field | Value | | -------- | ---------------------------------------------------------------------------- | | Registry | GitHub Container Registry | | Image | `YOUR_USERNAME/{imageName}` | | Tag | `latest` for Docker CLI, or the commit SHA from your GitHub Actions workflow | Go to the **Endpoints** tab, click **Add New Endpoint**, and set the container port to `80`. Click **Add Container**, then **Next Step**, and **Confirm and Create**. For more details, see the [quickstart guide](/docs/magic-containers/quickstart). ## Test your app Visit your container URL in the browser to see your Next.js site, or test the API route: ```bash theme={null} curl https://mc-xxx.bunny.run/api ``` ```json Response theme={null} { "message": "Hello from Bunny 🐰" } ``` You can [add a custom hostname](/docs/magic-containers/endpoints) from the **Endpoints** section in your app settings. ## Connect a database You can connect your app to [Bunny Database](/docs/database) directly from the dashboard: 1. Go to **Database > \[Your Database] > Access** 2. Click **Generate Tokens** 3. Click **Add Secrets to Magic Container App** 4. Select your app The `BUNNY_DATABASE_URL` and `BUNNY_DATABASE_AUTH_TOKEN` environment variables are now available in your app: ```typescript app/api/users/route.ts theme={null} import { NextResponse } from "next/server"; import { createClient } from "@libsql/client/web"; const client = createClient({ url: process.env.BUNNY_DATABASE_URL!, authToken: process.env.BUNNY_DATABASE_AUTH_TOKEN!, }); export async function GET() { const result = await client.execute("SELECT * FROM users"); return NextResponse.json(result.rows); } ``` See the [TypeScript SDK documentation](/docs/database/connect/typescript) for more details. ## Next steps 1. [Automate deploys with GitHub Actions](/docs/magic-containers/deploy-with-github-actions) 2. [Add a custom hostname](/docs/magic-containers/endpoints) 3. [Add a persistent volume](/docs/magic-containers/persistent-volumes) # Node.js API with Express Source: https://bunny.net/docs/magic-containers/guides/node-express-api Deploy a Node.js API with Express to Magic Containers This guide walks you through building and deploying a Node.js API using Express to Magic Containers with GitHub Container Registry. You'll need: * A GitHub account for source code and container registry Don't have a bunny.net account yet? [Sign up](https://dash.bunny.net/auth/register) and enable Magic Containers to get started. ## Create the Express API Create a new directory with the following files: ```javascript src/index.js theme={null} import express from "express"; const app = express(); app.use(express.json()); app.get("/", (req, res) => { res.json({ message: "Hello from Bunny 🐰" }); }); const port = Number(process.env.PORT) || 80; app.listen(port, () => { console.log(`Server starting on port ${port}`); }); ``` ```json package.json theme={null} { "name": "app-express-api", "type": "module", "scripts": { "dev": "node --watch src/index.js", "start": "node src/index.js" }, "dependencies": { "express": "^4.21.2" } } ``` ## Run locally Install dependencies and start the development server: ```bash theme={null} npm install npm run dev ``` Test the API: ```bash theme={null} curl http://localhost:80/ ``` ## Create the Dockerfile ```dockerfile Dockerfile theme={null} FROM node:22-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm install FROM node:22-alpine WORKDIR /app COPY package*.json ./ RUN npm install --omit=dev COPY --from=builder /app/node_modules ./node_modules COPY . . ENV PORT=80 EXPOSE 80 CMD ["node", "src/index.js"] ``` ## Build and push to GitHub Container Registry Create `.github/workflows/build.yml` to automatically build and push on every commit to `main`: ```yaml .github/workflows/build.yml theme={null} name: Build and Push on: push: branches: [main] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build-and-push: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v5 with: context: . push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} - name: Update container image on Magic Containers uses: BunnyWay/actions/container-update-image@main with: app_id: ${{ vars.APP_ID }} api_key: ${{ secrets.BUNNYNET_API_KEY }} container: app image_tag: "${{ github.sha }}" ``` Push your code to trigger the workflow: ```bash theme={null} git init git add . git commit -m "Initial commit" git remote add origin https://github.com/YOUR_USERNAME/app-express-api.git git push -u origin main ``` Build and push manually from your local machine. Go to [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens) and create a token with `write:packages` scope. ```bash theme={null} export CR_PAT=your_personal_access_token echo $CR_PAT | docker login ghcr.io -u YOUR_USERNAME --password-stdin ``` ```bash theme={null} docker build --platform linux/amd64 -t ghcr.io/YOUR_USERNAME/app-express-api:latest . ``` Magic Containers only supports images built for the **linux/amd64** architecture. The `--platform` flag ensures compatibility regardless of your local machine's architecture. ```bash theme={null} docker push ghcr.io/YOUR_USERNAME/app-express-api:latest ``` If your package is private, set the visibility to **Public** in GitHub or [configure Magic Containers with registry credentials](/docs/magic-containers/image-registries). ## Deploy to Magic Containers In the bunny.net dashboard, go to **Magic Containers** and click **Add App**. Enter a name and select your deployment option. Click **Add Container**, then configure: | Field | Value | | -------- | ---------------------------------------------------------------------------- | | Registry | GitHub Container Registry | | Image | `YOUR_USERNAME/{imageName}` | | Tag | `latest` for Docker CLI, or the commit SHA from your GitHub Actions workflow | Go to the **Endpoints** tab, click **Add New Endpoint**, and set the container port to `80`. Click **Add Container**, then **Next Step**, and **Confirm and Create**. For more details, see the [quickstart guide](/docs/magic-containers/quickstart). ## Test your API ```bash theme={null} curl https://mc-xxx.bunny.run/ ``` ```json Response theme={null} { "message": "Hello from Bunny 🐰" } ``` You can [add a custom hostname](/docs/magic-containers/endpoints) from the **Endpoints** section in your app settings. ## Connect a database You can connect your app to [Bunny Database](/docs/database) directly from the dashboard: 1. Go to **Database > \[Your Database] > Access** 2. Click **Generate Tokens** 3. Click **Add Secrets to Magic Container App** 4. Select your app The `BUNNY_DATABASE_URL` and `BUNNY_DATABASE_AUTH_TOKEN` environment variables are now available in your app: ```javascript theme={null} import { createClient } from "@libsql/client/web"; const client = createClient({ url: process.env.BUNNY_DATABASE_URL, authToken: process.env.BUNNY_DATABASE_AUTH_TOKEN, }); const result = await client.execute("SELECT * FROM users"); ``` See the [TypeScript SDK documentation](/docs/database/connect/typescript) for more details. ## Next steps 1. [Automate deploys with GitHub Actions](/docs/magic-containers/deploy-with-github-actions) 2. [Add a custom hostname](/docs/magic-containers/endpoints) 3. [Add a persistent volume](/docs/magic-containers/persistent-volumes) # Node.js API with Hono Source: https://bunny.net/docs/magic-containers/guides/node-hono-api Deploy a Node.js API with Hono to Magic Containers This guide walks you through building and deploying a Node.js API using Hono to Magic Containers with GitHub Container Registry. You'll need: * A GitHub account for source code and container registry Don't have a bunny.net account yet? [Sign up](https://dash.bunny.net/auth/register) and enable Magic Containers to get started. ## Create the Hono API Create a new directory with the following files: ```typescript src/index.ts theme={null} import { serve } from "@hono/node-server"; import { Hono } from "hono"; const app = new Hono(); app.get("/", (c) => { return c.json({ message: "Hello from Bunny 🐰" }); }); const port = Number(process.env.PORT) || 80; console.log(`Server starting on port ${port}`); serve({ fetch: app.fetch, port, }); ``` ```json package.json theme={null} { "name": "app-hono-api", "type": "module", "scripts": { "dev": "tsx watch src/index.ts", "build": "tsc", "start": "node dist/index.js" }, "dependencies": { "@hono/node-server": "^1.13.7", "hono": "^4.6.16" }, "devDependencies": { "@types/node": "^22.10.5", "tsx": "^4.19.2", "typescript": "^5.7.3" } } ``` ```json tsconfig.json theme={null} { "compilerOptions": { "target": "ES2022", "module": "ES2022", "moduleResolution": "node", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true }, "include": ["src/**/*"] } ``` ## Run locally Install dependencies and start the development server: ```bash theme={null} npm install npm run dev ``` Test the API: ```bash theme={null} curl http://localhost:80/ ``` ## Create the Dockerfile ```dockerfile Dockerfile theme={null} FROM node:22-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build FROM node:22-alpine WORKDIR /app COPY package*.json ./ RUN npm install --omit=dev COPY --from=builder /app/dist ./dist ENV PORT=80 EXPOSE 80 CMD ["node", "dist/index.js"] ``` ## Build and push to GitHub Container Registry Create `.github/workflows/build.yml` to automatically build and push on every commit to `main`: ```yaml .github/workflows/build.yml theme={null} name: Build and Push on: push: branches: [main] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build-and-push: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v5 with: context: . push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} - name: Update container image on Magic Containers uses: BunnyWay/actions/container-update-image@main with: app_id: ${{ vars.APP_ID }} api_key: ${{ secrets.BUNNYNET_API_KEY }} container: app image_tag: "${{ github.sha }}" ``` Push your code to trigger the workflow: ```bash theme={null} git init git add . git commit -m "Initial commit" git remote add origin https://github.com/YOUR_USERNAME/app-hono-api.git git push -u origin main ``` Build and push manually from your local machine. Go to [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens) and create a token with `write:packages` scope. ```bash theme={null} export CR_PAT=your_personal_access_token echo $CR_PAT | docker login ghcr.io -u YOUR_USERNAME --password-stdin ``` ```bash theme={null} docker build --platform linux/amd64 -t ghcr.io/YOUR_USERNAME/app-hono-api:latest . ``` Magic Containers only supports images built for the **linux/amd64** architecture. The `--platform` flag ensures compatibility regardless of your local machine's architecture. ```bash theme={null} docker push ghcr.io/YOUR_USERNAME/app-hono-api:latest ``` If your package is private, set the visibility to **Public** in GitHub or [configure Magic Containers with registry credentials](/docs/magic-containers/image-registries). ## Deploy to Magic Containers In the bunny.net dashboard, go to **Magic Containers** and click **Add App**. Enter a name and select your deployment option. Click **Add Container**, then configure: | Field | Value | | -------- | ---------------------------------------------------------------------------- | | Registry | GitHub Container Registry | | Image | `YOUR_USERNAME/{imageName}` | | Tag | `latest` for Docker CLI, or the commit SHA from your GitHub Actions workflow | Go to the **Endpoints** tab, click **Add New Endpoint**, and set the container port to `80`. Click **Add Container**, then **Next Step**, and **Confirm and Create**. For more details, see the [quickstart guide](/docs/magic-containers/quickstart). ## Test your API ```bash theme={null} curl https://mc-xxx.bunny.run/ ``` ```json Response theme={null} { "message": "Hello from Bunny 🐰" } ``` You can [add a custom hostname](/docs/magic-containers/endpoints) from the **Endpoints** section in your app settings. ## Connect a database You can connect your app to [Bunny Database](/docs/database) directly from the dashboard: 1. Go to **Database > \[Your Database] > Access** 2. Click **Generate Tokens** 3. Click **Add Secrets to Magic Container App** 4. Select your app The `BUNNY_DATABASE_URL` and `BUNNY_DATABASE_AUTH_TOKEN` environment variables are now available in your app: ```typescript theme={null} import { createClient } from "@libsql/client/web"; const client = createClient({ url: process.env.BUNNY_DATABASE_URL, authToken: process.env.BUNNY_DATABASE_AUTH_TOKEN, }); const result = await client.execute("SELECT * FROM users"); ``` See the [TypeScript SDK documentation](/docs/database/connect/typescript) for more details. ## Next steps 1. [Automate deploys with GitHub Actions](/docs/magic-containers/deploy-with-github-actions) 2. [Add a custom hostname](/docs/magic-containers/endpoints) 3. [Add a persistent volume](/docs/magic-containers/persistent-volumes) # Nuxt Source: https://bunny.net/docs/magic-containers/guides/nuxt Deploy a Nuxt application to Magic Containers This guide walks you through building and deploying a Nuxt application to Magic Containers with GitHub Container Registry. You'll need: * A GitHub account for source code and container registry Don't have a bunny.net account yet? [Sign up](https://dash.bunny.net/auth/register) and enable Magic Containers to get started. ## Create the Nuxt app Create a new Nuxt project: ```bash theme={null} npx nuxi@latest init app-nuxt cd app-nuxt ``` ### Create an API route Create a simple API route to test the deployment: ```typescript server/api/index.ts theme={null} export default defineEventHandler(() => { return { message: "Hello from Bunny 🐰" }; }); ``` ## Run locally Install dependencies and start the development server: ```bash theme={null} npm install npm run dev ``` Visit [http://localhost:3000](http://localhost:3000) in your browser, or test the API route: ```bash theme={null} curl http://localhost:3000/api ``` ## Create the Dockerfile ```dockerfile Dockerfile theme={null} FROM node:22-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build FROM node:22-alpine AS runtime WORKDIR /app COPY --from=build /app/.output ./ ENV PORT=80 EXPOSE 80 CMD ["node", "server/index.mjs"] ``` ## Build and push to GitHub Container Registry Create `.github/workflows/build.yml` to automatically build and push on every commit to `main`: ```yaml .github/workflows/build.yml theme={null} name: Build and Push on: push: branches: [main] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build-and-push: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v5 with: context: . push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} - name: Update container image on Magic Containers uses: BunnyWay/actions/container-update-image@main with: app_id: ${{ vars.APP_ID }} api_key: ${{ secrets.BUNNYNET_API_KEY }} container: app image_tag: "${{ github.sha }}" ``` Push your code to trigger the workflow: ```bash theme={null} git init git add . git commit -m "Initial commit" git remote add origin https://github.com/YOUR_USERNAME/app-nuxt.git git push -u origin main ``` Build and push manually from your local machine. Go to [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens) and create a token with `write:packages` scope. ```bash theme={null} export CR_PAT=your_personal_access_token echo $CR_PAT | docker login ghcr.io -u YOUR_USERNAME --password-stdin ``` ```bash theme={null} docker build --platform linux/amd64 -t ghcr.io/YOUR_USERNAME/app-nuxt:latest . ``` Magic Containers only supports images built for the **linux/amd64** architecture. The `--platform` flag ensures compatibility regardless of your local machine's architecture. ```bash theme={null} docker push ghcr.io/YOUR_USERNAME/app-nuxt:latest ``` If your package is private, set the visibility to **Public** in GitHub or [configure Magic Containers with registry credentials](/docs/magic-containers/image-registries). ## Deploy to Magic Containers In the bunny.net dashboard, go to **Magic Containers** and click **Add App**. Enter a name and select your deployment option. Click **Add Container**, then configure: | Field | Value | | -------- | ---------------------------------------------------------------------------- | | Registry | GitHub Container Registry | | Image | `YOUR_USERNAME/{imageName}` | | Tag | `latest` for Docker CLI, or the commit SHA from your GitHub Actions workflow | Go to the **Endpoints** tab, click **Add New Endpoint**, and set the container port to `80`. Click **Add Container**, then **Next Step**, and **Confirm and Create**. For more details, see the [quickstart guide](/docs/magic-containers/quickstart). ## Test your app Visit your container URL in the browser to see your Nuxt site, or test the API route: ```bash theme={null} curl https://mc-xxx.bunny.run/api ``` ```json Response theme={null} { "message": "Hello from Bunny 🐰" } ``` You can [add a custom hostname](/docs/magic-containers/endpoints) from the **Endpoints** section in your app settings. ## Connect a database You can connect your app to [Bunny Database](/docs/database) directly from the dashboard: 1. Go to **Database > \[Your Database] > Access** 2. Click **Generate Tokens** 3. Click **Add Secrets to Magic Container App** 4. Select your app The `BUNNY_DATABASE_URL` and `BUNNY_DATABASE_AUTH_TOKEN` environment variables are now available in your app: ```typescript server/api/users.ts theme={null} import { createClient } from "@libsql/client/web"; const client = createClient({ url: process.env.BUNNY_DATABASE_URL!, authToken: process.env.BUNNY_DATABASE_AUTH_TOKEN!, }); export default defineEventHandler(async () => { const result = await client.execute("SELECT * FROM users"); return result.rows; }); ``` See the [TypeScript SDK documentation](/docs/database/connect/typescript) for more details. ## Next steps 1. [Automate deploys with GitHub Actions](/docs/magic-containers/deploy-with-github-actions) 2. [Add a custom hostname](/docs/magic-containers/endpoints) 3. [Add a persistent volume](/docs/magic-containers/persistent-volumes) # PHP API with Slim Source: https://bunny.net/docs/magic-containers/guides/php-slim Deploy a PHP API with Slim Framework to Magic Containers This guide walks you through building and deploying a PHP API using Slim Framework to Magic Containers with GitHub Container Registry. You'll need: * A GitHub account for source code and container registry Don't have a bunny.net account yet? [Sign up](https://dash.bunny.net/auth/register) and enable Magic Containers to get started. ## Create the Slim app Create a new directory and initialize the project: ```bash theme={null} mkdir app-slim-api cd app-slim-api composer require slim/slim slim/psr7 ``` Create the following files: ```php public/index.php theme={null} get('/', function (Request $request, Response $response) { $response->getBody()->write(json_encode(['message' => 'Hello from Bunny 🐰'])); return $response->withHeader('Content-Type', 'application/json'); }); $app->run(); ``` ```json composer.json theme={null} { "require": { "slim/slim": "^4.0", "slim/psr7": "^1.0" } } ``` ## Run locally Start the PHP development server: ```bash theme={null} php -S localhost:8080 -t public ``` Test the API: ```bash theme={null} curl http://localhost:8080/ ``` ## Create the Dockerfile ```dockerfile Dockerfile theme={null} FROM php:8.3-apache RUN a2enmod rewrite COPY --from=composer:latest /usr/bin/composer /usr/bin/composer WORKDIR /var/www/html COPY composer.json . RUN composer install --no-dev --optimize-autoloader COPY public/ public/ RUN sed -i 's|/var/www/html|/var/www/html/public|g' /etc/apache2/sites-available/000-default.conf RUN echo '\n\ AllowOverride All\n\ Require all granted\n\ ' >> /etc/apache2/apache2.conf RUN echo 'RewriteEngine On\n\ RewriteCond %{REQUEST_FILENAME} !-f\n\ RewriteCond %{REQUEST_FILENAME} !-d\n\ RewriteRule ^ index.php [QSA,L]' > public/.htaccess EXPOSE 80 ``` ## Build and push to GitHub Container Registry Create `.github/workflows/build.yml` to automatically build and push on every commit to `main`: ```yaml .github/workflows/build.yml theme={null} name: Build and Push on: push: branches: [main] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build-and-push: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v5 with: context: . push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} - name: Update container image on Magic Containers uses: BunnyWay/actions/container-update-image@main with: app_id: ${{ vars.APP_ID }} api_key: ${{ secrets.BUNNYNET_API_KEY }} container: app image_tag: "${{ github.sha }}" ``` Push your code to trigger the workflow: ```bash theme={null} git init git add . git commit -m "Initial commit" git remote add origin https://github.com/YOUR_USERNAME/app-slim-api.git git push -u origin main ``` Build and push manually from your local machine. Go to [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens) and create a token with `write:packages` scope. ```bash theme={null} export CR_PAT=your_personal_access_token echo $CR_PAT | docker login ghcr.io -u YOUR_USERNAME --password-stdin ``` ```bash theme={null} docker build --platform linux/amd64 -t ghcr.io/YOUR_USERNAME/app-slim-api:latest . ``` Magic Containers only supports images built for the **linux/amd64** architecture. The `--platform` flag ensures compatibility regardless of your local machine's architecture. ```bash theme={null} docker push ghcr.io/YOUR_USERNAME/app-slim-api:latest ``` If your package is private, set the visibility to **Public** in GitHub or [configure Magic Containers with registry credentials](/docs/magic-containers/image-registries). ## Deploy to Magic Containers In the bunny.net dashboard, go to **Magic Containers** and click **Add App**. Enter a name and select your deployment option. Click **Add Container**, then configure: | Field | Value | | -------- | ---------------------------------------------------------------------------- | | Registry | GitHub Container Registry | | Image | `YOUR_USERNAME/{imageName}` | | Tag | `latest` for Docker CLI, or the commit SHA from your GitHub Actions workflow | Go to the **Endpoints** tab, click **Add New Endpoint**, and set the container port to `80`. Click **Add Container**, then **Next Step**, and **Confirm and Create**. For more details, see the [quickstart guide](/docs/magic-containers/quickstart). ## Test your API ```bash theme={null} curl https://mc-xxx.bunny.run/ ``` ```json Response theme={null} { "message": "Hello from Bunny 🐰" } ``` You can [add a custom hostname](/docs/magic-containers/endpoints) from the **Endpoints** section in your app settings. ## Connect a database You can connect your app to [Bunny Database](/docs/database) directly from the dashboard: 1. Go to **Database > \[Your Database] > Access** 2. Click **Generate Tokens** 3. Click **Add Secrets to Magic Container App** 4. Select your app The `BUNNY_DATABASE_URL` and `BUNNY_DATABASE_AUTH_TOKEN` environment variables are now available in your app. Install the libSQL PHP extension or use HTTP requests to connect: ```php theme={null} true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $dbToken, 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'statements' => ['SELECT * FROM users'] ]), ]); $result = json_decode(curl_exec($ch), true); curl_close($ch); ``` See the [Bunny Database documentation](/docs/database) for more details. ## Next steps 1. [Automate deploys with GitHub Actions](/docs/magic-containers/deploy-with-github-actions) 2. [Add a custom hostname](/docs/magic-containers/endpoints) 3. [Add a persistent volume](/docs/magic-containers/persistent-volumes) # PostgreSQL Source: https://bunny.net/docs/magic-containers/guides/postgresql Deploy PostgreSQL to Magic Containers This guide walks you through deploying PostgreSQL to Magic Containers, either as a standalone container or as part of a [multi-container](/docs/magic-containers/multi-container) app alongside your application. Don't have a bunny.net account yet? [Sign up](https://dash.bunny.net/auth/register) and enable Magic Containers to get started. ## Quickstart Go to the [bunny.net dashboard](https://dash.bunny.net), select **Magic Containers**, and click **Add App**. Select **Single region deployment**. Databases should use single region deployment with a single instance. Each pod gets its own dedicated volume with no data replication — this applies both across regions and within the same region. Scaling to multiple pods or regions would result in separate, isolated databases each with their own data. Click **Add Container** and configure the image: * **Registry**: Docker Hub * **Image**: `library/postgres` * **Tag**: `17-alpine` Magic Containers will automatically detect the required endpoint and environment variables for the image. Configure the environment variables as needed: * `POSTGRES_USER` = `postgres` * `POSTGRES_PASSWORD` = a strong password * `POSTGRES_DB` = `app` * `PGDATA` = `/var/lib/postgresql/data/pgdata` In the **Volumes** section of the container settings, add a volume: * **Name**: `postgres-data` * **Mount path**: `/var/lib/postgresql/data` This ensures your database files persist across restarts and redeployments. Without a volume, all data is lost when the container stops. See [persistent volumes](/docs/magic-containers/persistent-volumes) for more details on volume behavior and pricing. Review your settings and click **Confirm and Create**. Always set a strong `POSTGRES_PASSWORD`, even if the database is not exposed externally. Other containers in the same pod can access the network, and a password protects against accidental or unauthorized access. You must set `PGDATA` to a subdirectory of the volume mount (e.g. `/var/lib/postgresql/data/pgdata`). PostgreSQL requires the data directory to be empty on first initialization, and the volume mount point itself may contain system files. ## Environment variables The official PostgreSQL image supports these environment variables: | Variable | Description | Default | | ------------------- | --------------------------------------- | -------------------------- | | `POSTGRES_USER` | Superuser name | `postgres` | | `POSTGRES_PASSWORD` | Superuser password (required) | - | | `POSTGRES_DB` | Default database created on first start | `postgres` | | `PGDATA` | Data directory inside the container | `/var/lib/postgresql/data` | ## Connect from your app In a [multi-container](/docs/magic-containers/multi-container) setup, your app and PostgreSQL share the same localhost network. Connect using `127.0.0.1` and the default port `5432`. ``` postgresql://postgres:YOUR_PASSWORD@127.0.0.1:5432/app ``` ```javascript theme={null} import pg from "pg"; const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, }); const result = await pool.query("SELECT NOW()"); ``` ```go theme={null} import ( "database/sql" "os" _ "github.com/lib/pq" ) db, err := sql.Open("postgres", os.Getenv("DATABASE_URL")) if err != nil { log.Fatal(err) } defer db.Close() ``` ```python theme={null} import psycopg2 import os conn = psycopg2.connect(os.environ["DATABASE_URL"]) cur = conn.cursor() cur.execute("SELECT NOW()") ``` ```php theme={null} query('SELECT NOW()')->fetch(); ``` ## Multi-container example A typical setup pairs PostgreSQL with your application. When configuring the app, add two containers: ### App container * **Image**: your app image (e.g. `ghcr.io//my-app:latest`) * **Endpoint**: the port your app listens on * **Environment variables**: * `DATABASE_URL` = `postgresql://postgres:YOUR_PASSWORD@127.0.0.1:5432/app` ### PostgreSQL container * **Image**: `postgres:17-alpine` * **Volume**: mount path `/var/lib/postgresql/data` * **Environment variables**: * `POSTGRES_USER` = `postgres` * `POSTGRES_PASSWORD` = a strong password * `POSTGRES_DB` = `app` * `PGDATA` = `/var/lib/postgresql/data/pgdata` Both containers share the same localhost network, so your app connects to PostgreSQL at `127.0.0.1:5432`. See [multi-container apps](/docs/magic-containers/multi-container) for more details. ## External access To connect to PostgreSQL from outside Magic Containers (e.g. from your local terminal), add an [Anycast endpoint](/docs/magic-containers/endpoints): 1. Go to your app's **Endpoints** tab and click **Add New Endpoint** 2. Select **Anycast** as the type 3. Set **Container Port** to `5432` 4. Set **Exposed Port** to `5432` 5. Click **Add Endpoint** Then connect using the Anycast IP and the **exposed port**: ```bash theme={null} psql -h -p -U postgres -d app ``` The exposed port and container port may differ. When connecting externally, always use the exposed port shown in your endpoint configuration. Exposing your database to the internet means anyone with the credentials can connect. Use a strong password and consider removing the Anycast endpoint when external access is no longer needed. ## TimescaleDB [TimescaleDB](https://www.timescale.com/) is a PostgreSQL extension for time-series data. It uses the same base configuration as PostgreSQL with a different image. ### Configuration Use the official TimescaleDB image instead of the standard PostgreSQL image: * **Registry**: Docker Hub * **Image**: `timescale/timescaledb` * **Tag**: `latest-pg17` (or another [supported tag](https://hub.docker.com/r/timescale/timescaledb/tags)) Configure the same environment variables as PostgreSQL: * `POSTGRES_USER` = `postgres` * `POSTGRES_PASSWORD` = a strong password * `POSTGRES_DB` = `app` * `PGDATA` = `/var/lib/postgresql/data/pgdata` The `PGDATA` environment variable is required when mounting a volume at `/var/lib/postgresql/data`. The mount point may contain a `lost+found` directory or other system files, which causes TimescaleDB initialization to fail. Setting `PGDATA` to a subdirectory (e.g., `/var/lib/postgresql/data/pgdata`) resolves this issue. ### Volume configuration Add a persistent volume with the same mount path as PostgreSQL: * **Name**: `timescaledb-data` * **Mount path**: `/var/lib/postgresql/data` ### Connection TimescaleDB uses the same connection methods as PostgreSQL. Connect using port `5432`: ``` postgresql://postgres:YOUR_PASSWORD@127.0.0.1:5432/app ``` After connecting, enable the TimescaleDB extension in your database: ```sql theme={null} CREATE EXTENSION IF NOT EXISTS timescaledb; ``` # Python API with FastAPI Source: https://bunny.net/docs/magic-containers/guides/python-fastapi Deploy a Python API with FastAPI to Magic Containers This guide walks you through building and deploying a Python API using FastAPI to Magic Containers with GitHub Container Registry. You'll need: * A GitHub account for source code and container registry Don't have a bunny.net account yet? [Sign up](https://dash.bunny.net/auth/register) and enable Magic Containers to get started. ## Create the FastAPI app Create a new directory with the following files: ```python main.py theme={null} from fastapi import FastAPI import os app = FastAPI() @app.get("/") def read_root(): return {"message": "Hello from Bunny 🐰"} if __name__ == "__main__": import uvicorn port = int(os.environ.get("PORT", 80)) uvicorn.run(app, host="0.0.0.0", port=port) ``` ```txt requirements.txt theme={null} fastapi==0.115.6 uvicorn==0.34.0 ``` ## Run locally Install dependencies and start the development server: ```bash theme={null} pip install -r requirements.txt uvicorn main:app --reload ``` Test the API: ```bash theme={null} curl http://localhost:8000/ ``` ## Create the Dockerfile ```dockerfile Dockerfile theme={null} FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY main.py . EXPOSE 80 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"] ``` ## Build and push to GitHub Container Registry Create `.github/workflows/build.yml` to automatically build and push on every commit to `main`: ```yaml .github/workflows/build.yml theme={null} name: Build and Push on: push: branches: [main] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build-and-push: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v5 with: context: . push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} - name: Update container image on Magic Containers uses: BunnyWay/actions/container-update-image@main with: app_id: ${{ vars.APP_ID }} api_key: ${{ secrets.BUNNYNET_API_KEY }} container: app image_tag: "${{ github.sha }}" ``` Push your code to trigger the workflow: ```bash theme={null} git init git add . git commit -m "Initial commit" git remote add origin https://github.com/YOUR_USERNAME/app-fastapi.git git push -u origin main ``` Build and push manually from your local machine. Go to [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens) and create a token with `write:packages` scope. ```bash theme={null} export CR_PAT=your_personal_access_token echo $CR_PAT | docker login ghcr.io -u YOUR_USERNAME --password-stdin ``` ```bash theme={null} docker build --platform linux/amd64 -t ghcr.io/YOUR_USERNAME/app-fastapi:latest . ``` Magic Containers only supports images built for the **linux/amd64** architecture. The `--platform` flag ensures compatibility regardless of your local machine's architecture. ```bash theme={null} docker push ghcr.io/YOUR_USERNAME/app-fastapi:latest ``` If your package is private, set the visibility to **Public** in GitHub or [configure Magic Containers with registry credentials](/docs/magic-containers/image-registries). ## Deploy to Magic Containers In the bunny.net dashboard, go to **Magic Containers** and click **Add App**. Enter a name and select your deployment option. Click **Add Container**, then configure: | Field | Value | | -------- | ---------------------------------------------------------------------------- | | Registry | GitHub Container Registry | | Image | `YOUR_USERNAME/{imageName}` | | Tag | `latest` for Docker CLI, or the commit SHA from your GitHub Actions workflow | Go to the **Endpoints** tab, click **Add New Endpoint**, and set the container port to `80`. Click **Add Container**, then **Next Step**, and **Confirm and Create**. For more details, see the [quickstart guide](/docs/magic-containers/quickstart). ## Test your API ```bash theme={null} curl https://mc-xxx.bunny.run/ ``` ```json Response theme={null} { "message": "Hello from Bunny 🐰" } ``` You can [add a custom hostname](/docs/magic-containers/endpoints) from the **Endpoints** section in your app settings. ## Connect a database You can connect your app to [Bunny Database](/docs/database) directly from the dashboard: 1. Go to **Database > \[Your Database] > Access** 2. Click **Generate Tokens** 3. Click **Add Secrets to Magic Container App** 4. Select your app The `BUNNY_DATABASE_URL` and `BUNNY_DATABASE_AUTH_TOKEN` environment variables are now available in your app: ```python theme={null} import libsql_experimental as libsql import os conn = libsql.connect( database=os.environ["BUNNY_DATABASE_URL"], auth_token=os.environ["BUNNY_DATABASE_AUTH_TOKEN"] ) result = conn.execute("SELECT * FROM users").fetchall() ``` See the [Bunny Database documentation](/docs/database) for more details. ## Next steps 1. [Automate deploys with GitHub Actions](/docs/magic-containers/deploy-with-github-actions) 2. [Add a custom hostname](/docs/magic-containers/endpoints) 3. [Add a persistent volume](/docs/magic-containers/persistent-volumes) # Redis Source: https://bunny.net/docs/magic-containers/guides/redis Deploy Redis to Magic Containers This guide walks you through deploying Redis to Magic Containers, either as a standalone container or as part of a [multi-container](/docs/magic-containers/multi-container) app alongside your application. Don't have a bunny.net account yet? [Sign up](https://dash.bunny.net/auth/register) and enable Magic Containers to get started. ## Quickstart Go to the [bunny.net dashboard](https://dash.bunny.net), select **Magic Containers**, and click **Add App**. Select **Single region deployment**. Databases should use single region deployment with a single instance. Each pod gets its own dedicated volume with no data replication — this applies both across regions and within the same region. Scaling to multiple pods or regions would result in separate, isolated databases each with their own data. Click **Add Container** and configure the image: * **Registry**: Docker Hub * **Image**: `library/redis` * **Tag**: `7-alpine` Magic Containers will automatically detect the required endpoint and environment variables for the image. In the **Volumes** section of the container settings, add a volume: * **Name**: `redis-data` * **Mount path**: `/data` This ensures your data persists across restarts and redeployments. Without a volume, all data is lost when the container stops. See [persistent volumes](/docs/magic-containers/persistent-volumes) for more details on volume behavior and pricing. Review your settings and click **Confirm and Create**. ## Redis 8+ volume permissions Redis 8 changed its default user from `root` to a non-root user (`uid 999`, group `redis`). When Magic Containers mounts a persistent volume at `/data`, the directory is owned by `root`. This means the Redis process cannot write its RDB/AOF persistence files to this directory, causing the container to fail on startup with an error like: ``` Failed opening the temp RDB file temp-26.rdb (in server root dir /data) for saving: Permission denied ``` To fix this, configure a **Startup Command** that changes the ownership of the `/data` directory before starting Redis. In your container settings, set the startup command as: ```json theme={null} ["sh", "-c", "chown -R 999:999 /data && exec /usr/local/bin/docker-entrypoint.sh redis-server"] ``` The startup command must be specified as a array, not a string. This ensures proper argument parsing and execution. This command: 1. Changes ownership of `/data` to the `redis` user (uid 999) 2. Executes the standard Redis entrypoint script Redis 7 and earlier versions run as `root` by default and do not have this issue. If you're using Redis 7 (e.g., `redis:7-alpine`), no startup command is needed. Redis has no authentication enabled by default. It's recommended to set a password even if Redis is not exposed externally. Other containers in the same pod can access the network. Set a password by configuring the container command to `redis-server --requirepass YOUR_PASSWORD`, and update your connection string to `redis://:YOUR_PASSWORD@127.0.0.1:6379`. Redis uses `/data` as its default data directory. The official image is configured to use append-only file (AOF) persistence by default, which writes every operation to disk in the mounted volume. ## Connect from your app In a [multi-container](/docs/magic-containers/multi-container) setup, your app and Redis share the same localhost network. Connect using `127.0.0.1` and the default port `6379`. ``` redis://127.0.0.1:6379 ``` ```javascript theme={null} import { createClient } from "redis"; const client = createClient({ url: process.env.REDIS_URL, }); await client.connect(); await client.set("key", "value"); const value = await client.get("key"); ``` ```go theme={null} import ( "context" "os" "github.com/redis/go-redis/v9" ) client := redis.NewClient(&redis.Options{ Addr: "127.0.0.1:6379", }) ctx := context.Background() client.Set(ctx, "key", "value", 0) val, err := client.Get(ctx, "key").Result() ``` ```python theme={null} import redis r = redis.Redis(host="127.0.0.1", port=6379) r.set("key", "value") value = r.get("key") ``` ```php theme={null} connect('127.0.0.1', 6379); $redis->set('key', 'value'); $value = $redis->get('key'); ``` ## Multi-container example A typical setup pairs Redis with your application. When configuring the app, add two containers: ### App container * **Image**: your app image (e.g. `ghcr.io//my-app:latest`) * **Endpoint**: the port your app listens on * **Environment variables**: * `REDIS_URL` = `redis://127.0.0.1:6379` ### Redis container * **Image**: `library/redis:7-alpine` * **Volume**: mount path `/data` Both containers share the same localhost network, so your app connects to Redis at `127.0.0.1:6379`. See [multi-container apps](/docs/magic-containers/multi-container) for more details. ## External access To connect to Redis from outside Magic Containers (e.g. from your local terminal), add an [Anycast endpoint](/docs/magic-containers/endpoints): 1. Go to your app's **Endpoints** tab and click **Add New Endpoint** 2. Select **Anycast** as the type 3. Set **Container Port** to `6379` 4. Set **Exposed Port** to `6379` 5. Click **Add Endpoint** Then connect using the Anycast IP and the **exposed port**: ```bash theme={null} redis-cli -h -p ``` The exposed port and container port may differ. When connecting externally, always use the exposed port shown in your endpoint configuration. Redis has no authentication enabled by default. Exposing it to the internet without a password is a serious security risk. Consider setting a password by using the container command `redis-server --requirepass YOUR_PASSWORD`, and removing the Anycast endpoint when external access is no longer needed. # Ruby on Rails Source: https://bunny.net/docs/magic-containers/guides/ruby-on-rails Deploy a Ruby on Rails application to Magic Containers This guide walks you through building and deploying a Ruby on Rails application to Magic Containers with GitHub Container Registry. You'll need: * A GitHub account for source code and container registry * Ruby 3.2+ installed locally Don't have a bunny.net account yet? [Sign up](https://dash.bunny.net/auth/register) and enable Magic Containers to get started. ## Create the Rails app Create a new Rails application with views and assets: ```bash theme={null} rails new app-rails cd app-rails ``` Generate a controller with a view: ```bash theme={null} bin/rails generate controller Home index --skip-routes ``` Add the route in `config/routes.rb`: ```ruby config/routes.rb theme={null} Rails.application.routes.draw do root "home#index" get "up", to: "rails/health#show", as: :rails_health_check end ``` Update the view in `app/views/home/index.html.erb`: ```erb app/views/home/index.html.erb theme={null}

Hello from Bunny 🐰

```
Create a new Rails API application: ```bash theme={null} rails new app-rails --api cd app-rails ``` Generate a controller with a simple endpoint: ```bash theme={null} bin/rails generate controller Api::Health index --skip-routes ``` Add the route in `config/routes.rb`: ```ruby config/routes.rb theme={null} Rails.application.routes.draw do get "/", to: "api/health#index" get "up", to: "rails/health#show", as: :rails_health_check end ``` Update the controller: ```ruby app/controllers/api/health_controller.rb theme={null} class Api::HealthController < ApplicationController def index render json: { message: "Hello from Bunny 🐰" } end end ```
## Run locally Start the Rails development server: ```bash theme={null} bin/rails server ``` Test the app at [http://localhost:3000](http://localhost:3000), or with curl: ```bash theme={null} curl http://localhost:3000/ ``` ## Prepare for production Rails 7.1+ automatically generates a production-ready `Dockerfile` in your project root. Before building, prepare the app: ```bash theme={null} bin/rails db:migrate rm config/credentials.yml.enc ``` The credentials file is removed since we'll use environment variables instead. If you're using an older Rails version, you can generate a Dockerfile with: `bash bin/rails generate dockerfile ` ## Review the Dockerfile Rails 7.1+ generates a production-ready, multi-stage Dockerfile that includes: * **jemalloc** for reduced memory usage * **Bootsnap** precompilation for faster boot times * **Non-root user** for security * **Asset precompilation** (full-stack apps only) * **Thruster** HTTP/2 proxy (full-stack apps only) ## Build and push to GitHub Container Registry Create `.github/workflows/build.yml` to automatically build and push on every commit to `main`: ```yaml .github/workflows/build.yml theme={null} name: Build and Push on: push: branches: [main] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build-and-push: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v5 with: context: . push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} - name: Update container image on Magic Containers uses: BunnyWay/actions/container-update-image@main with: app_id: ${{ vars.APP_ID }} api_key: ${{ secrets.BUNNYNET_API_KEY }} container: app image_tag: "${{ github.sha }}" ``` Push your code to trigger the workflow: ```bash theme={null} git init git add . git commit -m "Initial commit" git remote add origin https://github.com/YOUR_USERNAME/app-rails.git git push -u origin main ``` Build and push manually from your local machine. Go to [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens) and create a token with `write:packages` scope. ```bash theme={null} export CR_PAT=your_personal_access_token echo $CR_PAT | docker login ghcr.io -u YOUR_USERNAME --password-stdin ``` ```bash theme={null} docker build --platform linux/amd64 -t ghcr.io/YOUR_USERNAME/app-rails:latest . ``` Magic Containers only supports images built for the **linux/amd64** architecture. The `--platform` flag ensures compatibility regardless of your local machine's architecture. ```bash theme={null} docker push ghcr.io/YOUR_USERNAME/app-rails:latest ``` If your package is private, set the visibility to **Public** in GitHub or [configure Magic Containers with registry credentials](/docs/magic-containers/image-registries). ## Deploy to Magic Containers In the bunny.net dashboard, go to **Magic Containers** and click **Add App**. Enter a name and select your deployment option. Click **Add Container**, then configure: | Field | Value | | -------- | ------------------------- | | Registry | GitHub Container Registry | | Image | `YOUR_USERNAME/app-rails` | | Tag | `latest` | Go to the **Environment Variables** tab and add: | Variable | Value | | ----------------- | -------------------------------- | | `SECRET_KEY_BASE` | Generate with `bin/rails secret` | Go to the **Endpoints** tab, click **Add New Endpoint**, and set the container port to `80`. Click **Add Container**, then **Next Step**, and **Confirm and Create**. For more details, see the [quickstart guide](/docs/magic-containers/quickstart). ## Test your app ```bash theme={null} curl https://mc-xxx.bunny.run/ ``` ```json Response theme={null} { "message": "Hello from Bunny 🐰" } ``` You can [add a custom hostname](/docs/magic-containers/endpoints) from the **Endpoints** section in your app settings. ## Add persistent storage Magic Containers are ephemeral, so data stored locally is lost on restarts. For Rails apps that need to persist data (Active Storage uploads, etc.), attach a [Persistent Volume](/docs/magic-containers/persistent-volumes). Go to your app's **Volumes** tab and click **Add Volume**. Set the mount path to `/rails/storage` and choose an initial size. Update `config/environments/production.rb` to use the volume for Active Storage: ```ruby config/environments/production.rb theme={null} config.active_storage.service = :local ``` To persist your SQLite database, update `config/database.yml`: ```yaml config/database.yml theme={null} production: <<: *default database: /rails/storage/production.sqlite3 ``` See the [Persistent Volumes documentation](/docs/magic-containers/persistent-volumes) for more details. ## Next steps 1. [Automate deploys with GitHub Actions](/docs/magic-containers/deploy-with-github-actions) 2. [Add a custom hostname](/docs/magic-containers/endpoints) # Go API Source: https://bunny.net/docs/magic-containers/guides/simple-go-api Deploy a Go API to Magic Containers This guide walks you through building and deploying a simple Go API to Magic Containers using GitHub Container Registry. You'll need: * A GitHub account for source code and container registry Don't have a bunny.net account yet? [Sign up](https://dash.bunny.net/auth/register) and enable Magic Containers to get started. ## Create the Go API Create a new directory with the following files: ```go main.go theme={null} package main import ( "encoding/json" "log" "net/http" "os" ) type Response struct { Message string `json:"message"` } func main() { port := os.Getenv("PORT") if port == "" { port = "80" } http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(Response{Message: "Hello from Bunny 🐰"}) }) log.Printf("Server starting on port %s", port) log.Fatal(http.ListenAndServe(":"+port, nil)) } ``` ```go go.mod theme={null} module app-go-api go 1.23 ``` ## Run locally Start the development server: ```bash theme={null} go run main.go ``` Test the API: ```bash theme={null} curl http://localhost:80/ ``` ## Create the Dockerfile ```dockerfile Dockerfile theme={null} FROM golang:1.23-alpine AS builder WORKDIR /app COPY go.mod ./ COPY main.go ./ RUN go build -o server . FROM alpine:latest WORKDIR /app COPY --from=builder /app/server . ENV PORT=80 EXPOSE 80 CMD ["./server"] ``` ## Build and push to GitHub Container Registry Create `.github/workflows/build.yml` to automatically build and push on every commit to `main`: ```yaml .github/workflows/build.yml theme={null} name: Build and Push on: push: branches: [main] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build-and-push: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v5 with: context: . push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} - name: Update container image on Magic Containers uses: BunnyWay/actions/container-update-image@main with: app_id: ${{ vars.APP_ID }} api_key: ${{ secrets.BUNNYNET_API_KEY }} container: app image_tag: "${{ github.sha }}" ``` Push your code to trigger the workflow: ```bash theme={null} git init git add . git commit -m "Initial commit" git remote add origin https://github.com/YOUR_USERNAME/app-go-api.git git push -u origin main ``` Build and push manually from your local machine. Go to [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens) and create a token with `write:packages` scope. ```bash theme={null} export CR_PAT=your_personal_access_token echo $CR_PAT | docker login ghcr.io -u YOUR_USERNAME --password-stdin ``` ```bash theme={null} docker build --platform linux/amd64 -t ghcr.io/YOUR_USERNAME/app-go-api:latest . ``` Magic Containers only supports images built for the **linux/amd64** architecture. The `--platform` flag ensures compatibility regardless of your local machine's architecture. ```bash theme={null} docker push ghcr.io/YOUR_USERNAME/app-go-api:latest ``` If your package is private, set the visibility to **Public** in GitHub or [configure Magic Containers with registry credentials](/docs/magic-containers/image-registries). ## Deploy to Magic Containers In the bunny.net dashboard, go to **Magic Containers** and click **Add App**. Enter a name and select your deployment option. Click **Add Container**, then configure: | Field | Value | | -------- | ---------------------------------------------------------------------------- | | Registry | GitHub Container Registry | | Image | `YOUR_USERNAME/{imageName}` | | Tag | `latest` for Docker CLI, or the commit SHA from your GitHub Actions workflow | Go to the **Endpoints** tab, click **Add New Endpoint**, and set the container port to `80`. Click **Add Container**, then **Next Step**, and **Confirm and Create**. For more details, see the [quickstart guide](/docs/magic-containers/quickstart). ## Test your API ```bash theme={null} curl https://mc-xxx.bunny.run/ ``` ```json Response theme={null} { "message": "Hello from Bunny 🐰" } ``` You can [add a custom hostname](/docs/magic-containers/endpoints) from the **Endpoints** section in your app settings. ## Connect a database You can connect your app to [Bunny Database](/docs/database) directly from the dashboard: 1. Go to **Database > \[Your Database] > Access** 2. Click **Generate Tokens** 3. Click **Add Secrets to Magic Container App** 4. Select your app The `BUNNY_DATABASE_URL` and `BUNNY_DATABASE_AUTH_TOKEN` environment variables are now available in your app: ```go theme={null} import ( "database/sql" "fmt" "os" _ "github.com/tursodatabase/libsql-client-go/libsql" ) url := fmt.Sprintf("%s?authToken=%s", os.Getenv("BUNNY_DATABASE_URL"), os.Getenv("BUNNY_DATABASE_AUTH_TOKEN"), ) db, err := sql.Open("libsql", url) if err != nil { fmt.Fprintf(os.Stderr, "failed to open db %s: %s", url, err) os.Exit(1) } defer db.Close() ``` See the [Go SDK documentation](/docs/database/connect/go) for more details. ## Next steps 1. [Automate deploys with GitHub Actions](/docs/magic-containers/deploy-with-github-actions) 2. [Add a custom hostname](/docs/magic-containers/endpoints) 3. [Add a persistent volume](/docs/magic-containers/persistent-volumes) # WordPress Source: https://bunny.net/docs/magic-containers/guides/wordpress Deploy WordPress to Magic Containers This guide walks you through deploying WordPress with MariaDB to Magic Containers. You can deploy using a one-click template or configure the containers manually. This is a starter guide intended to show how quickly a real workload can run on Magic Containers. It's aimed at developers comfortable customising Docker images, working with environment variables and volumes, and making changes to a WordPress install at the file level. A production-grade WordPress deployment will need additional hardening, image tuning, backup, and caching decisions beyond what's covered here. Don't have a bunny.net account yet? [Sign up](https://dash.bunny.net/auth/register) and enable Magic Containers to get started. ## Deploy with a template The fastest way to get WordPress running is to use the built-in template, which pre-configures WordPress and MariaDB with persistent volumes. In the bunny.net dashboard, go to **Magic Containers** and open the [**WordPress template**](https://dash.bunny.net/magic-containers/templates/wordpress-0). Give your application a name. The template includes pre-configured environment variables for WordPress and MariaDB. Review and update passwords as needed before deploying. Select the region where you want to deploy your application. WordPress with MariaDB should use single region deployment with a single instance. Each pod gets its own dedicated volume with no data replication — this applies both across regions and within the same region. Click **Deploy** to launch your WordPress site. Once deployed, open the endpoint URL to complete the WordPress installation wizard. ## Deploy manually If you prefer to configure everything yourself, follow these steps. Go to the [bunny.net dashboard](https://dash.bunny.net), select **Magic Containers**, and click **Add App**. Select **Single region deployment**. WordPress with a database should use single region deployment with a single instance. Each pod gets its own dedicated volume with no data replication — scaling to multiple pods or regions would result in separate, isolated databases each with their own data. Click **Add Container** and configure the image: * **Registry**: Docker Hub * **Image**: `library/wordpress` * **Tag**: `apache` Configure the following environment variables: | Variable | Value | | ----------------------- | ----------------- | | `WORDPRESS_DB_HOST` | `127.0.0.1` | | `WORDPRESS_DB_USER` | `wordpress` | | `WORDPRESS_DB_PASSWORD` | a strong password | | `WORDPRESS_DB_NAME` | `wordpress` | In the **Volumes** section of the WordPress container, add a volume: * **Name**: `wp-content` * **Mount path**: `/var/www/html/wp-content` This persists your themes, plugins, and uploads across restarts. Click **Add Container** and configure a second container: * **Registry**: Docker Hub * **Image**: `library/mariadb` * **Tag**: `latest` Configure the following environment variables: | Variable | Value | | ----------------------- | ---------------------------------------- | | `MARIADB_ROOT_PASSWORD` | a strong root password | | `MARIADB_USER` | `wordpress` | | `MARIADB_PASSWORD` | same password as `WORDPRESS_DB_PASSWORD` | | `MARIADB_DATABASE` | `wordpress` | In the **Volumes** section of the MariaDB container, add a volume: * **Name**: `mariadb-data` * **Mount path**: `/var/lib/mysql` This ensures your database files persist across restarts and redeployments. Go to the **Endpoints** tab, click **Add New Endpoint**, and set the container port to `80`. Select the **WordPress** container. Review your settings and click **Confirm and Create**. After deploying, copy the WordPress endpoint URL from the **Endpoints** tab (e.g. `https://mc-xxx.bunny.run`). Go to the WordPress container's **Environment Variables** and add: | Variable | Value | | ------------ | --------------------------- | | `WP_HOME` | your WordPress endpoint URL | | `WP_SITEURL` | same as `WP_HOME` | This prevents WordPress from auto-detecting the wrong URL, which can happen in multi-container setups where multiple endpoints exist. Redeploy after adding these variables. ## Complete the installation If you plan to use a custom domain, add it to your WordPress endpoint and update `WP_HOME` and `WP_SITEURL` **before** running the installation wizard. WordPress stores the site URL during installation, and completing setup on the default `mc-xxx.bunny.run` URL means you'll need to update it later. See [Add a custom hostname](#add-a-custom-hostname) for details. Once your app is running, open the WordPress endpoint URL in your browser. WordPress will display the installation wizard where you can: 1. Select your language 2. Set the site title 3. Create an admin account 4. Complete the installation After setup, your WordPress site is live and accessible at the endpoint URL. ## Environment variables The WordPress container supports these key environment variables: | Variable | Description | | ------------------------ | ----------------------------------------------------------------- | | `WORDPRESS_DB_HOST` | Database host (use `127.0.0.1` for multi-container) | | `WORDPRESS_DB_USER` | Database username | | `WORDPRESS_DB_PASSWORD` | Database password | | `WORDPRESS_DB_NAME` | Database name | | `WORDPRESS_TABLE_PREFIX` | Table prefix (default: `wp_`) | | `WORDPRESS_DEBUG` | Enable debug mode (`1` or `0`) | | `WP_HOME` | Full URL of the site (e.g. `https://mc-xxx.bunny.run`) | | `WP_SITEURL` | Full URL where WordPress is installed (usually same as `WP_HOME`) | Use `127.0.0.1` instead of `localhost` for the database host. Some clients interpret `localhost` as a Unix socket connection, which will fail in a container environment. Using `127.0.0.1` forces a TCP connection. ## Persistent volumes The WordPress template uses two persistent volumes: | Volume | Mount path | Purpose | | -------------- | -------------------------- | ---------------------------- | | `wp-content` | `/var/www/html/wp-content` | Themes, plugins, and uploads | | `mariadb-data` | `/var/lib/mysql` | Database files | Without these volumes, all data is lost when containers restart. See [persistent volumes](/docs/magic-containers/persistent-volumes) for more details. Always set strong passwords for database credentials, even if the database is not exposed externally. Other containers in the same pod can access the network, and a password protects against accidental or unauthorized access. ## Manage files with File Browser Since Magic Containers don't yet provide SSH access, you can temporarily add [File Browser](https://filebrowser.org) as a sidecar container when you need to inspect files, copy something out, or run a manual procedure that can't be done from WordPress admin (for example, editing `wp-config.php`, removing a broken plugin directory, or uploading a file that exceeds the WordPress upload limit). Treat File Browser as a short-lived tool. The `filebrowser/filebrowser` image doesn't accept credentials through environment variables, and on first boot it generates a random password that is printed **once** to the container logs and not stored anywhere you can retrieve later. Use it for the task at hand, then remove the endpoint and container. For anything you do regularly, do it through WordPress admin instead. In your app settings, click **Add Container** and configure: * **Registry**: Docker Hub * **Image**: `filebrowser/filebrowser` * **Tag**: `latest` Since WordPress already uses port `80`, configure File Browser to listen on a different port by setting the following environment variable: | Variable | Value | | --------- | ------ | | `FB_PORT` | `8085` | In the **Volumes** section, mount the same `wp-content` volume used by the WordPress container: * **Name**: `wp-content` * **Mount path**: `/srv` File Browser serves files from `/srv` by default, so this gives it access to your WordPress themes, plugins, and uploads. Go to the **Endpoints** tab and add a new endpoint for the File Browser container on port `8085`. Open your app and go to the **Logs** tab as it deploys. On first boot, File Browser prints a line similar to: ``` User 'admin' initialized with randomly generated password: ``` The password is shown once. If you miss it, redeploy the container to generate a new one. Open the File Browser endpoint URL in your browser and sign in: * **Username**: `admin` * **Password**: the value you copied from the logs Do your file work, then move on to the cleanup step below. Once you've finished, remove the File Browser endpoint and container from your app and redeploy. This closes off the publicly reachable login page and avoids leaving a long-lived tool exposed alongside your site. Anyone with the endpoint URL can reach the File Browser login page. ## Add a custom hostname To use your own domain with WordPress: 1. Go to your app's **Endpoints** tab 2. Add a custom hostname to your CDN endpoint 3. Update the `WP_HOME` and `WP_SITEURL` environment variables to your custom domain (e.g. `https://example.com`) If you installed WordPress using the default `mc-xxx.bunny.run` URL, you must update `WP_HOME` and `WP_SITEURL` to match your custom domain. Without this, WordPress will continue redirecting to the old URL for login, admin pages, and internal links. See [endpoints](/docs/magic-containers/endpoints) for more details on configuring custom hostnames. # Health Checks Source: https://bunny.net/docs/magic-containers/health-checks Configure health checks to ensure your containerized applications are reliable and available. Health checks verify that your application is functioning correctly and ready to handle incoming requests. By checking startup readiness and ongoing liveness, Magic Containers can prevent issues before they impact your users. ## Health check types Magic Containers offers three types of health checks: * **Startup** - Verifies the application has successfully started. No requests are routed until this check passes. * **Readiness** - Ensures the application is ready to handle incoming requests. We strongly recommend enabling this check to avoid failed requests. * **Liveness** - Confirms the application is actively running without problems. ## Configuration Navigate to **Magic Containers**, select your app, click **Container Settings**, then **Edit**. In the Container Settings menu, select the **Monitoring** tab. Click the checkbox for each health check type you want to enable (**Startup**, **Readiness**, **Liveness**). Select either **HTTP GET** or **TCP** from the dropdown: **HTTP GET** - Provide the path and port for the HTTP request. **TCP** - Specify the port for the TCP health check. We strongly recommend enabling at least the Readiness health check to minimize failed requests and ensure a smoother user experience. # Image Registries Source: https://bunny.net/docs/magic-containers/image-registries Connect public or private container registries to Magic Containers. Magic Containers supports both public and private container registries from Docker Hub and GitHub. Magic Containers only supports images built for the **linux/amd64** architecture. When building your container image, ensure you target this platform using `--platform linux/amd64`. ## Public registries Public Docker Hub and GitHub container registries are already connected to Magic Containers. You can deploy any public image without additional configuration. ## Private registries To deploy images from private repositories, connect your registry by providing authentication credentials. In the bunny.net dashboard, go to **Magic Containers** and select **Image Registries**. Click **Add Image Registry**. Fill in the following fields: * **Registry** - Select Docker or GitHub * **Username** - Your username or organization name * **Personal access token** - Your access token with read-only permissions Click **Add Image Registry**. To create a personal access token, see the official [GitHub](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-with-a-personal-access-token-classic) or [Docker](https://docs.docker.com/security/for-developers/access-tokens/#create-an-access-token) documentation. # Magic Containers Source: https://bunny.net/docs/magic-containers/index Deploy and manage containerized applications on a distributed network of bare-metal servers. bunny.net Magic Containers Magic Containers is an edge compute platform for deploying containerized applications globally. The platform automatically identifies user locations and deploys your applications close to them, with workloads scaling dynamically based on demand. ## Key features * **Speed that flies** - Deploy on ultra-fast CPUs and NVMe storage across 40+ regions. Milliseconds matter. * **Security, locked down** - No shared memory. No noisy neighbors. Your app runs fully isolated and fully virtualized in its own dedicated space. * **Scalability on autopilot** - Traffic spikes? No problem. AI scales your app instantly—within regions and across the globe. No limits. * **Simplicity, borderline ridiculous** - Just select your image, hit deploy, and let the magic happen. * **Everything built in** - CDN, anycast, load balancing, auto-scaling, seamlessly integrated. # IP Addresses Source: https://bunny.net/docs/magic-containers/ip-addresses You can use the following IP addresses to whitelist Bunny.net traffic in your firewall or ACLs. The API provides two endpoints for retrieving Magic Containers node IP addresses: `https://api.bunny.net/mc/nodes` returns a JSON array, and `https://api.bunny.net/mc/nodes/plain` returns plain text with one IP per line. The list returned from the API is dynamic, make sure to request it periodically to avoid missing newly added IPs. ```bash application/json theme={null} curl -H "Accept: application/json" https://api.bunny.net/mc/nodes ``` ```bash text/plain theme={null} curl https://api.bunny.net/mc/nodes/plain ``` ```json application/json theme={null} { "items": [ "104.166.147.46", "109.61.83.105", "109.61.83.248" ], "meta": { "totalItems": 3 }, "cursor": "" } ``` ```bash text/plain theme={null} 104.166.147.46 109.61.83.105 109.61.83.248 ``` # Limits Source: https://bunny.net/docs/magic-containers/limits To make sure everyone’s applications run reliably and smoothly, we have a few resource and usage limits in place. Below, you’ll find details on these limits, covering CPU, memory, storage, and more so that you can plan your deployments with confidence. # Dynamic runtime limits The following resource limits apply to each instance (pod) of a Magic Containers application running on the dynamic runtime: * **CPU**: 8 CPUs * **Memory**: 32 GiB * **Network ingress**: 1 Gbps * **Network egress**: 1 Gbps * **Outbound connections**: 500 * **Ephemeral storage**: 10 GB On [trial accounts](#trial-accounts), each instance runs with 1 CPU and 4 GiB of memory instead. The remaining runtime limits above are the same for trial and standard accounts. The default bandwidth limit is 1 Gbps. If your application requires higher bandwidth, you can request an increase by submitting a support ticket. # Persistent volume limits The following I/O limits are defined in the pod cgroup and apply to each [persistent volume](/docs/magic-containers/persistent-volumes) individually. If a pod has multiple volumes attached, each volume receives these limits: * **Read throughput**: 10 MB/s * **Write throughput**: 10 MB/s Once an application consumes 10 GB of ephemeral storage, the container/pod will be evicted and automatically restarted. Any data stored in ephemeral storage is lost during this process. Magic Containers automatically attempts to restart a container if it detects a failure. ## CPU core detection This section assumes a standard account, where each instance gets 8 CPUs. On a [trial account](#trial-accounts) each instance gets 1 CPU, so use 1 wherever the examples below use 8. While Magic Containers allocates 8 CPUs worth of CPU time to each pod, applications running inside the container may still detect the total number of CPU cores available on the host machine (which can be 32 or more cores). This can lead to suboptimal performance because many applications automatically spawn threads based on the detected CPU count, resulting in excessive thread contention when only 8 CPUs worth of processing time is actually available. ### Symptoms of incorrect CPU detection * Application performance is significantly slower than expected * CPU utilization appears low despite the application being busy * Multi-threaded workloads (encoding, compilation, etc.) perform worse than on less powerful machines ### Optimizing multi-threaded applications To achieve optimal performance, configure your applications to use 8 threads or fewer. Below are examples for common use cases. #### FFmpeg video encoding FFmpeg and its video encoders may auto-detect the host's CPU count and spawn too many threads. Here's how to configure different encoders for optimal performance on Magic Containers: **x265 (HEVC) encoding** Create a wrapper script `/usr/local/bin/ffmpeg-x265-optimized`: ```bash theme={null} #!/bin/sh X265_THREADS="${X265_THREADS:-pools=8:frame-threads=3}" exec ffmpeg "$@" -x265-params "${X265_THREADS}:log-level=error" ``` Or set the environment variable in your Dockerfile: ```dockerfile theme={null} ENV X265_THREADS="pools=8:frame-threads=3" ``` Then use it in your ffmpeg command: ```bash theme={null} ffmpeg -i input.mp4 -c:v libx265 -x265-params "pools=8:frame-threads=3" output.mp4 ``` **x264 (H.264) encoding** ```bash theme={null} ffmpeg -i input.mp4 -c:v libx264 -threads 8 output.mp4 ``` **VP9 and AV1 encoding** ```bash theme={null} # VP9 ffmpeg -i input.mp4 -c:v libvpx-vp9 -threads 8 output.webm # AV1 (libaom) ffmpeg -i input.mp4 -c:v libaom-av1 -threads 8 output.mp4 ``` #### General thread configuration For other applications, look for thread or worker count configuration options and set them to 8 or fewer. Common environment variables and settings include: * `OMP_NUM_THREADS=8` - OpenMP applications * `GOMAXPROCS=8` - Go applications * `UV_THREADPOOL_SIZE=8` - Node.js applications * `--workers=8` or `-j8` - Various CLI tools When in doubt, explicitly set thread counts to 8 in your application configuration rather than relying on auto-detection. This ensures consistent and optimal performance on Magic Containers. The system will attempt to restart the container up to 10 times before giving up. # Port Limits Our platform applies default restrictions to inbound and outbound traffic on the following ports: **25**, **465**, **587**, and **2525**. This is to maintain a secure environment and prevent unauthorized mail relay. If your application requires any of these ports to be opened, contact our support team for assistance. They will review and enable the necessary configurations to ensure your application functions correctly. # Account-level limits ## Standard accounts By default, the following limits apply to each standard (non-trial) account on the Magic Containers platform: * Number of applications per account: 20. * Number of regions per application: No limit (all available regions). * Number of instances/pods per region per application: Up to 10. * Number of persistent volumes per application: 2. * Maximum persistent volume size: 100 GB. ## Trial accounts If you are on a trial account, the following limits apply instead: * Number of applications per account: 1. * Number of regions per application: 3. * Number of instances/pods per region per application: 3. * Number of instances/pods per application: 3 across all regions. * Number of persistent volumes per application: 1. * Maximum persistent volume size: 30 GB. * CPU per instance: 1 CPU. * Memory per instance: 4 GiB. Autoscaling is capped accordingly: the maximum number of instances per region cannot be set above 3, and the minimum number of instances multiplied by the number of [base regions](/docs/magic-containers/deploy) cannot exceed 3. For example, an application with three base regions can keep one instance running in each of them. A verified payment card is required before a trial account can deploy or manage applications. Verifying your card does not raise the limits above. Trial limits are lifted when your account moves to a paid plan. Your existing applications are then updated automatically to the standard CPU and memory limits. # Log Forwarding Source: https://bunny.net/docs/magic-containers/log-forwarding Forward container logs in real-time to your Syslog endpoint for monitoring and debugging. # What is Log forwarding? Log forwarding enables your **Magic Container's** application to send raw logs in real-time to the configured Syslog destination. There might be up to a 10-30 second delay between an actual request and the log hitting your Syslog endpoint. # What Log format and protocol does it support? **Log format** The logs are sent in the standard **Syslog RFC 5424 & Syslog RFC 3164 protocol**. An example **RFC 5424 log** would look like this: ```bash bash theme={null} <134>1 2025-07-10T02:03:30.998108+00:00 - - - - - {"log":"Pod deployment finished","time":"2025-07-10T02:03:25.840126Z"} ``` **Log server protocol** The logs can be sent in either **UDP** or **TCP** transport layer protocol. ### UDP reliability The UDP protocol is unaware of lost packets and does not contain an auto-retry mechanism. This means that in a case of packet loss or connectivity issue between your application and your logging service, some packets might get lost on the way. While this is likely a rare occurrence, it is important to keep in mind in case your logging relies on receiving 100% of the requests. ### UDP security The [UDP protocol](https://bunny.net/academy/network/what-is-user-datagram-protocol-udp-and-how-does-it-work) does not provide a layer of security. This means all the logs are sent to your endpoint in a raw, unencrypted form. While unlikely, please note that those might be vulnerable to a potential **man-in-the-middle attack** if you send critical information as part of your logs. # How to configure Log forwarding? To enable realtime **Log forwarding** on your Magic Container application, you can follow the following steps: 1. Visit your **Logging** page for your Magic Containers application in the left-side menu. 2. Open the **Settings** panel inside of the Logging option. 3. Make sure that the **Enable Log forwarding** feature is enabled. 4. Enter the **Hostname** of your Syslog endpoint. *This can be either an IP or a host address where your listening server is enabled.* 5. Enter the **Port** of your Syslog endpoint. *Make sure the port is open to the internet, as otherwise our servers will not be able to reach you.* 6. *(Optional)* Configure the **Token**. *For secure log forwarding* 7. Select the **Log server protocol** you would like to use. 8. Select the **Log format** of choice. 9. Click on the **Save Forwarding Configuration** button. 10. Run your **Magic Containers application** and monitor your endpoint for new logs. # Logs Source: https://bunny.net/docs/magic-containers/logs Logs are a critical component of Magic Containers and are accessible through the dedicated Logs tab on the Dashboard. This single interface provides both platform-level (system) logs and application-level (app) logs. System logs reveal the internal events and decisions made by the Magic Containers infrastructure, such as successful or failed container starts, networking issues, or health check responses. App logs consist of the standard output and error streams from the containers themselves, showing any messages your software generates. Because logs are live-streamed, you can only view them in real time. The platform currently does not retain historical logs, so troubleshooting is most effective when you keep the log view open while reproducing the problem in your application. To quickly pinpoint issues, you can narrow your scope by focusing on a specific region, pod, or container within the pod. You can also switch between system logs and app logs to determine whether a malfunction originates in the platform’s orchestration layer or your application code. # Filtering logs Filtering logs by application or region is also helpful if you manage many deployments across different environments. Narrowing down the view to a single location can highlight specific regional issues, like network latency or hardware constraints. If the entire application experiences recurring difficulties, switching to a broader application-level view will reveal whether the problem occurs in multiple pods and regions simultaneously. # Monitoring Source: https://bunny.net/docs/magic-containers/monitoring Understanding how your application behaves in real time is essential for making informed decisions, identifying performance bottlenecks, and delivering a seamless user experience. Magic Containers provides robust monitoring features, allowing you to gain deep insights into various aspects of your application's performance. # What you'll need Before you dive in, make sure you have the following prerequisites in place: * A [bunny.net](https://bunny.net/) account ([Log in](https://dash.bunny.net/auth/login?pk_buttonlocation=menu) or sign up for a [free trial](https://dash.bunny.net/auth/register)). * Ensure that you have **already deployed the application** you want to monitor. # Accessing app monitoring To access the App Monitoring feature, follow the steps below: 1. Login to [bunny.net dashboard](https://dash.bunny.net/auth/login?pk_buttonlocation=menu). 2. Navigate to the **Magic Containers** section and select the app you want to monitor. 3. Once you've selected your app, click the **Statistics** tab to access detailed insights into its performance. # Available metrics The Statistics tab provides a range of metrics that give you a comprehensive view of your application's behavior. Here are the key metrics available: * **Latency**: Latency measures the time it takes for a request to travel from the source to the destination and receive a response. In the context of Magic Containers, it indicates the responsiveness of your application. * **Instances and active regions**: This metric provides insights into the number of instances running your application and the active regions where these instances are distributed. * **Traffic served**: This measures the amount of data served by your application, providing insights into its overall usage. * **CPU usage**: The central processing unit (CPU) monitors your application's usage, helping you understand the computational load. * **Memory usage**: Tracks the memory consumption of your application, highlighting its memory usage patterns. # Configuring display options Effectively utilizing Magic Containers' monitoring tools involves tailoring the display to meet your specific analysis needs. Customize the display options to focus on the metrics and timeframes that matter most to you. ## Time Period You can toggle between daily and hourly statistics to analyze trends over different timeframes. This flexibility helps you identify patterns and trends in your application's behavior. ## Date Selection Select a specific date to focus on particular periods, facilitating a detailed analysis of your application's historical performance. # Multi-Container Apps Source: https://bunny.net/docs/magic-containers/multi-container When using bunny.net Magic Containers, you can add multiple containers to the same application. These containers will all run inside a single pod sandbox, meaning they share the same network namespace and can communicate with each other via localhost. This document describes how to configure and optimize your setup when using multiple containers in the same pod. # Key concepts All containers run within a single pod. Because they share the same network namespace, they can communicate directly via `localhost`, which improves performance and simplifies networking. Containers must listen on distinct ports to avoid conflicts, as any collision will prevent the pod from starting. Sharing a pod also means containers share resources and have the same lifecycle, allowing you to manage them together and scale them in unison. The containers you add to the application all run in the same pod sandbox. Containers that are part of the same pod share the same network namespace. Containers can communicate via `localhost`. For example, if you have an application with two containers: one running an API and another running Redis, the API container can access the Redis container through `localhost:`. Multiple containers cannot use the same port. Since the network namespace is shared, different containers cannot use the same port as they will collide and the pod will not start. [Persistent volumes](/docs/magic-containers/persistent-volumes) can be shared between containers in the same pod. This allows containers to read and write to the same storage, which is useful for sharing files, caches, or other data across services. # Persistent Volumes Source: https://bunny.net/docs/magic-containers/persistent-volumes Attach persistent storage to your Magic Containers applications. Persistent Volumes provide durable storage that persists across restarts and redeployments. Attach a volume directly to your Magic Containers application to keep your data safe, secure, and accessible as your application scales globally. * **Auto-scaling** - Volumes automatically scale up with regional pod deployments and detach when scaling down * **Encrypted by default** - All persistent data is AES encrypted * **Flexible storage** - Each volume can expand up to 100GB (standard accounts) or 30GB (trial accounts), but can only be extended, never reduced * **Dedicated volumes** - Each pod generates its own unique blank volume with no data duplication * **Shared access** - Containers within the same pod can share the same volume * **Automatic reattachment** - Detached volumes reattach automatically if your app scales up regionally * **Unique mount paths** - Mount paths are unique to the application ## Regional behavior Volumes are tied to specific regions. This means: * If your app runs in multiple regions (e.g., EU, US, APAC), each region gets its own independent volumes * A pod in one region cannot access volumes from another region * When scaling to a new region, new volumes are automatically provisioned in that region * Each pod within the same region gets its own dedicated blank volume (no data duplication between pods) ## Scaling behavior When your application scales up, new volume instances are created and attached to pods on available nodes. When it scales down, those volume instances are detached but not deleted automatically. Detached volumes remain in your account and continue to reserve storage capacity. It is your responsibility to delete them from the **Volumes** tab if they are no longer needed. When your application scales back up, the platform will attempt to reuse existing detached volume instances. However, since workloads are scheduled dynamically, pods may be placed on nodes that do not host your previous volume instances, which means reattachment cannot be guaranteed and new empty volumes may be provisioned instead. Keep this behavior in mind when scaling applications with persistent volumes. Scaling down leaves detached volumes that persist until deleted, and a subsequent scale-up may not restore access to the same data if your pods are placed on different nodes. ## Node unavailability Persistent volumes are bound to specific nodes within a region. If a node becomes temporarily unavailable (for example, due to maintenance or infrastructure changes), the platform preserves your volume and pod association so data remains intact when the node returns: * **Volume retention** - The persistent volume stays safely on its node, preserving all data during the outage * **Pod association** - The pod remains associated with its volume, ensuring it resumes exactly where it left off once the node recovers * **Automatic resumption** - When the node returns to service, your pod restarts automatically with the existing volume and all data intact ### Recovery options If you need your pod running sooner, you have two paths forward: The recommended option when your data matters. Once the node is back, your pod resumes automatically with the existing volume and all data intact — no action required. If the data is not needed, delete the detached volume from the **Volumes** tab. The pod will be scheduled to a healthy node in the same region and start fresh with a new empty volume. ### Hardware failure In the rare event that a node requires a disk replacement, the affected volume is recreated as a new empty volume once the node is restored. Persistent volumes do not currently support automatic backups or replication. For critical data, implement a backup strategy within your application (for example, periodic exports to object storage) so you can restore quickly if needed. ## Quickstart During container setup, add your volume details including the mount path and initial size. Select **Update Container** and save your settings. Your application now has scalable persistent volumes attached to each container. Manage your volumes from the dedicated **Volumes** tab. ## Limits | Feature | Standard account | Trial account | | ----------------------- | ---------------- | ------------- | | Volumes per application | 2 | 1 | | Maximum volume size | 100GB | 30GB | | Read throughput | 10 MB/s | 10 MB/s | | Write throughput | 10 MB/s | 10 MB/s | Throughput limits are defined in the pod cgroup and apply to each volume individually. If a pod has two volumes attached, each volume receives its own 10 MB/s read and 10 MB/s write limits. ## File permissions When multiple containers in a pod share a volume, their ability to read, modify, or delete each other's files depends on which user each container runs as. **Which user does a container run as?** By default, if no `USER` instruction is present in your Dockerfile, the container runs as root (`uid=0`). If a `USER` instruction is specified, the container runs as that user. ```dockerfile theme={null} # Runs as root (no USER specified) FROM ubuntu:22.04 CMD ["myapp"] # Runs as a non-root user FROM ubuntu:22.04 USER 1000 CMD ["myapp"] ``` Volumes are only shared between containers within the same pod. Containers from different pods cannot access each other's volumes. ### Root creates files, non-root accesses them | Operation | Result | | --------- | ------ | | Read | ✅ | | Modify | ❌ | | Delete | ❌ | Files created by a root process can be read but not modified or deleted by non-root containers. ### Non-root creates files, root accesses them | Operation | Result | | --------- | ------ | | Read | ✅ | | Modify | ✅ | | Delete | ✅ | Root always has full access regardless of file permissions. ### Both containers run as the same non-root user | Operation | Result | | --------- | ------ | | Read | ✅ | | Modify | ✅ | | Delete | ✅ | Full access as both containers are the file owner. ### Each container runs as a different non-root user | Operation | Result | | --------- | ------ | | Read | ✅ | | Modify | ❌ | | Delete | ❌ | The accessing container is not the file owner. It can read files but cannot modify or delete them. ### Summary | Scenario | Read | Modify | Delete | | ------------------------------- | ---- | ------ | ------ | | Root creates, non-root accesses | ✅ | ❌ | ❌ | | Non-root creates, root accesses | ✅ | ✅ | ✅ | | Same non-root user | ✅ | ✅ | ✅ | | Different non-root users | ✅ | ❌ | ❌ | ### A note on deletion On Linux, whether a process can delete a file is determined by write permission on the **directory** containing the file, not by the file's own permissions. Because all pod containers share write access to the volume directory, a non-root container would ordinarily be able to delete any file inside it regardless of who created it. To prevent this, the volume directory is configured with the **sticky bit** — the same mechanism used by shared system directories such as `/tmp`. With the sticky bit set, only the file's owner or root can delete or rename a file, even with directory write access. The `Delete ❌` entries in the tables above rely on this. ## FAQ Yes, you are charged while the volume exists even if it's detached, as it still reserves storage capacity. The default behavior means the volume will detach when scaling down. No, you cannot reduce the size of your volume. You can only extend it to make it larger. Persistent volumes are bound to specific nodes. If a node becomes unavailable (due to maintenance or other issues), your pod cannot be rescheduled to another node because it remains bound to the volume on the unavailable node. You can either wait for the node to recover, or delete the volume to allow the pod to reschedule with a new volume. No, persistent volumes do not currently support automatic backups or replication. If the underlying hardware fails and the disk is replaced, all data on that volume will be lost. For critical data, you should implement your own backup strategy within your application. ## Pricing Pricing is \$0.10 per GB/month, charged based on the allocated (provisioned) volume size. # Pricing Source: https://bunny.net/docs/magic-containers/pricing Magic Containers uses a pay-as-you-go pricing model, charging you based on your actual consumption of CPU, RAM, storage, and bandwidth. Magic Containers is ideal for any type of workload, from small experiments to large-scale production applications. ## Cost components and rates There are two types of cost components: based only on amount or on amount and time. * Cost components based on time and amount * CPU * RAM * Anycast IP * Persistent volumes * Cost components based on amount only * Egress traffic ### CPU CPU is billed per second of CPU time consumed by your containers. If your container uses an entire CPU core for one second, you pay for one second of CPU time. If it uses half a core for one second, that counts as 0.5 seconds of CPU time. If two cores are used for one second, that counts as 2 seconds of CPU time. **Calculation example:** * CPU cost: **\$0.02 per CPU per hour** * Cost per second of CPU time: \$0.02 / 60 / 60 = **\$0.000005555555556 per second** **Example scenario:** If your application uses 3 seconds of full CPU time per minute; every minute for the period of entire month: * 1 minute = 60 seconds * CPU usage per hour = 180 seconds ( 3 second per minute \* 60 minutes) * Hourly CPU cost = 180 x \$0.000005555555556 ≈ **\$0.001** * Daily CPU cost = 24 x \$0.001≈ **\$0.024** * Over an entire month (31 days), your CPU cost would be roughly **\$0.744**. ### RAM usage Memory (RAM) is billed in 64 MB increments per hour. Even if your application uses only 10 MB, you’ll pay for one 64 MB block. Usage is calculated hourly and aggregated at the end of the billing cycle. > For actual workload memory, please note that the total reported memory usage charged will include an additional system overhead that is variable with the workload. **Calculation example:** * RAM Cost: \$0.005 per GB per hour (1 GB \~ 16 blocks by 64MB) * Cost per 64 MB block per hour: \$0.005 / 16 = **\$0.0003125** **Example scenario:** If your application uses 512 MB of RAM usage (8 blocks of 64 MB) per hour; every hour for the period of entire month: * Price per 512 MB per hour: \$0.00025 x 8 = **\$0.002** * Price per day: \$0.002 x 24 = **\$0.048** * Price per month (31d): \$0.048 x 31 = **\$1.488** ### Anycast IP usage The cost for Anycast IP (IPv4) is \$2 per month. An Anycast IP is billed for as long as it is attached to your application. Because an attached IP is reserved for you, it is considered in use and is charged even when the application is stopped, deactivated, or not serving any traffic. To stop being charged for an Anycast IP, detach (remove) it from your application. If the Anycast IP is attached to your application only for some period within the month, we will charge only for that period. **Calculation example:** * Anycast IP Cost: **\$2 per month** * Anycast IP Cost per minute (if 31d month): 2 / 31 / 24 / 60 = **\$0.000044802867384** **Practical scenario:** If Anycast IP is attached to your application for 10 days, 7 hours, and 15 minutes in the last month you will be charged for: * Anycast IP Cost per hour: \$0.000044802867384 x 60 = **\$0.002688172043011** * Anycast IP Cost per day: \$0.002688172043011 x 24 = **\$0.064516129032258** * Total Anycast cost: 10 \* \$0.064516129032258 + 7 x \$0.002688172043011 + 15 x \$0.000044802867384 = **\$0.664650537634417** ### Persistent volumes [Persistent volumes](/docs/magic-containers/persistent-volumes) are billed based on the **allocated (provisioned)** volume size, not the actual used space. **Calculation example:** * Storage cost: **\$0.10 per GB per month** * Daily cost per 1 GB (31 days): \$0.10 / 31 = **\$0.003225806451613 per GB per day** **Practical scenario:** If you allocate a 20 GB volume for 15 days: * Cost = 20 x 15 x \$0.003225806451613 = **\$0.97** ### Network traffic usage Network traffic is charged based on the total data transferred each month. The prices vary by region. The MC costs include the following traffic: * All egress traffic, except for traffic going through the CDN (CDN traffic is charged as part of the CDN service). **Region prices:** * Europe & North America: \$0.01/GB * South America: \$0.045/GB * Asia & Oceania: \$0.03/GB * Middle East & Africa: \$0.06/GB **Practical Scenario:** If your application served 15 GB of traffic from Europe, 10 GB of traffic from the Middle East, and 3 GB from South America, the costs would be calculated as follows: * Europe traffic cost: \$0.01 x 15 = **\$0.15** * Middle East traffic cost: \$0.06 x 10 = **\$0.6** * South America traffic cost: \$0.045 x 3 = **\$0.135** * Total: **\$0.885** # Quick Deploy Source: https://bunny.net/docs/magic-containers/quick-deploy Deploy containerized applications globally in seconds with a streamlined, single-form flow. **Quick Deploy** is a streamlined alternative to **Advanced Deploy**. Instead of stepping through multiple configuration screens, everything is presented in a single form, from picking your image to setting environment variables, so you can go from zero to deployed in seconds. From the **Quick Deploy** page, use the search field to find a container image. The search pulls from all registries connected to your dashboard, including DockerHub Public, GitHub Public, and any private registries you've added. Select an image from quick deploy Once you select an image, its available tags are displayed. If a `latest` tag exists, it is selected automatically. You can switch to any other tag before deploying. Quick deploy form Under **App**, decide whether to deploy into a new app or add this container to an existing one: * **New**: enter a name for your new app. The image name is pre-filled as a sensible default. * **Existing**: select an app you've already created to add this container alongside its current containers. Endpoints define how your container is reachable from the internet. Two endpoint types are available: * **CDN**: routes traffic through bunny.net's CDN edge network. Best for HTTP/HTTPS services that benefit from caching and global distribution. * **Anycast IP**: exposes your container directly via a globally anycast IP. Better suited for non-HTTP protocols or latency-sensitive TCP/UDP services. When an image exposes known ports, Quick Deploy will attempt to auto-populate an endpoint for you. Review these carefully before deploying. Endpoints make your container publicly accessible. Only create endpoints for services that are meant to be public-facing, such as web servers or APIs. **Do not expose private services** (databases, caches, internal APIs, etc.) through an endpoint. Click **+ CDN** or **+ Anycast IP** to add additional endpoints if your app needs to expose multiple ports. If your container needs to write data that should survive restarts or redeployments, add a persistent volume under **Persistent Volumes**. Click **+ Add** and specify the mount path inside the container. Leave this section empty if your container is stateless. Under **Environment Variables**, configure any runtime settings your container needs. Quick Deploy inspects the image metadata and pre-fills known environment variables with common default values. Review each one before deploying. Pay special attention to: * **Passwords and secrets**: replace any placeholder values with strong, unique secrets. Never leave default or example passwords in production. * **Paths and ports**: confirm that pre-filled values match your actual container configuration. Click **+ Add** to include any additional variables not detected automatically. Under **Region**, Quick Deploy automatically selects the region geographically closest to you. You can change this to any available region using the dropdown if you need to deploy closer to your users or a specific data source. Once everything looks good, click **Deploy**. Quick Deploy hands off to the same provisioning pipeline as Advanced Deploy. Your app will be created (or updated), the container scheduled, and endpoints activated. You'll be taken to the app overview where you can monitor the deployment status. App overview page # Quickstart Source: https://bunny.net/docs/magic-containers/quickstart Learn how to deploy your first application with Magic Containers. Magic Containers lets you deploy any Docker container. Bring your own image, pick a pre-built template, or connect a GitHub Action for continuous deployment. To create a new app, select **Add > App** from the sidebar, or go to **Magic Containers > Deploy**. From here, choose how you want to deploy: If you are using a trial account: * You will need to add valid payment card details before you can deploy an application. * Trial accounts are limited to one application, three regions, and up to three instances per region. See [Limits](/docs/magic-containers/limits#trial-accounts) for the full list. # Regions Source: https://bunny.net/docs/magic-containers/regions Deploy your applications globally across Magic Containers' distributed network of servers. Magic Containers provides servers across multiple continents, allowing you to deploy applications close to your users for optimal performance and reduced latency. ## Available regions * Ashburn - Atlanta - Boston - Chicago - Dallas - Denver - Los Angeles - Miami - New York City - San Jose - Seattle - Toronto * Amsterdam - Athens - Bucharest - Copenhagen - Frankfurt - London - Madrid * Milan - Paris - Prague - Stockholm - Vienna - Warsaw - Zagreb * Bangkok - Hong Kong - Istanbul - Jakarta - Kuala Lumpur - Manila - Singapore - Tel Aviv - Tokyo * Bogota - Mexico City - Sao Paulo - Sydney - Johannesburg - Lagos ## Autoprovisioning Autoprovisioning uses reinforcement learning to automatically distribute your app across regions based on traffic patterns. The provisioner periodically analyzes your app's traffic and adjusts deployments to ensure optimal performance. The autoprovisioning process: 1. **Traffic analysis** - The provisioner analyzes your app's global traffic distribution 2. **Optimization decision** - Based on traffic patterns, it decides whether to deploy or undeploy instances in specific regions 3. **Deployment** - New instances are deployed in regions with increased demand, or removed from regions with lower demand 4. **Continuous adaptation** - This process runs periodically to adapt to changing traffic patterns # Rolling Updates Source: https://bunny.net/docs/magic-containers/rolling-updates The Rolling Update process on Magic Containers ensures that updates to your application's container settings are applied smoothly and without any downtime. By updating pods incrementally, your application remains available to users throughout the update. # How does the rolling update work? A Rolling Update is initiated when changes are made to the Container Settings of your application, such as: * Updating the container image * Changing the container name * Modifying health check settings Changes to endpoints do not trigger a Rolling Update. Endpoint changes are applied immediately without interrupting the running containers. The Rolling Update process occurs simultaneously in all regions where the application is deployed. During a Rolling Update, other changes to the application are paused. This includes scaling operations, whether upscaling or downscaling, provisioning, and any additional settings updates. These changes will only be applied after the Rolling Update completes. # Practical example Imagine you have an application deployed in 3 regions, each with 10 pods. If you update the container image: 1. Iteration 1: * Add 2 new pods (20% of 10) in each region. * Wait until new pods are running and healthy. * Remove 2 old pods in each region. 2. Iterations 2–5: * Repeat the above steps until all pods are updated. **Total Iterations** - 5 iterations to update all pods in each region. # Applications with persistent volumes When your application uses [persistent volumes](/docs/magic-containers/persistent-volumes), the rolling update process is adjusted to prevent data corruption. Rather than starting new pods before removing old ones, existing pods are shut down first to fully unmount their volumes before new pods are started. Applications with persistent volumes may experience a short period of downtime during a rolling update if the number of pods is less than 2, as the old pod must release the volume before the new pod can attach it. ## Practical example Imagine you have an application deployed in 3 regions, each with 10 pods, and each pod has a persistent volume attached. If you update the container image: 1. Iteration 1: * Remove 2 old pods (20% of 10) in each region and wait for their volumes to fully unmount. * Start 2 new pods in each region and wait until they are running and healthy. 2. Iterations 2–5: * Repeat the above steps until all pods are updated. **Total Iterations** - 5 iterations to update all pods in each region. # Handling issues during a rolling update If the Rolling Update encounters problems, such as deploying a broken image, it handles the situation by detecting any new pod that fails to reach a running state within ten minutes and considering it unhealthy. In such cases, the Rolling Update is terminated. All new pods with the faulty configuration are deleted, and old pods are restored to maintain application stability. The Rolling Update then restarts automatically, using the latest configuration. If the issue is fixed, such as by providing a healthy image, the update will proceed successfully. # Sandboxing Source: https://bunny.net/docs/magic-containers/sandbox Learn how Magic Containers uses gVisor for application kernel isolation and enhanced container security. Traditional containers use Linux namespaces to establish resource limits, but a malicious deployment could potentially breach container boundaries. Magic Containers addresses this by using [gVisor](https://gvisor.dev/) as the container runtime. gVisor intercepts application system calls and handles them in a user-space kernel, creating strong isolation between the application and the host kernel without the overhead of full virtualization. ## Syscall Compatibility gVisor implements a subset of Linux system calls. While most common applications work without modification, some workloads that rely on specialized or less common syscalls may experience compatibility issues. Applications using standard networking, file I/O, and process management typically work without issues. For a complete list of supported syscalls, see the [gVisor compatibility documentation](https://gvisor.dev/docs/user_guide/compatibility/linux/amd64/). ## Network Isolation Magic Containers uses Virtual Extensible LAN (VXLAN) to create virtual networks over the physical infrastructure. This provides scalable connectivity between containers across different hosts while maintaining isolation and segmentation. # Templates Source: https://bunny.net/docs/magic-containers/templates Deploy pre-built application templates with Magic Containers. Templates provide pre-built applications you can deploy with just a few clicks. Each template includes a ready-to-run container image, and some include a sidecar database. ## Deploy a template In the bunny.net dashboard, go to **Magic Containers** and select [**Templates**](https://dash.bunny.net/magic-containers/templates). The following templates are available: **Frameworks** | Template | Description | | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | | [Django with Redis](https://dash.bunny.net/magic-containers/templates/django-with-redis-0) | A minimal Django key-value API backed by Redis | | [FastAPI KV with Redis](https://dash.bunny.net/magic-containers/templates/fastapi-with-redis-0) | A Python FastAPI backend paired with Redis for high-performance APIs with caching and data storage | | [Flask](https://dash.bunny.net/magic-containers/templates/flask-0) | A Python Flask template for building lightweight, flexible web applications and REST APIs | | [Flask with Redis](https://dash.bunny.net/magic-containers/templates/flask-with-redis-0) | A Python Flask backend paired with Redis for simple key-value storage and lightweight APIs | | [Go API](https://dash.bunny.net/magic-containers/templates/go-api-0) | A minimal Go HTTP API template for building high-performance, compiled backend services | | [Go API with Redis](https://dash.bunny.net/magic-containers/templates/go-api-with-redis-0) | A Go API backend paired with Redis for fast caching, session management, and data storage | | [Hono API](https://dash.bunny.net/magic-containers/templates/hono-api-0) | A lightweight Hono API template for building fast, minimal HTTP APIs with modern JavaScript | | [Hono API with DuckDB](https://dash.bunny.net/magic-containers/templates/hono-api-with-duckdb-0) | A Hono API backend with embedded DuckDB for analytical queries and lightweight data processing | | [Laravel with MariaDB](https://dash.bunny.net/magic-containers/templates/laravel-with-mariadb-0) | A full-stack PHP Laravel application with a MariaDB database, ready for web apps and APIs | | [Next.js](https://dash.bunny.net/magic-containers/templates/nextjs-0) | A Next.js template for building modern React applications with server-side rendering and static generation | | [Next.js with PostgreSQL](https://dash.bunny.net/magic-containers/templates/nextjs-with-psql-0) | A full-stack Next.js application connected to PostgreSQL for data-driven web apps | | [Rails App](https://dash.bunny.net/magic-containers/templates/rails-0) | A Ruby on Rails template for rapidly building full-featured web applications and APIs | | [Vite + React (Nginx)](https://dash.bunny.net/magic-containers/templates/vite-react-nginx-0) | A Vite-powered React frontend served through Nginx for fast, production-ready single-page applications | **Apps** | Template | Description | | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | [Minecraft](https://dash.bunny.net/magic-containers/templates/minecraft-0) | A ready-to-run Minecraft server for quickly hosting and managing your own multiplayer world | | [n8n](https://dash.bunny.net/magic-containers/templates/n8n-with-psql-0) | n8n workflow automation platform with PostgreSQL | | [Umami Analytics](https://dash.bunny.net/magic-containers/templates/umami-psql-0) | A self-hosted Umami analytics setup for simple, privacy-focused website visitor insights | | [WordPress](https://dash.bunny.net/magic-containers/templates/wordpress-0) | A self-hosted WordPress CMS with MariaDB | Click on a template to view its details and begin deployment. Give your application a name. Some templates include configurable environment variables, such as database usernames and passwords. Update these values as needed before deploying. Select the region where you want to deploy your application. Click **Deploy** to launch your application. ## Eject to GitHub Templates that include application code and a sidecar database can be ejected to GitHub, giving you full control over the source code. From the **Magic Containers** apps overview, select the app you deployed from a template. Click **Eject to GitHub** and connect your GitHub account if you haven't already. After ejecting, configure an [image registry](/docs/magic-containers/image-registries) so Magic Containers can fetch your container images from GitHub. # Troubleshooting Source: https://bunny.net/docs/magic-containers/troubleshooting This page describes how to troubleshoot issues with applications deployed on Magic Containers. # Checking the application status The first step is to verify the status of your application. Open the Magic Containers Dashboard and go to the Overview tab. In the top-right corner, you will find the global application status. If your application is experiencing issues, this status will remain in a “Processing” state. This indicates that the platform is attempting to auto-heal the application. However, if there is a problem with the container image or the application fails to start due to failing health probes, it may remain stuck in this state. In such cases, the Overview tab will display relevant information or errors indicating the current deployment status. If errors appear on this screen, the next step is to inspect the detailed status in each region hosting your application. # Checking region and pod status If the global application status shows “Processing” or indicates an error, select the specific region you suspect is having issues. This will navigate you to the region details page, where you can view the status of all pods deployed in that region. Look for any pods flagged as “Error” or showing other failure indicators. Selecting the problematic pod will open a side panel where you can view further details, including live logs and error messages. Pods that cannot start or fail health checks are usually labeled with an error message clarifying the reason. Inspect these messages to determine whether the issue is related to the container image, network configuration, environment variables, or a failing health probe. The side panel also provides a live log stream to help identify application-level exceptions as they occur. Note that historical logs are not yet available, so you must monitor logs in real time to capture relevant information. To get more details, select the affected region to check details about the pod(s) running in that region. You can do this by clicking on the troublesome region. The view will navigate to the region details and display the status of all pods deployed in that region. Pods with issues will have an error indicating the cause. For further details, click on the pod to open the side panel, where you can view additional information, including detailed logs. At this time, we only support live streaming of logs. Historical log access is not yet available. # Additional support If you have attempted these troubleshooting steps without resolving your issue, consider redeploying your application with a fresh container image and reviewing your configuration for potential errors. If the issue persists or you suspect a problem within the bunny.net platform, you can reach out to our support team for further assistance. # Undeploy Source: https://bunny.net/docs/magic-containers/undeploy Temporarily stop a running container without deleting it. Undeploying stops your container but preserves its configuration, allowing you to redeploy later. Go to **Magic Containers** and select the container you want to undeploy. Click the menu icon (three dots) on the right side of the screen and click **Undeploy**. Click **Undeploy** to confirm. # Update App Source: https://bunny.net/docs/magic-containers/update Update your container to a new image version. To update a [deployed container](/docs/magic-containers/quickstart) to a new image version, make sure the new version is available in your container registry, then follow these steps. Go to **Magic Containers** and select the container you want to update. Click **Container Settings**, then click **Edit**. Edit container settings Select the new version from the image dropdown. Select image and tag Click **Update Container** to deploy the new version. Update container If there are no other changes to make to the app, click **Save Changes** to trigger a new deployment. Save changes Confirm the changes in the modal that pops up. Confirm changes Once saved, you'll see a **Processing** indicator while the app restarts with the new image version. To automate deployments, you can use a [GitHub Action](/docs/magic-containers/deploy-with-github-actions) to update your container whenever you push changes to your repository. # OpenAPI Specifications Source: https://bunny.net/docs/openapi Download OpenAPI specification files for bunny.net APIs. Download the OpenAPI specification files for bunny.net APIs to use with your favorite API client, code generator, or documentation tool. * [Core Platform API](https://core-api-public-docs.b-cdn.net/docs/v3/public.json) * [Origin Errors API](/docs/api-reference/origin-errors/openapi.json) * [CDN Logging API](https://logging.bunnycdn.com/docs/all/swagger.json) * [Storage API](/docs/api-reference/storage/openapi.json) * [Stream API](https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json) * [Shield API](https://api.bunny.net/shield/docs/v1/swagger.json) * [Edge Scripting API](https://core-api-public-docs.b-cdn.net/docs/v3/compute.json) * [Magic Containers API](https://api-mc.opsbunny.net/docs/public/swagger.json) # Automatic Optimization Source: https://bunny.net/docs/optimizer/automatic-optimization Configure automatic image, CSS, and JavaScript optimization settings Once [Bunny Optimizer](/docs/optimizer) is enabled, it automatically optimizes your static assets without any code changes. This guide covers the available configuration options. ## WebP Image Compression Automatically converts images to WebP format for supported browsers, reducing file sizes by up to 80% while maintaining visual quality. **How it works:** * Original URLs remain unchanged (e.g., `image.jpg`) * WebP is served transparently when supported * Verify by checking the `content-type` header: `image/webp` * Browsers without WebP support receive the original format ## Smart Image Optimization Serves appropriately sized images based on device type (desktop vs mobile). **Configuration:** * **Maximum desktop image width:** Images larger than this are automatically downsized * **Maximum mobile image width:** Separate sizing for mobile devices * **Desktop image quality:** 0-100% (recommended: 85-90%) * **Mobile image quality:** 0-100% (recommended: 75-85%) Lower quality settings on mobile save bandwidth without noticeable quality loss on smaller screens. ## CSS & JavaScript Minification Minification strips whitespace, comments, and unnecessary characters from your CSS and JavaScript files, reducing file sizes and improving download times without changing functionality. CSS and JavaScript minification can be toggled independently in your *Optimizer settings*. ### When to enable Enable minification if your files are not already minified. This reduces bandwidth usage and improves load times. ### When to disable Disable minification if your files are already minified. Re-processing pre-minified files adds serving latency with no meaningful size reduction. ### Mixed environments Files named `*.min.js` or `*.min.css` are automatically excluded from optimization. This allows you to keep minification enabled for unminified files while avoiding redundant processing for others. Ensure pre-minified files to follow the `.min.js` / `.min.css` convention. # Burrow Smart Routing Source: https://bunny.net/docs/optimizer/burrow-smart-routing Optimize uncached requests with intelligent path selection Burrow Smart Routing accelerates uncached, origin-bound requests by dynamically choosing the fastest, most reliable paths across Bunny's global backbone network. The internet relies on a fragile system of cables, infrastructure, and partnerships. Network outages, congestion, and cable cuts happen regularly and can cause severe connectivity issues. Burrow helps your traffic avoid these problems by intelligently routing requests around issues in real time. ## How to enable 1. Open your **Pull Zone** settings 2. Navigate to **Optimizer** 3. **Enable Bunny Optimizer** if not already enabled 4. Open the **Burrow** tab and click **Enable** Enable Burrow Smart
Routing Your uncached traffic will immediately start using intelligent path selection across the Bunny backbone network. ## How it works Burrow is an always-on, self-optimizing routing engine built into the bunny.net network infrastructure. When a request isn't in cache, Burrow: 1. **Monitors** global network conditions across 119 datacenters in 77 countries 2. **Analyzes** real-time latency and health signals 3. **Selects** the optimal path to your origin for each request 4. **Routes** traffic around congestion, packet loss, outages, and cable cuts 5. **Optimizes** TLS connections by reusing pre-secured connections instead of performing slow handshakes from distant locations On a typical day, Burrow delivers up to 40% faster average response times for dynamic or uncached content. During network outages or congestion, it can be the difference between staying online and experiencing downtime. Cached traffic continues to be served instantly from the edge as usual. Bunny Burrow ## Benefits **Faster response times:** Lower TTFB and up to 40% improvement in average response times for uncached requests. **Network resilience:** Automatic routing around outages, packet loss, and congestion across the global internet. **Optimized connections:** Pre-secured, reused TLS connections reduce handshake overhead and connection errors. **Better caching:** Improved connection reliability means more consistent cache population. **Self-optimizing:** Learns from real global traffic patterns to continuously improve path selection. **No configuration required:** Works automatically with no code changes or migrations needed. Burrow is particularly beneficial for API traffic, real-time applications, personalized content, e-commerce transactions, and SaaS applications where users are geographically distributed or far from your origin server. ## Pricing **During early access:** Free with no traffic limitations **After early access:** * 50 GB/month of uncached traffic included with Optimizer * \$0.06/GB for additional uncached traffic beyond the free tier * Only uncached, origin-bound requests count toward usage * Cached requests are not counted ## Performance monitoring View Burrow's impact in your Bunny Optimizer Statistics: * **Total time saved:** Cumulative milliseconds saved across all requests * **Average time saved per request:** Per-request performance improvement ## Active Origin Probing Coming soon Active probing will periodically test origin connection quality from edge locations with lightweight checks. This allows Burrow to map optimal paths ahead of time, ensuring the routing engine is always prepared with up-to-date network information, even during low-traffic periods. This feature is particularly beneficial for smaller projects that don't generate enough traffic volume for Burrow to learn patterns organically. # Changelog Source: https://bunny.net/docs/optimizer/changelog Latest updates and improvements to Bunny Optimizer ## AVIF, HEIC and HEIF are now GA Support for AVIF, HEIC and HEIF as input formats, and AVIF as an output format, has been promoted to [General Availability](/docs/product-release-stages). [Learn more](/docs/optimizer/dynamic-images/formats) ## AVIF output area limit AVIF output is now limited to a maximum area of 4 megapixels. Larger images automatically fall back to another format (WebP, JPEG, or GIF) to keep latency low. A new `304` processing error code is returned when this limit is reached. [Learn more](/docs/optimizer/dynamic-images/formats) ## AVIF support The Optimizer now supports AVIF as both an input and output format, offering better compression than WebP with excellent quality. [Learn more](/docs/optimizer/dynamic-images/formats) ## Image Upscaling Introduced the 'upscaling' parameter to allow images to be enlarged beyond their original dimensions using resampling. [Learn more](/docs/optimizer/dynamic-images/resizing) # Blur and Sharpen Source: https://bunny.net/docs/optimizer/dynamic-images/blur-and-sharpen Apply blur effects or sharpen images for clarity and focus control Control image sharpness and focus with blur and sharpen filters. Perfect for emphasizing subjects, creating depth effects, obscuring sensitive information, or enhancing image clarity. ## Parameters Enhance image clarity by increasing edge definition. **Default:** `false` Apply a blur effect to soften the image. **Range:** `0` to `100` **Default:** `0` ## How it works Bunny Optimizer provides two complementary filters for controlling image sharpness: **Sharpen:** Enhances edge definition and detail by increasing contrast along edges. Makes images appear crisper and more defined. **Blur:** Softens the image by reducing detail and creating a smooth, out-of-focus effect. Higher values produce stronger blur. ## Usage ### Sharpen Enhance image clarity by sharpening edges and details. The sharpen filter increases contrast along edges to make the image appear crisper. ```bash Original image theme={null} https://yourzone.b-cdn.net/image.jpg?sharpen=false ``` | sharpen=false | sharpen=true | | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | ![Original sharpness](https://bunny-optimizer-demo.b-cdn.net/bunny_next_to_flower.jpg?sharpen=false) | ![Sharpened image](https://bunny-optimizer-demo.b-cdn.net/bunny_next_to_flower.jpg?sharpen=true) | Useful for enhancing soft or slightly out-of-focus images, improving perceived detail after resizing, or compensating for compression artifacts. ### Blur Apply a blur effect to soften the image. The blur intensity increases with higher values, creating progressively softer results. ```bash No blur theme={null} https://yourzone.b-cdn.net/image.jpg?blur=0 ``` | blur=0 | blur=10 | | ------------------------------------------------------------------ | ---------------------------------------------------------------------- | | ![No blur](https://bunnyoptimizerdemo.b-cdn.net/bunny5.jpg?blur=0) | ![Light blur](https://bunnyoptimizerdemo.b-cdn.net/bunny5.jpg?blur=10) | ```bash Medium blur theme={null} https://yourzone.b-cdn.net/image.jpg?blur=25 ``` | blur=0 | blur=25 | | ------------------------------------------------------------------ | ----------------------------------------------------------------------- | | ![No blur](https://bunnyoptimizerdemo.b-cdn.net/bunny5.jpg?blur=0) | ![Medium blur](https://bunnyoptimizerdemo.b-cdn.net/bunny5.jpg?blur=25) | ```bash Heavy blur theme={null} https://yourzone.b-cdn.net/image.jpg?blur=60 ``` | blur=0 | blur=60 | | ------------------------------------------------------------------ | ---------------------------------------------------------------------- | | ![No blur](https://bunnyoptimizerdemo.b-cdn.net/bunny5.jpg?blur=0) | ![Heavy blur](https://bunnyoptimizerdemo.b-cdn.net/bunny5.jpg?blur=60) | Perfect for creating background effects, obscuring sensitive information like faces or license plates, generating thumbnails with privacy protection, or adding depth-of-field effects to emphasize foreground subjects. ## Combining with other transformations Blur and sharpen filters work seamlessly with other Bunny Optimizer parameters: ```bash Sharpen after resize theme={null} https://yourzone.b-cdn.net/image.jpg?width=800&sharpen=true ``` ```bash Blur with crop theme={null} https://yourzone.b-cdn.net/image.jpg?crop=600,400&blur=15 ``` ```bash Sharpen with brightness theme={null} https://yourzone.b-cdn.net/image.jpg?sharpen=true&brightness=10 ``` ```bash Blur background with focus crop theme={null} https://yourzone.b-cdn.net/image.jpg?focus_crop=400,300,0.5,0.5&blur=30 ``` Applying sharpen after resizing images can help restore clarity lost during the resize operation. This is especially useful when creating smaller thumbnails or responsive images. # Color Manipulation Source: https://bunny.net/docs/optimizer/dynamic-images/color-manipulation Adjust saturation, hue, contrast, tint, and sepia for precise color control Transform the color characteristics of your images with precise control over saturation, hue rotation, contrast, tinting, and sepia effects. Perfect for maintaining brand consistency, creating stylistic effects, or correcting color issues. ## Parameters Adjust the intensity of colors in the image. **Range:** `-100` to `100` **Default:** `0` Rotate colors around the color wheel. **Range:** `0` to `100` **Default:** `0` Adjust the difference between light and dark tones. **Range:** `-100` to `100` **Default:** `0` Apply a subtractive color tint over the image. **Format:** Hex color code (e.g., `aaffff`, `ff0000`) **Default:** `ffffff` (no tint) Apply a sepia tone effect to the image. **Range:** `0` to `100` **Default:** `0` ## How it works Bunny Optimizer provides multiple color manipulation tools: **Saturation:** Controls color intensity. Negative values desaturate (moving toward grayscale), positive values increase color vibrancy. **Hue:** Rotates colors around the color wheel. Every 33 units shifts to the next primary color in the spectrum. **Contrast:** Adjusts the difference between bright and dark areas. Positive values increase contrast, negative values reduce it. **Tint:** Applies a subtractive color overlay. The tint color is subtracted from the image, creating a color cast effect. **Sepia:** Converts the image to warm brown tones reminiscent of antique photographs. ## Usage ### Saturation adjustment Control the intensity of colors in your image. Negative values desaturate toward grayscale, while positive values increase color vibrancy. ```bash Comparison theme={null} https://yourzone.b-cdn.net/image.jpg?saturation=VALUE ``` | saturation=-100 | saturation=-25 | saturation=0 | saturation=25 | | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | | ![Fully desaturated](https://bunnyoptimizerdemo.b-cdn.net/bunny17.jpg?saturation=-100) | ![Partially desaturated](https://bunnyoptimizerdemo.b-cdn.net/bunny17.jpg?saturation=-25) | ![Original saturation](https://bunnyoptimizerdemo.b-cdn.net/bunny17.jpg?saturation=0) | ![Increased saturation](https://bunnyoptimizerdemo.b-cdn.net/bunny17.jpg?saturation=25) | A value of `-100` produces a completely grayscale image. Useful for creating black and white photos, reducing oversaturated colors, or maintaining consistent color intensity across different source images. ### Hue rotation Rotate colors around the color wheel. The hue parameter shifts colors through the spectrum, with every 33 units moving to the next primary color. ```bash Original colors theme={null} https://yourzone.b-cdn.net/image.jpg?hue=0 ``` | hue=0 | hue=33 | | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | | ![Original hue](https://bunny-optimizer-demo.b-cdn.net/easter_bunny.jpg?hue=0) | ![Hue shifted 33](https://bunny-optimizer-demo.b-cdn.net/easter_bunny.jpg?hue=33) | | hue=66 | hue=90 | | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | ![Hue shifted 66](https://bunny-optimizer-demo.b-cdn.net/easter_bunny.jpg?hue=66) | ![Hue shifted 90](https://bunny-optimizer-demo.b-cdn.net/easter_bunny.jpg?hue=90) | Perfect for color theme variations, creating stylistic effects, or adjusting color casts without affecting luminosity. ### Contrast adjustment Adjust the difference between bright and dark areas. Positive values make bright areas brighter and dark areas darker, while negative values flatten the tonal range. ```bash Original contrast theme={null} https://yourzone.b-cdn.net/image.jpg?contrast=0 ``` | contrast=0 | contrast=20 | contrast=-20 | | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | ![Original contrast](https://bunnyoptimizerdemo.b-cdn.net/bunny19.jpg?contrast=0) | ![Increased contrast](https://bunnyoptimizerdemo.b-cdn.net/bunny19.jpg?contrast=20) | ![Decreased contrast](https://bunnyoptimizerdemo.b-cdn.net/bunny19.jpg?contrast=-20) | Useful for making flat images pop, correcting overexposed or underexposed photos, or creating high-contrast stylistic effects. ### Tint application Apply a subtractive color tint over the image. The tint function works by subtracting the specified color from the image, creating a color cast effect. ```bash Apply cyan tint theme={null} https://yourzone.b-cdn.net/image.jpg?tint=aaffff ``` | Original | tint=aaffff (cyan) | tint=ffffaa (yellow) | tint=ffaaff (magenta) | | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | ![Original image](https://bunny-optimizer-demo.b-cdn.net/white_bunny_closeup_1.jpg) | ![Cyan tint](https://bunny-optimizer-demo.b-cdn.net/white_bunny_closeup_1.jpg?tint=aaffff) | ![Yellow tint](https://bunny-optimizer-demo.b-cdn.net/white_bunny_closeup_1.jpg?tint=ffffaa) | ![Magenta tint](https://bunny-optimizer-demo.b-cdn.net/white_bunny_closeup_1.jpg?tint=ffaaff) | Ideal for creating mood-based color grading, maintaining brand color themes, or correcting color temperature issues. Use hex color codes without the `#` prefix. ### Sepia effect Apply a warm, brown-toned sepia effect reminiscent of antique photographs. The intensity ranges from 0 (no effect) to 100 (full sepia tone). ```bash Apply sepia effect theme={null} https://yourzone.b-cdn.net/image.jpg?sepia=50 ``` | sepia=0 | sepia=50 | sepia=100 | | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | ![No sepia](https://bunny-optimizer-demo.b-cdn.net/bunny-at-the-edge-of-a-forrest.png?sepia=0) | ![Partial sepia](https://bunny-optimizer-demo.b-cdn.net/bunny-at-the-edge-of-a-forrest.png?sepia=50) | ![Full sepia](https://bunny-optimizer-demo.b-cdn.net/bunny-at-the-edge-of-a-forrest.png?sepia=100) | Perfect for creating vintage aesthetics, nostalgic effects, or warm-toned artistic presentations. ## Combining color adjustments Color manipulation parameters work seamlessly together and with other Bunny Optimizer transformations: ```bash Desaturate and increase contrast theme={null} https://yourzone.b-cdn.net/image.jpg?saturation=-50&contrast=15 ``` ```bash Hue shift with brightness theme={null} https://yourzone.b-cdn.net/image.jpg?hue=33&brightness=10 ``` ```bash Sepia with gamma correction theme={null} https://yourzone.b-cdn.net/image.jpg?sepia=75&gamma=15 ``` ```bash Tint and resize theme={null} https://yourzone.b-cdn.net/image.jpg?tint=aaffff&width=800 ``` Combine multiple adjustments to achieve precise color grading, correct multiple issues simultaneously, or create complex stylistic effects. # Cropping Source: https://bunny.net/docs/optimizer/dynamic-images/cropping Crop images to specific dimensions with precise control over positioning Crop images to exact dimensions with flexible positioning options. Perfect for creating thumbnails, hero images, or ensuring images fit specific layout requirements while keeping the important content in frame. Crop operations are processed before [resizing](/docs/optimizer/dynamic-images/resizing). When combining crop with width or height parameters, make sure to base your crop values on the original image dimensions. ## Parameters Crop the output image to specific dimensions with optional positioning. **Format 1 (Center crop):** Specify `width,height` to crop from the center or use with `crop_gravity` to control positioning. **Format 2 (Positioned crop):** Specify `width,height,x,y` where x and y are pixel offsets from the top-left corner. **Unit:** pixels Crop the image to match a specific aspect ratio while maintaining the center point. **Format:** `width:height` (e.g., `16:9`, `1:1`, `4:3`) **Default:** `auto` Set the anchor point for center-based crops (Format 1 only). **Values:** `center`, `north`, `south`, `east`, `west`, `northeast`, `northwest`, `southeast`, `southwest` **Default:** `center` Crop with a defined focal point that stays centered when possible. **Format 1 (Absolute):** `width,height,x_coordinate,y_coordinate` where x,y are pixel coordinates of the focal point. **Format 2 (Relative):** `width,height,x_relative,y_relative` where x,y are decimal values from 0.0 to 1.0. ## How it works When you apply crop parameters, Bunny Optimizer: 1. Identifies the crop dimensions and positioning you specified 2. Extracts the selected region from the original image 3. Applies any additional transformations (resize, filters, etc.) 4. Delivers the cropped result from the edge cache ## Usage ### Basic cropping Specify width and height to crop from the center of the image. ```bash Center crop theme={null} https://yourzone.b-cdn.net/image.jpg?crop=300,300 ``` | Original | crop=300,300 | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | | ![Original image](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass_4.jpg) | ![300x300 center crop](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass_4.jpg?crop=300,300) | Ideal for creating square thumbnails or consistently sized previews from various source images. ### Positioned cropping Add x and y coordinates to specify exactly where the crop should start. The coordinates represent pixels from the top-left corner of the original image. ```bash Crop from specific position theme={null} https://yourzone.b-cdn.net/image.jpg?crop=400,300,200,150 ``` | Original | crop=400,300,200,150 | | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | | ![Original image](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass_4.jpg) | ![Positioned crop](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass_4.jpg?crop=400,300,200,150) | Perfect for extracting specific portions of an image when you know the exact location of product details, logos, or areas of interest. ### Aspect ratio cropping Crop to a specific aspect ratio while keeping the center of the image in frame. ```bash Square crop theme={null} https://yourzone.b-cdn.net/image.jpg?aspect_ratio=1:1 ``` | Original | aspect\_ratio=1:1 | | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | | ![Original image](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass_4.jpg) | ![Square aspect ratio](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass_4.jpg?aspect_ratio=1:1) | ```bash Widescreen crop theme={null} https://yourzone.b-cdn.net/image.jpg?aspect_ratio=16:9 ``` | Original | aspect\_ratio=16:9 | | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | | ![Original image](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass_4.jpg) | ![16:9 aspect ratio](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass_4.jpg?aspect_ratio=16:9) | ```bash Portrait crop theme={null} https://yourzone.b-cdn.net/image.jpg?aspect_ratio=2:3 ``` | Original | aspect\_ratio=2:3 | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | | ![Original image](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass_4.jpg) | ![2:3 aspect ratio](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass_4.jpg?aspect_ratio=2:3) | Ideal for creating consistent image proportions across different content types like social media posts, product listings, or blog headers. ### Crop gravity Control where the crop is positioned using gravity anchors. Works with Format 1 cropping (width and height only) to snap the crop to different positions. ```bash Northwest gravity theme={null} https://yourzone.b-cdn.net/image.jpg?crop=400,300&crop_gravity=northwest ``` | Original | crop\_gravity=northwest | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | | ![Original image](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass_4.jpg) | ![Northwest crop](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass_4.jpg?crop=400,300\&crop_gravity=northwest) | ```bash South gravity theme={null} https://yourzone.b-cdn.net/image.jpg?crop=400,300&crop_gravity=south ``` | Original | crop\_gravity=south | | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | | ![Original image](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass_4.jpg) | ![South crop](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass_4.jpg?crop=400,300\&crop_gravity=south) | ```bash East gravity theme={null} https://yourzone.b-cdn.net/image.jpg?crop=400,300&crop_gravity=east ``` | Original | crop\_gravity=east | | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- | | ![Original image](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass_4.jpg) | ![East crop](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass_4.jpg?crop=400,300\&crop_gravity=east) | Perfect for ensuring important content stays in frame when cropping to fixed dimensions, such as keeping subjects visible in portraits or horizons aligned in landscape photography. Available gravity values: `center` (default), `north`, `south`, `east`, `west`, `northeast`, `northwest`, `southeast`, `southwest` ### Focus cropping Define a focal point that the crop centers around. Unlike standard positioned cropping, focus crop ensures your point of interest stays in the middle of the cropped area. When the focal point is too close to the image borders, Bunny Optimizer automatically adjusts the crop to maintain the specified dimensions while keeping the focal point as centered as possible. ```bash Absolute coordinates theme={null} https://yourzone.b-cdn.net/image.jpg?focus_crop=400,300,500,400 ``` | Original | focus\_crop=400,300,500,400 | | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | | ![Original image](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass_4.jpg) | ![Focus crop absolute](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass_4.jpg?focus_crop=400,300,500,400) | ```bash Relative coordinates theme={null} https://yourzone.b-cdn.net/image.jpg?focus_crop=400,300,0.5,0.5 ``` | Original | focus\_crop=400,300,0.5,0.5 | | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | | ![Original image](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass_4.jpg) | ![Focus crop relative](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass_4.jpg?focus_crop=400,300,0.5,0.5) | Use absolute pixel coordinates when you know the exact position, or relative positioning (0.0 to 1.0) for responsive focal points across images of different sizes. Ideal for user-uploaded images where you need to maintain specific focal points like profile pictures or product showcases. ## Combining with other transformations Cropping works seamlessly with other Bunny Optimizer parameters. Crop first, then apply resizing, quality adjustments, or filters to the cropped result: ```bash Crop and resize theme={null} https://yourzone.b-cdn.net/image.jpg?crop=800,600&width=400 ``` ```bash Crop with aspect ratio and quality theme={null} https://yourzone.b-cdn.net/image.jpg?aspect_ratio=16:9&quality=85 ``` ```bash Crop and convert format theme={null} https://yourzone.b-cdn.net/image.jpg?crop=600,600&format=webp ``` Remember that crop operations are processed before resizing. If you apply `crop=800,600&width=400`, the image is first cropped to 800×600, then resized to 400×300 (maintaining the aspect ratio from the crop). # Face Detection Source: https://bunny.net/docs/optimizer/dynamic-images/face-detection Automatically detect faces and crop images intelligently The Face Detection API uses AI to automatically detect faces in your images and create intelligent crops that keep people in frame. Face detection processing happens on the first request. The result is cached at the edge for instant delivery on subsequent requests. ## Parameter Automatically detect faces and center the crop around them. **Format 1 (Boolean):** Set to `true` to detect all faces and ensure they're in frame with padding. If no faces are detected, the image will not be cropped. **Format 2 (Dimensions):** Specify `width,height` to crop the image using the specified dimensions, with detected faces as the center point. If no faces are detected, the image is still cropped from the centre. ## How it works When you enable face detection, Bunny Optimizer: 1. Scans the image for human faces 2. Identifies all detected faces 3. Calculates the optimal center point (average of all face positions) 4. Crops the image to keep faces in frame with appropriate padding ## Usage ### Automatic padding Set `face_crop=true` to automatically detect faces and add padding around them. The algorithm ensures all detected faces remain in frame with intelligent padding. ```bash theme={null} https://yourzone.b-cdn.net/image.jpg?face_crop=true ``` | Original | With face\_crop=true | | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | ![Original image](https://bunny-optimizer-demo.b-cdn.net/face_1.jpeg) | ![Face detected with padding](https://bunny-optimizer-demo.b-cdn.net/face_1.jpeg?face_crop=true) | Ideal for dynamically cropping user-uploaded photos where you want to keep faces in frame without fixed dimensions. ### Dimensions (fixed size) Specify exact dimensions using `width,height` in pixels to crop around detected faces. Bunny Optimizer finds all faces and centers the crop at the average position. ```bash Portrait crop theme={null} https://yourzone.b-cdn.net/image.jpg?face_crop=800,1000 ``` ```bash Square thumbnail theme={null} https://yourzone.b-cdn.net/image.jpg?face_crop=500,500 ``` | Original | face\_crop=800,1000 | | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | ![Original image](https://bunny-optimizer-demo.b-cdn.net/face_1.jpeg) | ![800x1000 crop centered on faces](https://bunny-optimizer-demo.b-cdn.net/face_1.jpeg?face_crop=800,1000) | Perfect for creating consistent thumbnail sizes across your site while ensuring faces remain centered. ## Multiple faces When multiple faces are detected: * Bunny Optimizer calculates the center point between all faces * The crop is positioned to include as many faces as possible * With dimension mode, faces are prioritized at the center of the crop When only one face is detected: * The single face becomes the center point * Padding is added around the face (boolean mode) * The face is centered in the specified dimensions (dimension mode) ## Combining with other transformations Face detection works seamlessly with other parameters: ```bash Face crop with brightness adjustment theme={null} https://yourzone.b-cdn.net/image.jpg?face_crop=600,600&brightness=5 ``` ```bash Face crop with aspect ratio theme={null} https://yourzone.b-cdn.net/image.jpg?face_crop=true&aspect_ratio=1:1 ``` ```bash Face crop with blur theme={null} https://yourzone.b-cdn.net/image.jpg?face_crop=800,800&blur=5 ``` # Formats Source: https://bunny.net/docs/optimizer/dynamic-images/formats Convert images between formats for optimal compatibility and compression Convert images to modern, efficient formats for better compression and performance. Bunny Optimizer supports a wide range of input formats and can transcode to optimized output formats. ## Parameter Convert the image to a specific output format. **Accepted values:** `webp`, `jpeg`, `png`, `gif`, `avif` **Default:** Original format (or auto-optimized based on configuration) ## How it works Bunny Optimizer can automatically convert between image formats, enabling you to serve modern, efficient formats like WebP while accepting legacy formats as input. When configured with automatic format optimization, Bunny Optimizer detects client browser capabilities and serves the most optimal format. Browsers that support WebP receive WebP images, while older browsers receive JPEG or PNG. ## Supported formats ### Input formats Bunny Optimizer accepts the following input formats: * **JPEG** - Standard compressed photo format * **PNG** - Lossless format with transparency support * **WebP** - Compressed format with transparency support * **GIF** - Animation and simple graphics format * **BMP** - Uncompressed bitmap image format * **TIFF** - Flexible format for high-detail images * **TGA** - Raster graphics image format * **PBM** - Simple monochrome bitmap format * **HEIC** - Compressed photo format commonly used by Apple devices * **HEIF** - Image container with efficient compression * **AVIF** - Image format using AV1 compression ### Output formats Bunny Optimizer can convert images to these output formats: * **JPEG** - Best for photographs without transparency * **PNG** - Best for graphics, logos, or images requiring transparency * **WebP** - Modern format offering 25-35% better compression than JPEG * **GIF** - Best for simple animations or limited-color graphics * **AVIF** - Next-generation format with better compression than WebP and excellent quality, but slower encoding and slightly less universal support WebP output is limited to a maximum dimension of 16,383 pixels on either width or height. Bunny optimizer limits AVIF output to a maximum area of 4 megapixels. Because AVIF encoding time grows with image area, the Optimizer favours lower latency and automatically falls back to another format (WebP, JPEG, or GIF) for larger images. For most on-the-fly optimisation use cases, we recommend WebP as the best overall balance between compression, image quality, browser compatibility, and processing performance. AVIF can offer excellent compression, but is generally better suited for smaller images or workflows where the additional encoding time is acceptable. ## Usage ### Format conversion Convert images to different formats to optimize compression or ensure compatibility. ```bash Convert to WebP theme={null} https://yourzone.b-cdn.net/image.jpg?format=webp ``` ```bash Convert to PNG for transparency theme={null} https://yourzone.b-cdn.net/image.jpg?format=png ``` ```bash Convert to JPEG for smaller file size theme={null} https://yourzone.b-cdn.net/image.png?format=jpeg ``` WebP typically offers 25-35% better compression than JPEG at equivalent quality levels, making it ideal for web delivery when browser support is available. While AVIF can achieve excellent compression, WebP is generally recommended for on-the-fly optimisation because it provides the best overall balance between compression, quality, compatibility, and processing performance. ### Automatic format optimization Configure Bunny Optimizer to automatically serve the best format based on client browser capabilities. Enable WebP in your Optimizer settings to allow automatic format selection. When enabled: * Browsers supporting WebP receive WebP images * Older browsers receive JPEG or PNG fallbacks * No code changes required on your end This ensures optimal performance without requiring manual format detection or maintaining multiple URL variants. When automatic format optimization is enabled, your image URLs will remain as their original file type (e.g., `.png` or `.jpg`). The actual format served is determined by the `content-type` HTTP header, which will show `image/webp` when WebP is delivered. This approach ensures compatibility with browsers that don't support WebP while making implementation easier. ### Animation support Bunny Optimizer supports manipulation of animated image formats: * **WebP** (animated) * **GIF** (animated) * **PNG** (APNG format) We do not support transcoding animations to AVIF. If the original image is already in AVIF format and remains unmodified, the animation will be preserved. However, if the image is altered or converted from another format, the animation will be lost. Apply transformations like resizing, cropping, and color adjustments to animated images. The animation frames are processed while maintaining timing and loop settings. ```bash Resize animated GIF theme={null} https://yourzone.b-cdn.net/animation.gif?width=400 ``` ```bash Convert animated GIF to WebP theme={null} https://yourzone.b-cdn.net/animation.gif?format=webp ``` ## Common considerations ### Transparency handling When converting from formats that support transparency (PNG, WebP, GIF, AVIF) to formats that don't (JPEG), all transparent areas become opaque with a white or black background. If you need to preserve transparency, ensure your output format supports it: * Use `format=png` or `format=webp` for transparent images * Avoid `format=jpeg` when transparency is required ### WebP configuration Images are only encoded as WebP when you have WebP enabled in your Bunny Optimizer settings. To enable WebP output: 1. Navigate to your Pull Zone settings 2. Go to **Optimizer → Settings** 3. Enable WebP support Without this configuration, `format=webp` requests may not produce WebP output. ### Format selection guidance **Use JPEG when:** * Serving photographs or complex images * Transparency is not required * Maximum browser compatibility is needed **Use PNG when:** * Images contain transparency * Graphics, logos, or text require sharp edges * Lossless compression is preferred **Use WebP when:** * You need smaller file sizes than JPEG * Browser support for WebP is acceptable (most modern browsers) * You want transparency with better compression than PNG **Use GIF when:** * Simple animations are required * Limited color palettes are acceptable * Legacy compatibility is essential ## Combining with other transformations Format conversion works seamlessly with all other Bunny Optimizer parameters: ```bash Convert format and adjust quality theme={null} https://yourzone.b-cdn.net/image.jpg?format=webp&quality=75 ``` ```bash Resize and convert format theme={null} https://yourzone.b-cdn.net/image.png?width=800&format=jpeg ``` ```bash Complete optimization with format conversion theme={null} https://yourzone.b-cdn.net/image.jpg?width=1200&crop=800,600&format=webp&quality=80&sharpen=true ``` Combining format conversion with quality control and resizing creates an efficient optimization pipeline that reduces bandwidth while maintaining visual quality. # Luminosity Source: https://bunny.net/docs/optimizer/dynamic-images/luminosity Adjust brightness and gamma to control image exposure and tonality Control the brightness and exposure of your images with luminosity adjustments. Fix underexposed or overexposed photos, correct tonality issues, or create stylistic lighting effects. ## Parameters Uniformly lighten or darken the image by adjusting all pixels equally. **Range:** `-100` to `100` **Default:** `0` Adjust brightness based on each pixel's current luminosity, affecting bright pixels more than dark pixels. **Range:** `-100` to `100` **Default:** `0` ## How it works Bunny Optimizer provides two methods for adjusting image luminosity: **Brightness:** Applies a uniform adjustment to every pixel. Increases or decreases all pixel values by the same amount, which can quickly clip highlights (lose whites) when brightening or crush shadows (lose blacks) when darkening. **Gamma:** Applies a non-linear adjustment based on each pixel's current luminosity. Bright pixels are affected more than dark pixels, which mimics how human vision perceives brightness. This better preserves detail in both highlights and shadows. ## Usage ### Brightness adjustment Uniformly lighten or darken all pixels in the image. The adjustment affects each pixel equally regardless of its current value. ```bash Original theme={null} https://yourzone.b-cdn.net/image.jpg?brightness=0 ``` | brightness=0 | brightness=15 | brightness=-15 | | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | ![Original brightness](https://bunnyoptimizerdemo.b-cdn.net/bunny15.jpg?brightness=0) | ![Brightened image](https://bunnyoptimizerdemo.b-cdn.net/bunny15.jpg?brightness=15) | ![Darkened image](https://bunnyoptimizerdemo.b-cdn.net/bunny15.jpg?brightness=-15) | Positive values lighten the image, while negative values darken it. Useful for quick exposure corrections, though extreme values may lose detail in highlights or shadows. ```bash Lighten significantly theme={null} https://yourzone.b-cdn.net/image.jpg?brightness=30 ``` | brightness=0 | brightness=30 | | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | ![Original brightness](https://bunnyoptimizerdemo.b-cdn.net/bunny15.jpg?brightness=0) | ![Significantly brightened](https://bunnyoptimizerdemo.b-cdn.net/bunny15.jpg?brightness=30) | ```bash Darken significantly theme={null} https://yourzone.b-cdn.net/image.jpg?brightness=-30 ``` | brightness=0 | brightness=-30 | | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | ![Original brightness](https://bunnyoptimizerdemo.b-cdn.net/bunny15.jpg?brightness=0) | ![Significantly darkened](https://bunnyoptimizerdemo.b-cdn.net/bunny15.jpg?brightness=-30) | Brightness adjustments affect all pixels uniformly. High positive values can cause bright areas to lose detail (blown highlights), while high negative values can cause dark areas to lose detail (crushed blacks). ### Gamma correction Adjust brightness based on luminosity values, affecting bright pixels more than dark ones. This creates more natural-looking adjustments that preserve detail in both highlights and shadows. ```bash Original theme={null} https://yourzone.b-cdn.net/image.jpg?gamma=0 ``` | gamma=0 | gamma=20 | gamma=-20 | | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | ![Original gamma](https://bunnyoptimizerdemo.b-cdn.net/bunny15.jpg?gamma=0) | ![Gamma increased](https://bunnyoptimizerdemo.b-cdn.net/bunny15.jpg?gamma=20) | ![Gamma decreased](https://bunnyoptimizerdemo.b-cdn.net/bunny15.jpg?gamma=-20) | Positive values brighten the image while better preserving shadow detail. Negative values darken while maintaining highlight detail. Ideal for fixing global tonality issues or exposing additional detail in challenging lighting conditions. ```bash Brighten with gamma theme={null} https://yourzone.b-cdn.net/image.jpg?gamma=40 ``` | gamma=0 | gamma=40 | | --------------------------------------------------------------------------- | -------------------------------------------------------------------------- | | ![Original gamma](https://bunnyoptimizerdemo.b-cdn.net/bunny15.jpg?gamma=0) | ![Higher gamma](https://bunnyoptimizerdemo.b-cdn.net/bunny15.jpg?gamma=40) | ```bash Darken with gamma theme={null} https://yourzone.b-cdn.net/image.jpg?gamma=-40 ``` | gamma=0 | gamma=-40 | | --------------------------------------------------------------------------- | -------------------------------------------------------------------------- | | ![Original gamma](https://bunnyoptimizerdemo.b-cdn.net/bunny15.jpg?gamma=0) | ![Lower gamma](https://bunnyoptimizerdemo.b-cdn.net/bunny15.jpg?gamma=-40) | Perfect for correcting images shot in challenging lighting, fixing tonality issues, or creating more nuanced exposure adjustments than brightness alone. ## Brightness vs Gamma The key difference lies in how each adjustment affects pixels: **Brightness** adds or subtracts the same value from all pixels. A brightness of `+20` adds 20 to every pixel value, whether it's already bright or dark. This can quickly clip highlights or shadows. **Gamma** adjusts pixels proportionally based on their current luminosity. Bright pixels receive larger adjustments than dark pixels, creating a curve that mimics human vision and preserves more detail across the tonal range. Use brightness for simple, uniform adjustments. Use gamma when you need to preserve detail in highlights and shadows, or when correcting tonality issues in photos with varied lighting. ## Combining with other transformations Luminosity adjustments work seamlessly with other Bunny Optimizer parameters: ```bash Brighten and increase saturation theme={null} https://yourzone.b-cdn.net/image.jpg?gamma=15&saturation=10 ``` ```bash Darken and sharpen theme={null} https://yourzone.b-cdn.net/image.jpg?brightness=-10&sharpen=5 ``` ```bash Gamma correction with contrast theme={null} https://yourzone.b-cdn.net/image.jpg?gamma=20&contrast=5 ``` # Dynamic Images Source: https://bunny.net/docs/optimizer/dynamic-images/overview Transform images on the fly using URL parameters The Dynamic Images API lets you resize, crop, and transform images in real time by appending query parameters to image URLs. **No pre-processing or multiple file versions required**, just add parameters to your image URLs and Bunny Optimizer handles the rest. ## How it works Add transformation parameters to any image URL served through your Pull Zone: ```bash theme={null} https://yourzone.b-cdn.net/image.jpg?width=500&sharpen=true ``` Bunny Optimizer processes the transformation on the first request and caches the result at the edge. Subsequent requests are served instantly from the global CDN cache. All transformations preserve the original image. You can request the same source image with different parameters to generate multiple variations without storing duplicates. ## Prerequisites Before using Dynamic Images, ensure: 1. **Bunny Optimizer is enabled** on your Pull Zone 2. **Dynamic Image API is enabled** in Optimizer settings (**Optimizer → Settings**) 3. (Optional) Create [Image Classes](/docs/optimizer/image-classes) for predefined transformation presets ## Quick examples Perfect for responsive images across different devices and screen resolutions. **Resize while maintaining aspect ratio:** ```bash theme={null} https://yourzone.b-cdn.net/image.jpg?width=500 https://yourzone.b-cdn.net/image.jpg?height=300 ``` **Create thumbnails with specific aspect ratios:** Ideal for product listings and galleries. ```bash theme={null} https://yourzone.b-cdn.net/image.jpg?aspect_ratio=16:9&width=256 https://yourzone.b-cdn.net/image.jpg?aspect_ratio=1:1&height=400 ``` **Crop to specific dimensions:** ```bash theme={null} https://yourzone.b-cdn.net/image.jpg?crop=800,600 https://yourzone.b-cdn.net/image.jpg?crop=400,300&crop_gravity=north ``` **Smart cropping with face detection:** ```bash theme={null} https://yourzone.b-cdn.net/image.jpg?face_crop=600,600 ``` **Adjust colors and exposure:** ```bash theme={null} https://yourzone.b-cdn.net/image.jpg?brightness=10&contrast=5&saturation=15 https://yourzone.b-cdn.net/image.jpg?gamma=20&hue=33 ``` **Apply filters and effects:** Great for privacy protection and brand consistency. ```bash theme={null} https://yourzone.b-cdn.net/image.jpg?sharpen=true&blur=0 https://yourzone.b-cdn.net/image.jpg?sepia=75 ``` **Optimize quality and format:** Essential for performance optimization and social media. ```bash theme={null} https://yourzone.b-cdn.net/image.jpg?quality=75&format=webp ``` **Combine multiple transformations:** ```bash theme={null} https://yourzone.b-cdn.net/image.jpg?width=800&crop=600,400&brightness=5&sharpen=true&quality=80 https://yourzone.b-cdn.net/image.jpg?aspect_ratio=16:9&gamma=10&saturation=10&format=webp ``` ## Available transformations Resize by width or height while maintaining aspect ratio Crop to dimensions, aspect ratios, or focal points Automatically detect and crop around faces Adjust saturation, hue, contrast, tint, and sepia Control brightness and gamma for exposure correction Apply blur effects or enhance sharpness Flip, flop, and rotate in 90-degree increments Control compression levels and file size Convert between JPEG, PNG, WebP, and GIF ## Transformation order When combining multiple parameters, transformations are applied in a specific order: 1. **Crop operations** (`crop`, `aspect_ratio`, `focus_crop`, `face_crop`) 2. **Resize operations** (`width`, `height`) 3. **Reflections and rotations** (`flip`, `flop`, `rotate`) 4. **Filters** (`blur`, `sharpen`) 5. **Color and luminosity adjustments** (`brightness`, `gamma`, `contrast`, `saturation`, `hue`, `tint`, `sepia`) 6. **Format and quality** (`format`, `quality`) Understanding this order helps you predict the final output when combining multiple transformations. ## Performance and caching All transformations are processed once and cached at the edge: * **First request:** Bunny Optimizer processes the transformation and stores the result in the global CDN cache * **Subsequent requests:** Served instantly from the nearest edge location * **Cache invalidation:** Purging the original image also purges all transformed variations This approach ensures fast delivery while maintaining flexibility. You can request new transformation combinations at any time without impacting performance of existing cached variations. ## Best practices **Use Image Classes for consistency:** Define common transformation patterns as [Image Classes](/docs/optimizer/image-classes) to maintain consistency and simplify URLs. **Combine transformations in single requests:** Apply multiple transformations in one URL rather than chaining multiple requests to reduce processing overhead. **Test quality settings:** Always verify that quality and compression settings produce acceptable visual results on target devices. **Leverage automatic format conversion:** Enable WebP in Optimizer settings to automatically serve modern formats to supported browsers. **Consider transformation order:** Remember that crops are applied before resizing. Base crop dimensions on the original image size, not the target size. **Monitor bandwidth savings:** Use quality control and format conversion together to achieve significant file size reductions while maintaining visual quality. # Quality Source: https://bunny.net/docs/optimizer/dynamic-images/quality Control image compression for optimal balance between file size and visual quality Optimize image file size with quality controls that balance visual fidelity with bandwidth savings. Adjust compression levels to deliver fast-loading images without sacrificing appearance. ## Parameter Control the compression level of the output image. **Range:** `0` to `100` (higher values = better quality, larger file size) **Default:** `85` (or configured Smart Image Optimization value) ## How it works The quality parameter controls the compression ratio applied to the image. Higher values preserve more detail but result in larger file sizes. Lower values reduce file size but may introduce compression artifacts. Quality affects JPEG, WebP, and AVIF formats. For formats that don't support lossy compression (like PNG), the quality parameter has no effect. ## Usage ### Quality control Adjust the compression level to balance visual quality with file size. Higher quality values preserve more detail but increase bandwidth usage. ```bash High quality theme={null} https://yourzone.b-cdn.net/image.jpg?quality=80 ``` | quality=80 | quality=50 | quality=10 | | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | ![High quality (117KB)](https://bunny-optimizer-demo.b-cdn.net/bunnies_1.jpg?quality=80) | ![Medium quality (81KB)](https://bunny-optimizer-demo.b-cdn.net/bunnies_1.jpg?quality=50) | ![Low quality (35KB)](https://bunny-optimizer-demo.b-cdn.net/bunnies_1.jpg?quality=10) | As shown above, reducing quality from 80 to 50 cuts file size by nearly 30% with minimal visible impact. Extreme compression (`quality=10`) significantly reduces file size but introduces noticeable artifacts. ```bash Recommended for web use theme={null} https://yourzone.b-cdn.net/image.jpg?quality=75 ``` A quality setting between 70-85 typically provides the best balance for web images, maintaining good visual quality while achieving substantial file size reduction. ### Default quality behavior If you don't specify a quality parameter, Bunny Optimizer uses a default value based on your configuration. #### Smart Image Optimization Configure quality defaults in your Pull Zone settings at **Optimizer → Settings**. You can set different quality levels for desktop, mobile, and other device types. With Smart Image Optimization enabled, desktop requests might use `quality=80` while mobile devices use `quality=70` to reduce bandwidth on slower connections. #### Fallback default When Smart Image Optimization is not configured and no quality parameter is provided, images default to `quality=85`. **Priority order:** Quality specified in query parameters takes highest priority, followed by Smart Image Optimization settings, then the fallback default of 85. ## Combining with other transformations Quality works seamlessly with all other Bunny Optimizer transformations: ```bash Resize with quality control theme={null} https://yourzone.b-cdn.net/image.jpg?width=800&quality=75 ``` ```bash Crop, resize, and optimize theme={null} https://yourzone.b-cdn.net/image.jpg?crop=600,400&width=300&quality=80 ``` ```bash Complete optimization pipeline theme={null} https://yourzone.b-cdn.net/image.jpg?width=1200&quality=75&format=webp&sharpen=true ``` Combining transformations allows you to resize, crop, apply filters, and optimize compression in a single request, reducing processing overhead and simplifying your image workflow. ## Best practices **For photography and detailed images:** Use quality values between 75-85 to preserve detail while achieving good compression. **For thumbnails and previews:** Quality values between 60-75 are often sufficient, as small images hide compression artifacts better. **For images with text or sharp edges:** Consider using PNG format or higher quality values (85+) to avoid artifacts around text. **For maximum bandwidth savings:** Combine quality settings with WebP or AVIF [format conversion](/docs/optimizer/dynamic-images/formats), but always verify visual quality meets your standards. **Test on real devices:** Quality perception varies across devices and screen resolutions. Test your quality settings on target devices to ensure acceptable visual results. **Consider content type:** Photographs can tolerate lower quality settings (60-75) better than graphics, logos, or screenshots which benefit from higher settings (80-90). # Reflections and Rotations Source: https://bunny.net/docs/optimizer/dynamic-images/reflections-and-rotations Flip, flop, and rotate images to adjust orientation Transform image orientation with flip, flop, and rotation operations. Perfect for correcting image orientation, creating mirror effects, or ensuring consistent alignment across your content. ## Parameters Flip the image vertically along the horizontal axis. **Default:** `false` Flip the image horizontally along the vertical axis (mirror effect). **Default:** `false` Rotate the image by a specified angle. **Accepted values:** `-270`, `-180`, `-90`, `0`, `90`, `180`, `270` **Default:** `0` ## How it works Bunny Optimizer provides three types of orientation transformations: **Flip:** Creates a vertical reflection by flipping the image along the horizontal axis (top becomes bottom). **Flop:** Creates a horizontal reflection by flipping the image along the vertical axis (left becomes right), producing a mirror effect. **Rotate:** Rotates the image clockwise (positive values) or counter-clockwise (negative values) in 90-degree increments. ## Usage ### Flip (vertical reflection) Flip the image vertically along the horizontal axis. The top of the image becomes the bottom and vice versa. ```bash Original image theme={null} https://yourzone.b-cdn.net/image.jpg?flip=false ``` | flip=false | flip=true | | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | ![Original orientation](https://bunnyoptimizerdemo.b-cdn.net/bunny7.jpg?flip=false) | ![Flipped vertically](https://bunnyoptimizerdemo.b-cdn.net/bunny7.jpg?flip=true) | Useful for correcting upside-down images or creating reflection effects. ### Flop (horizontal reflection) Flip the image horizontally along the vertical axis, creating a mirror effect. The left side becomes the right side and vice versa. ```bash Original image theme={null} https://yourzone.b-cdn.net/image.jpg?flop=false ``` | flop=false | flop=true | | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | | ![Original orientation](https://bunny-optimizer-demo.b-cdn.net/red_bunny_closeup.jpg?flop=false) | ![Flopped horizontally](https://bunny-optimizer-demo.b-cdn.net/red_bunny_closeup.jpg?flop=true) | Perfect for creating mirror images, correcting camera orientation, or ensuring subjects face the desired direction in your layout. ### Rotate Rotate the image in 90-degree increments. Positive values rotate clockwise, negative values rotate counter-clockwise. ```bash Rotate 90 degrees clockwise theme={null} https://yourzone.b-cdn.net/image.jpg?rotate=90 ``` | rotate=0 | rotate=90 | | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | | ![Original rotation](https://bunnyoptimizerdemo.b-cdn.net/bunny7.jpg?rotate=0) | ![Rotated 90 degrees](https://bunnyoptimizerdemo.b-cdn.net/bunny7.jpg?rotate=90) | ```bash Rotate 180 degrees theme={null} https://yourzone.b-cdn.net/image.jpg?rotate=180 ``` | rotate=0 | rotate=180 | | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | | ![Original rotation](https://bunnyoptimizerdemo.b-cdn.net/bunny7.jpg?rotate=0) | ![Rotated 180 degrees](https://bunnyoptimizerdemo.b-cdn.net/bunny7.jpg?rotate=180) | ```bash Rotate 90 degrees counter-clockwise theme={null} https://yourzone.b-cdn.net/image.jpg?rotate=-90 ``` | rotate=0 | rotate=-90 | | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | | ![Original rotation](https://bunnyoptimizerdemo.b-cdn.net/bunny7.jpg?rotate=0) | ![Rotated -90 degrees](https://bunnyoptimizerdemo.b-cdn.net/bunny7.jpg?rotate=-90) | Ideal for correcting images taken in portrait mode, fixing EXIF orientation issues, or adjusting content to fit different layout orientations. ## Combining transformations Reflection and rotation operations work seamlessly with other Bunny Optimizer parameters: ```bash Flip and resize theme={null} https://yourzone.b-cdn.net/image.jpg?flip=true&width=800 ``` ```bash Rotate and crop theme={null} https://yourzone.b-cdn.net/image.jpg?rotate=90&crop=600,400 ``` ```bash Flop with brightness theme={null} https://yourzone.b-cdn.net/image.jpg?flop=true&brightness=10 ``` ```bash Rotate and convert format theme={null} https://yourzone.b-cdn.net/image.jpg?rotate=180&format=webp ``` You can also combine multiple orientation operations: ```bash Flip and rotate theme={null} https://yourzone.b-cdn.net/image.jpg?flip=true&rotate=90 ``` This applies both transformations sequentially, allowing for complex orientation adjustments. # Resizing Source: https://bunny.net/docs/optimizer/dynamic-images/resizing Resize images by width or height while maintaining aspect ratio Resize images to specific dimensions while automatically maintaining aspect ratio. Generate responsive images for different screen sizes, create consistent thumbnails, or optimize oversized images for faster loading. When both width and height are specified, Bunny Optimizer automatically uses whichever produces the smaller image while preserving aspect ratio. ## Parameters Resize the image to match the given width in pixels while maintaining aspect ratio. **Unit:** pixels Resize the image to match the given height in pixels while maintaining aspect ratio. **Unit:** pixels Allows the image to be enlarged beyond its original dimensions. **Values:** `off`, `resampling` **Default:** `off` The default upscaling behaviour can be changed to `resampling` (so you don't need to specify the query parameter each time) in the Pull Zone config via the [Update Pull Zone](/docs/api-reference/core/pull-zone/update-pull-zone) API by setting `OptimizerEnableUpscaling` to `true`. ## How it works When you apply width or height parameters, Bunny Optimizer: 1. Calculates the new dimensions based on your specified value 2. Maintains the original aspect ratio automatically 3. Resizes the image proportionally 4. Delivers the optimized result from the edge cache ## Usage ### Width-based resizing Specify a width to resize the image proportionally. The height adjusts automatically to maintain aspect ratio. Ideal for creating responsive images where you need consistent widths across different screen sizes or generating multiple sizes for `srcset` attributes. ```bash theme={null} https://yourzone.b-cdn.net/image.jpg?width=300 ``` | Original | width=300 | | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | ![Original image](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass.jpg) | ![Resized to 300px width](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass.jpg?width=300) | ```bash Smaller width theme={null} https://yourzone.b-cdn.net/image.jpg?width=150 ``` | Original | width=150 | | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | ![Original image](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass.jpg) | ![Resized to 150px width](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass.jpg?width=150) | ### Height-based resizing Specify a height to resize the image proportionally. The width adjusts automatically to maintain aspect ratio. Perfect for layouts where vertical space is constrained, such as horizontal carousels, fixed-height containers, or product listing grids. ```bash theme={null} https://yourzone.b-cdn.net/image.jpg?height=150 ``` | Original | height=150 | | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | ![Original image](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass.jpg) | ![Resized to 150px height](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass.jpg?height=150) | ```bash Smaller height theme={null} https://yourzone.b-cdn.net/image.jpg?height=100 ``` | Original | height=100 | | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | ![Original image](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass.jpg) | ![Resized to 100px height](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass.jpg?height=100) | ## Combining width and height When both width and height are specified, Bunny Optimizer intelligently selects whichever produces the smaller image while preserving aspect ratio. This ensures images fit within specific layout constraints while maintaining their original proportions. ```bash theme={null} https://yourzone.b-cdn.net/image.jpg?width=100&height=75 ``` Given an image with dimensions 500×333: | width=100 | height=75 | width=100\&height=75 (result) | | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | ![Resized by width](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass.jpg?width=100) | ![Resized by height](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass.jpg?height=75) | ![Both parameters applied](https://bunny-optimizer-demo.b-cdn.net/bunny_in_grass.jpg?width=100\&height=75) | In this example, `height=75` produces a smaller image (113×75) compared to `width=100` (100×67), so the height parameter takes effect. ## Combining with other transformations Resizing works seamlessly with other Bunny Optimizer parameters. Reduce bandwidth by combining with quality adjustments, convert formats for better compression, or apply filters to enhance resized images: ```bash Resize with quality adjustment theme={null} https://yourzone.b-cdn.net/image.jpg?width=800&quality=85 ``` ```bash Resize with format conversion theme={null} https://yourzone.b-cdn.net/image.jpg?width=600&format=webp ``` ```bash Resize with sharpen filter theme={null} https://yourzone.b-cdn.net/image.jpg?height=400&sharpen=3 ``` [Crop](/docs/optimizer/dynamic-images/cropping) operations have priority over width or height parameters. If any crop is defined in the query, it will be applied first, then resizing is applied to the cropped result. ## Upscaling By default, Bunny Optimizer does not enlarge images beyond their original size. Use `upscaling=resampling` to permit standard resampling-based upscaling. When enabled, upscaling is limited to an output size of 24 megapixels while preserving aspect ratio. This limit applies only when the result would be larger than the original image. If the original image is already larger than 24 megapixels, it is not reduced unless another transformation requires it. ```bash Upscale 1024x512 image to 2048x1024 theme={null} https://yourzone.b-cdn.net/image.jpg?width=2048&upscaling=resampling ``` ```bash Upscale 1024x512 image to 1536x768 theme={null} https://yourzone.b-cdn.net/image.jpg?width=1536&height=768&upscaling=resampling ``` ```bash Downscale 1024x512 image to 512x256. No upscaling (target is smaller than original). theme={null} https://yourzone.b-cdn.net/image.jpg?width=512&upscaling=resampling ``` # HTML Prerender Source: https://bunny.net/docs/optimizer/html-prerender Improve SEO for single-page applications HTML Prerender improves search engine optimization and social media link previews for single-page applications by serving fully rendered HTML to bots and crawlers. Modern JavaScript frameworks like React, Vue, Angular, Blazor, and others make building rich, interactive experiences easier. However, search engines and AI crawlers don't execute JavaScript during indexing, which means your content can be missed or deprioritized. HTML Prerender solves this by capturing fully rendered HTML snapshots and serving them to bots, while your users continue to receive the full dynamic application. ## How to enable 1. Open your **Pull Zone** settings 2. Navigate to **Optimizer** 3. **Enable Bunny Optimizer** if not already enabled 4. Enable **HTML Prerender** Enable HTML Prender Changes propagate across the CDN within a few minutes. Crawlers and bots will immediately start receiving fully rendered HTML snapshots. ## How it works When enabled, Bunny Optimizer detects search engine bots and social media crawlers, then: 1. **Detects** crawlers and bots based on user agents and creates a specialized cache key 2. **Renders** your page in a real browser to generate a complete HTML snapshot 3. **Optimizes** critical metadata (title, description, canonical, Open Graph, Twitter Card tags) for reliable parsing 4. **Caches** snapshots globally at the edge, revalidating based on your cache headers and purge rules 5. **Serves** the static HTML to crawlers while delivering your dynamic app to human visitors **No code changes required.** Your URLs and frontend remain unchanged. Before and after Bunny Optimizer Prerender
comparison ## Benefits **Improved crawl performance:** Crawlers get instant access to your content without waiting for JavaScript execution, reducing page load time by up to 99.9%. **Better search visibility:** Search engines can index your content immediately, potentially improving visibility by up to 95%. **AI-ready content:** Large language models and AI crawlers can quickly parse your content structure and headings. **Optimized social previews:** Open Graph and Twitter Card metadata is present in the first response, ensuring rich link previews on social platforms like Facebook, LinkedIn, X, Slack, and Discord. **Reduced crawl budget waste:** Faster response times mean crawlers can discover more of your pages per session. HTML Prerender is particularly valuable for single-page applications, headless CMS sites, e-commerce catalogs, marketing pages, documentation sites, and WebAssembly applications. Even if you're using SSR, SSG, or ISR, it ensures crawlers consistently receive clean, static HTML from the edge. ## Supported crawlers Prerender automatically serves rendered HTML to: * Google, Bing, Yahoo search bots * Facebook, Twitter/X, LinkedIn social crawlers * AI model crawlers (ChatGPT, Claude, etc.) * Other known SEO and preview bots Human visitors always receive the standard application. ## Content integrity HTML Prerender is designed to help search engines and bots consume your content optimally, not to manipulate rankings: * **No cloaking:** Serves semantically equivalent content; the prerender is a faithful representation of what users see * **Respects directives:** Honors robots.txt and meta tag directives * **No manipulation:** Only optimizes delivery, never alters content structure or adds keywords * **Developer-friendly:** Works with any origin behind Bunny Optimizer, including Bunny Storage ## Controlling when a snapshot is captured HTML Prerender automatically determines when a page is ready by monitoring network activity. In some applications, the network can become idle before the page has finished updating the DOM, which may result in an incomplete snapshot. You can explicitly control when the page is considered ready to be captured by setting `window.prerenderReady` to `false` as early as possible, ideally in the document ``: ```javascript theme={null} ``` Once your application has finished fetching data and rendering the final DOM, set it to `true`: ```javascript theme={null} window.prerenderReady = true; ``` HTML Prerender will then capture the page once rendering is complete. A maximum rendering timeout is enforced. If a page takes too long to finish rendering, HTML Prerender may still capture its current state before all content is available. ## Limitations * Only publicly accessible URLs are prerendered * Pages requiring authentication are not prerendered * Complex client-side interactions may not be captured # Image Classes Source: https://bunny.net/docs/optimizer/image-classes Define reusable transformation presets Image Classes let you create named presets for Dynamic Images API transformations, making it easier to maintain consistent image styles across your website. ## Why use Image Classes? Instead of this: ```bash theme={null} https://yourzone.b-cdn.net/image.jpg?width=300&height=200&quality=85&sharpen=true ``` Use this: ```bash theme={null} https://yourzone.b-cdn.net/image.jpg?class=thumbnail ``` **Benefits:** * Consistent transformations across your site * Easier to update styles (change the class, not every URL) * Improved cache hit rates * Enhanced security with forced transformation classes ## Creating Image Classes Navigate to **Optimizer** → **Image Classes** Image Classes overview Configure the transformation settings for your image class: * Width, height, aspect ratio * Quality settings * Filters and effects * Any Dynamic Images API parameter Add image class properties Save the class with a memorable name that describes its purpose (e.g., `thumbnail`, `hero`, `product-mobile`). ## Forced transformation classes Enable this option to restrict the Dynamic Images API to only use predefined classes: * Improves cache efficiency (fewer unique transformations) * Prevents arbitrary transformations (security benefit) * Returns 403 for invalid or missing classes This is recommended for production environments where you want tight control over image transformations. ## Example classes | Property | `thumbnail` | `hero_image` | `mobile_product` | | ------------------- | ------------- | ------------ | --------------------------- | | Width | 300px | 1920px | 800px | | Height | 200px | - | - | | Aspect Ratio | - | 16:9 | - | | Quality | 80% | 90% | 75% | | Additional Settings | Sharpen: true | - | Smart optimization: enabled | # Bunny Optimizer Source: https://bunny.net/docs/optimizer/index Bunny Optimizer accelerates your website with automatic asset optimization, smart routing, and dynamic image manipulation. bunny.net Optimizer Bunny Optimizer works alongside [Bunny CDN](/docs/cdn) to make your website faster through automatic optimization and intelligent delivery. ## What's included Users typically see significant improvements: * Up to **5x faster load times** with WebP compression * Up to **40% faster uncached requests** with Burrow * Up to **80% file size reduction** for images Fast websites improve user experience and increase conversion rates. Optimizer helps you deliver that performance with just a few clicks. WebP compression, CSS/JS minification, responsive image optimization, and watermarking with unlimited requests. Real-time image manipulation via URL parameters. Resize, crop, transform, and apply filters on the fly. Intelligent path selection for uncached requests with up to 40% faster response times. Fully rendered HTML for search engines and social crawlers. Improves SEO for SPAs with no code changes. Display a branded loading screen when your origin is slow to respond, improving perceived performance. # Optimizer limits Source: https://bunny.net/docs/optimizer/limits Overview of the default limits for the Bunny Optimiser The following limits are fixed and cannot be changed. *** | Limit | Value | | -------------------------------------------------------------- | ----- | | Maximum time (in seconds) to download an image from the origin | 20 | | Maximum time (in seconds) spent processing an image | 60 | | Maximum upscaling size (in megapixels) | 24 | | Maximum AVIF output area (in megapixels) | 4 | # Pricing Source: https://bunny.net/docs/optimizer/pricing Simple, predictable pricing for optimising your website. Bunny Optimizer is priced at \$9.50 per pull zone per month and includes unlimited optimisation, requests, and transformations. CDN bandwidth is billed separately. # Quickstart Source: https://bunny.net/docs/optimizer/quickstart Enable Bunny Optimizer and start optimizing your website in minutes 1. From your Bunny dashboard, go to the left-hand navigation menu 2. Select the Pull Zone where you want to enable Bunny Optimizer 3. Once inside your Pull Zone, scroll down in the left-hand menu and click on “Optimizer” Access the CDN Zone You will now see the Bunny Optimizer overview screen Click the orange button labeled “Turn on Bunny Optimizer.” That’s it! Bunny Optimizer is now active for your zone Access the CDN Zone That's it! Your CSS, JavaScript, and images are now being automatically optimized. **Image optimization:** * Set maximum image widths for desktop and mobile * Adjust quality settings (0-100%) * Enable smart device-specific optimization **Minification:** * Toggle CSS minification * Toggle JavaScript minification **WebP compression:** * Automatically serves WebP to supported browsers * No URL changes required See the [Automatic Optimization](/docs/optimizer/automatic-optimization) guide for detailed configuration options. Transform images on the fly by adding query parameters: 1. Go to **Optimizer** → **Settings** 2. Enable **Dynamic Image API** Now you can manipulate any image: ```bash theme={null} # Original https://yourzone.b-cdn.net/photo.jpg # Resize to 500px width https://yourzone.b-cdn.net/photo.jpg?width=500 # Create 16:9 thumbnail https://yourzone.b-cdn.net/photo.jpg?aspect_ratio=16:9&width=256 ``` Learn more in the [Dynamic Images guide](/docs/optimizer/dynamic-images). Routes uncached requests over optimized paths for faster origin response times. Improves SEO for single-page applications by serving rendered HTML to crawlers. # Troubleshooting Source: https://bunny.net/docs/optimizer/troubleshooting This page provides guidance on how to troubleshoot issues with the Bunny Optimiser The information on this page should be used for troubleshooting purposes only. Clients should not rely on these headers or error codes, as they may change in the future. ## Response headers Responses from the Bunny Optimiser may include the following headers: * `X-DownloadSize`: Size of the original asset downloaded from the origin * `X-BO-CompressionRatio`: Percentage reduction in size of the final asset compared to the original * `X-BO-OriginDownloadTime`: Time spent downloading files from the origin (including the watermark, if present), in milliseconds * `X-BO-ProcessingTime`: Time spent on local processing, in milliseconds * `X-BO-Processing-Error`: Lists any errors that occurred while processing the request (see below) ### Error codes The following errors may be returned as the value of the `X-BO-Processing-Error` header. #### General errors * `101` → An unknown error occurred * `102` → The original source file could not be downloaded from the origin * `103` → The source image could not be decoded or parsed as a valid image * `104` → Optimisation was skipped or failed because the output became larger than the original * `105` → The GIF file is invalid or cannot be processed * `106` → Text-based content could not be minified successfully * `107` → The requested output format is not supported #### Watermark-related * `200` → The watermark image could not be retrieved from its origin * `201` → The watermark origin request was cancelled before completion * `202` → The watermark image could not be decoded or parsed as a valid image * `204` → The watermark image URL returned a 404 Not Found response #### Processing constraints * `301` → The generated or requested WebP image exceeds the allowed size limit * `302` → The requested resize would upscale the image, but upscaling is not enabled * `303` → The requested upscale exceeds the allowed limit * `304` → The AVIF output image exceeds the allowed size limit and was encoded in another format instead # Image Watermarking Source: https://bunny.net/docs/optimizer/watermarking Automatically protect and brand your images with custom watermarks Bunny Optimizer can automatically apply watermarks to your images to protect intellectual property, reinforce branding, or mark user-generated content. Watermarks are applied during the optimization process and cached. There's no performance penalty after the first request. ## Configuration Access watermarking settings in **Optimizer** → **Settings** → **Watermark images** Enable Watermarking ### Watermark image URL The publicly accessible URL to your watermark PNG (transparency supported). **Best practices:** * Use PNG format with transparency * Design at 2x resolution for sharp display * Keep file size under 100KB * Test on both light and dark images ### Minimum image size Only images with width OR height greater than this value will be watermarked (in pixels). **Example:** Setting this to 800px prevents watermarks on small thumbnails while protecting larger downloads. ### Position Choose from a few different placement options: * Bottom Right * Bottom Left * Top Right * Top Left * Center * Center Stretched ### Border offset Distance from the edge as a percentage (0-100%). **Example:** 5% offset on a bottom-right watermark places it 5% away from both the bottom and right edges. ## Combining with Dynamic Images API Watermarking applies after Dynamic Images transformations, ensuring your watermark appears on all generated variants. # Product Release Stages Source: https://bunny.net/docs/product-release-stages We introduce new products and major features through a phased release process. This allows us to validate real-world and production use cases, ensure reliability and performance, gather customer feedback, and align support and go-to-market readiness before declaring a product fully production-ready. Each stage sets clear expectations around access, pricing, stability, and support. ## **Release stages at a glance** | Stage | Access | Pricing | Intended use | | ----------------------------- | ----------- | -------------------------------- | ----------------------------------------------- | | **Closed Preview** | Invite-only | Free | Early validation with trusted users | | **Public Preview** | Public | Free or paid (varies by product) | Real-world and **limited production** workloads | | **General Availability (GA)** | Public | Paid | Production workloads | ## **Stage 1: Closed Preview** **Closed Preview** is an invite-only early access phase focused on validating core functionality with a small group of trusted users. During this stage, we work closely with participants to validate use cases and iterate rapidly. ### **Access and pricing** * Access: Invite-only * Pricing: Free ### **What you can expect** * Functional end-to-end, but not fully polished * A **“Preview”** label in the UI and documentation * Features and APIs may change * Feedback directly influences the roadmap * Users are selected from early adopters, partners, or the community ### **Support and reliability** * Best-effort support * Not recommended for mission-critical production workloads ## **Stage 2: Public Preview** **Public Preview** makes the product available to a broader audience for real-world validation at scale. Some customers may also choose to run production workloads during this phase, depending on their risk tolerance and use case. ### **Access and pricing** * Access: Public * Pricing: **May be free or paid**, depending on the product and underlying infrastructure or operational costs Pricing during Public Preview varies by product and is always clearly communicated before use. ### **What you can expect** * A visible **“Preview”** label * Stable and reliable for real-world usage * Suitable for **production workloads**, with the understanding that: * Features may still evolve * Breaking changes are unlikely but possible * Onboarding, documentation, and self-serve flows are available * Some features may still be under active development ### **Support and reliability** * Support is available through standard or community channels * Feedback continues to drive prioritization and improvements *** ## **Stage 3: General Availability (GA)** **General Availability (GA)** represents the full public launch of a product. At this stage, the product is production-ready and fully supported for long-term use. ### **Access and pricing** * Access: Public * Pricing: Paid (standard pricing applies) ### **What you can expect** * A stable, production-grade product * Fully finalized documentation and APIs * Mature onboarding, monitoring, and operational tooling * Designed for broad adoption and long-term scalability ### **Support and status** * Support is handled through established customer support channels * Service health and incidents are communicated via the status page when applicable *** ## **Release labeling** We use the following labels to clearly communicate product maturity: * **Preview**: Applies to Closed Preview and Public Preview releases * **GA**: Indicates the product is production-ready with full support # Get started with bunny.net Source: https://bunny.net/docs/quickstart It's free to get started. ### Create an account Your free trial includes **\$20 in trial credits** with no payment card required. Add billing information to unlock an additional **\$30 in trial credits** for a total of \$50. Trial credits expire at the end of your trial and any unused balance is removed. [Sign-up](https://dash.bunny.net/auth/register) and register for a free trial account. Sign up page You must use the link in the email sent to you. Once done, you'll be able to create and manage as many resources in your account as you need during the trial. Email confirmation prompt After confirming your email, you can claim an additional **\$30 in credits** by adding your billing information to your account. Claim trial credits This step is optional but highly recommended, as it provides even more value during your trial period. To add billing information, click on **Claim \$30 trial credits** and fill in the required information: Billing information form With a total of \$50 in credits, you'll be able to explore all the powerful features of the bunny.net platform. Your dashboard includes a handy countdown timer that tracks the remaining days of your trial. This feature helps you plan your usage effectively, ensuring you make the most of your trial period. Trial countdown timer ### Trial balance notifications We will send you email alerts when your trial balance reaches key thresholds: * **Unverified accounts**: Notifications at \$10 and \$1 remaining * **Verified accounts** (with payment card): Notifications at \$20 and \$5 remaining All services are charged at our standard rates and deducted from your trial balance. If you add funds to your account, charges will continue to be deducted from your trial balance first until it is exhausted or expired. ### When your trial expires Once your 14-day trial expires, any remaining trial credits are removed from your account. Trial credits cannot be carried over or converted to paid credits. Trial expired However, you can continue enjoying bunny.net's services by clicking the **Recharge Your Account** button and adding funds. ### Upgrading to a paid account You can add funds to your account anytime during or after the trial. Even after adding funds, you will continue to use your trial credits until they expire, ensuring a seamless transition. When you top up your account, the full amount you add will be credited to your balance, regardless of your negative trial balance. To reactivate your account, simply add funds to bring your balance positive. ### Refer your friends and earn credits The bunny.net platform also offers an exciting **refer-a-friend program** that rewards you for sharing the platform with others. Referral program For every friend you refer who signs up and becomes a paying customer, you'll earn **\$20 in credits**. There's no limit to how many friends you can refer—more referrals mean more credits for you! To participate, share your **unique referral link** (available in your dashboard) with friends, colleagues, or anyone who could benefit from bunny.net's services. # API Reference Source: https://bunny.net/docs/scripting/api-reference Complete API reference for deploying and managing Edge Scripts programmatically. # Before cache execution Source: https://bunny.net/docs/scripting/before-cache Run your edge scripts before the cache layer to take control of every request, instead of only executing when the cache is missed. By default, edge scripts run **after** the cache. The CDN serves cached responses directly, without invoking your script, so cached traffic is not slowed down by script execution. With before cache execution enabled, your script runs on **every request**, before the cache lookup. This gives you full control over the request flow at the cost of executing your script even for cached content. Post-cache execution remains the default. Enable before cache execution when you need per-request logic, such as authentication, tenant-aware routing, or custom caching rules. ## Enable before cache execution Before cache execution is enabled at the Pull Zone level and applies to the script linked to that Pull Zone. Navigate to your Pull Zone, then go to **General** > **Origin** and enable **Run script before cache**. For standalone scripts, you can use the same Pull Zone toggle or enable it directly in the script settings. ## Middleware scripts When before cache execution is enabled, middleware scripts gain access to two additional hooks that run on the client side of the cache: * **`onClientRequest`**: runs on every incoming request before the cache lookup. Modify the request or short-circuit by returning a response directly. * **`onClientResponse`**: runs just before the response is sent to the client, including responses served from the cache. ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk@0.13.0-rc.0"; BunnySDK.net.http .servePullZone({ url: "https://your-origin.com" }) .onClientRequest((ctx) => { // Runs before the cache lookup on every request return Promise.resolve(ctx.request); }) .onClientResponse((ctx) => { // Runs before the response is sent to the client return Promise.resolve(ctx.response); }); ``` See [Middleware scripts](/docs/scripting/middleware/overview) for the full hook reference and workflow. ## Standalone scripts Standalone scripts act as the origin for their Pull Zone, so by default they only execute when the cache is missed. With before cache execution enabled, your script handles **every request** before the cache lookup, and decides whether and how responses are cached. See [Standalone scripts](/docs/scripting/standalone/overview) for details. ## Workflow ```mermaid theme={null} sequenceDiagram participant Client participant Script participant Cache participant Origin Client->>Script: Request Script->>Cache: Cache lookup Cache->>Origin: On cache MISS Origin-->>Cache: Response Cache-->>Script: Response Script-->>Client: Response ``` # Cache examples Source: https://bunny.net/docs/scripting/cache/examples End-to-end Cache API recipes for Edge Scripting. ## Quickstart This example caches an HTML page at the edge, then rewrites part of it per request from a query parameter. ```typescript theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve( async (request: Request): Response | Promise => { const url = new URL(request.url); const now = new Date(); const expires = new Date(now.getTime() + 60 * 1000); // + 1min try { const cache = await caches.open("cache:v1"); // 1. check to see if we already have a cache entry let res = await cache.match(`${url.origin}/example.html`); // 2. if we don't, create one and put it into the cache for 60 seconds if (!res) { res = new Response( `

Hello

  • created at: ${now.toISOString()}
  • expires at: ${expires.toISOString()}
`, { headers: { "Cache-Control": "max-age=60", "Content-Type": "text/html; charset=utf-8", }, }, ); await cache.put(`${url.origin}/example.html`, res.clone()); } // 3. modify the contents const content = new HTMLRewriter() .on("span", { element(el) { el.setInnerContent( url.searchParams.get("name") ?? "Nameless User (use ?name= to define this)", ); }, }) .transform(res); // 3. return modified contents to the user return new Response(await content.text(), { headers: { "Cache-Control": "no-cache", "Content-Type": "text/html; charset=utf-8", }, }); } catch (error) { return new Response(`Error: ${error.message}`, { status: 500, }); } }, ); ``` ## Updating a cache in middleware This example writes to the cache from middleware when the incoming request is a POST, and reads the entry back from a standalone script. ```typescript theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; async function onOriginRequest(context: { request: Request; }): Promise | Response | Promise | Request | void { const now = new Date(); try { const cache = await caches.open("cache:v1"); if (context.request.method == "POST") { const res = new Response(`Content from POST: ${now.toISOString()}`, { headers: { "Cache-Control": "max-age=3600", "Content-Type": "text/plain; charset=utf-8", }, }); Bunny.v1.waitUntil(cache.put(context.request, res)); } } catch (e) { console.log(e); } } BunnySDK.net.http.servePullZone().onOriginRequest(onOriginRequest); ``` ```typescript theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve( async (request: Request): Response | Promise => { try { const cache = await caches.open("cache:v1"); let res = await cache.match(request); if (!res) { const res = Response(await "Content from GET", { headers: { "Cache-Control": "no-cache", "Content-Type": "text/plain; charset=utf-8", }, }); } return res; } catch (error) { return new Response(`Error: ${error.message}`, { status: 500, }); } }, ); ``` ## Refresh and purge a cached value on demand Expose a single URL that serves a cached JSON payload on `GET`, regenerates it on `POST`, and clears it on `DELETE`. Useful for cache-warming endpoints, manual invalidation hooks, or scheduled refreshes from an upstream job. ```typescript theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk@0.12.0"; BunnySDK.net.http.serve(async (request: Request): Promise => { const url = new URL(request.url); const cache = caches.default; // Share one key across all three methods by normalizing the request to a // GET on the same URL. const cacheKey = new Request(url.toString(), { method: "GET" }); try { if (request.method === "POST") { const fresh = Response.json( { generatedAt: new Date().toISOString(), random: Math.random(), }, { headers: { "Cache-Control": "s-maxage=60" }, }, ); await cache.put(cacheKey, fresh.clone()); return new Response(`Cached "${cacheKey.url}" for 60s`, { headers: { "Cache-Control": "no-cache" }, }); } if (request.method === "GET") { const hit = await cache.match(cacheKey); if (hit) { return hit; } return new Response( "No cached value yet. POST to this URL to generate one.", { status: 404, headers: { "Cache-Control": "no-cache" } }, ); } if (request.method === "DELETE") { const deleted = await cache.delete(cacheKey); return new Response( deleted ? `Purged "${cacheKey.url}"` : `Nothing to purge for "${cacheKey.url}"`, { status: deleted ? 200 : 404, headers: { "Cache-Control": "no-cache" }, }, ); } return new Response("Method Not Allowed", { status: 405, headers: { "Allow": "GET, POST, DELETE", "Cache-Control": "no-cache", }, }); } catch (e) { return new Response(`Cache error: ${(e as Error).message}`, { status: 500 }); } }); ``` The POST handler awaits `cache.put()` so the acknowledgement reflects that the value actually landed. For fire-and-forget refreshes, wrap it with `Bunny.v1.waitUntil(cache.put(cacheKey, fresh.clone()))` and return the ack immediately. ## References * [Cache API overview](./index): regional behavior, limitations, and API surface. * [Managing caches](./managing-caches): the global `caches` object and named cache instances. * [Reading & writing](./reading-writing): the `match`, `put`, and `delete` method reference. * [Runtime: waitUntil](../runtime#waituntil): extend the isolate lifetime for background cache writes. # Cache API Source: https://bunny.net/docs/scripting/cache/index Fine-grained control over reading and writing from the bunny.net edge cache from Edge Scripting. The Cache API is an implementation of the [MSDN Cache Interface](https://developer.mozilla.org/en-US/docs/Web/API/Cache). It stores Request and Response pairs in long lived memory. The API is available globally, but cache contents do not replicate outside the originating region. A GET `/users` response cached in one region will not exist in another until a request for that resource is made from there. An origin can have multiple, named Cache objects, and it is up to your script to decide how cache updates happen. Items in a Cache respect the `Cache-Control` request header, which is how you control the life cycle of your caches. Entries are purged automatically a short time after they expire. Version your caches by name, and only use a cache from a version of the script that can safely operate on it. Cache instances are shared across all domains associated with your PullZone, but they **must** be accessed via the currently requesting host name. Use the current `Request` object as the key, or build the key from the current request URL, for example `const key = new URL(req.url).origin + "/img/example.png";`. Either way your caches will work reliably across every domain on the PullZone. **Note:** There is a hard limit of `100MB` per cache file. ## Limitations API surface limits: * **`CacheStorage`** (the global `caches` object) * Supported: `caches.default`, `caches.open(name)` * Not supported: `caches.has`, `caches.delete`, `caches.keys`, `caches.match` * **`Cache`** (an instance returned by `caches.default` or `caches.open`) * Supported: `match`, `put`, `delete` * Not supported: `matchAll`, `add`, `addAll`, `keys` We recommend versioning your cache names (e.g. `cache:v1`, `cache:v2`) so that a cache is completely purged when updating scripts. ## Quickstart A minimal cache-aside pattern: look up by URL, generate on miss, write back in the background. ```typescript theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk@0.12.0"; BunnySDK.net.http.serve(async (request: Request): Promise => { const url = new URL(request.url); try { // Normalize the key to a GET on the request URL so reads and writes // resolve to the same entry regardless of the inbound method. const cacheKey = new Request(url.toString(), { method: "GET" }); const cache = caches.default; const cached = await cache.match(cacheKey); if (cached) { console.log(`Cache hit for: ${request.url}.`); return cached; } console.log(`Cache miss for: ${request.url}. Generating and caching.`); // In a real script this is typically `await fetch(originUrl)`. const response = Response.json( { value: Math.random() }, { headers: { "Cache-Control": "s-maxage=10" } }, ); // Fire-and-forget the write so we can return immediately. Bunny.v1.waitUntil(cache.put(cacheKey, response.clone())); return response; } catch (e) { return new Response(`Cache error: ${(e as Error).message}`, { status: 500 }); } }); ``` See [Examples](./examples) for longer recipes: HTMLRewriter integration, middleware writes, and on-demand refresh and purge. ## References * [MSDN CacheStorage](https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage) - Mozilla's CacheStorage interface documentation * [MSDN Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache) - Mozilla's Cache interface documentation # Managing caches Source: https://bunny.net/docs/scripting/cache/managing-caches The global caches object, covering the default cache and how to open named caches. ## Default cache The `caches.default` API follows the web browsers' Cache API interface closely. It gives you a single, unnamed cache shared across requests. ```typescript theme={null} await caches.default.match(request); ``` ## Named caches You may create and manage additional Cache instances via the `caches.open` method. Named caches are useful when you want to version cache contents (e.g. `cache:v1`, `cache:v2`) so a deploy can switch over cleanly. ```typescript theme={null} const cache = await caches.open("cache:v1"); await cache.match(request); ``` **Note:** When using the Cache API, avoid overriding the hostname in cache requests, as this can lead to unexpected behaviour when using multiple domains associated with a PullZone. ```typescript theme={null} // recommended approach: use current request url const url = new URL(request.url); const cache = await caches.open("cache:v1"); const response = await cache.match(`${url.origin}/example.html`); ``` ## References * [Reading & writing](./reading-writing): the `match`, `put`, and `delete` method reference. * [Examples](./examples): end-to-end recipes using named and default caches. * [Cache API overview](./index): regional behavior, limitations, and API surface. # Reading & writing Source: https://bunny.net/docs/scripting/cache/reading-writing Reading from, writing to, and deleting entries on a Cache instance. ## Cache-Control behavior Cache entry lifetimes are controlled via the `Cache-Control` header on the response you pass to `put()`. The same directives that govern HTTP caching apply here. For the full directive list, see [Cache-Control on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control). ```typescript theme={null} await cache.put( `${url.origin}/example.html`, new Response("content", { headers: { "Cache-Control": "max-age=60" }, }), ); ``` This caches the string `content` under the key `/example.html` for 60 seconds. ## Instance methods ### Delete The `delete()` method finds the Cache entry whose key matches the request and removes it, returning `true` if an entry was found and deleted, or `false` if there was no matching entry. #### Syntax ```typescript theme={null} await cache.delete(request); ``` Unlike the browser Cache API, Bunny Edge Scripting does not support the options bag (`ignoreSearch`, `ignoreMethod`, `ignoreVary`). To get equivalent behaviour, normalize the key at `put()` time, for example by stripping query strings from the URL before calling `put()`. #### Parameters `request` The Request you are looking to delete. This can be a Request object or a URL. #### Return Value A `Promise` that resolves to `true` if a matching entry was found and removed, or `false` if no entry existed. After the promise resolves, a subsequent `cache.match()` for the same key will miss. #### Exceptions Can throw an exception if there is an issue accessing the underlying cache storage. #### Example ```typescript theme={null} const cache = await caches.open("cache:v1"); const deleted = await cache.delete(`${url.origin}/example.html`); ``` See [Refresh and purge a cached value on demand](./examples#refresh-and-purge-a-cached-value-on-demand) for a full request handler that exposes `DELETE` as a purge endpoint. ### Match The `match()` method of the Cache returns a Promise that resolves to the Response associated with the first matching request in the Cache object. If no match is found, the Promise resolves to `undefined`. #### Syntax ```typescript theme={null} await cache.match(request); ``` Unlike the browser Cache API, Bunny Edge Scripting does not support the options bag (`ignoreSearch`, `ignoreMethod`, `ignoreVary`). You can accomplish equivalent behaviour by removing query strings or unwanted headers from the key at `put()` time. #### Parameters `request` The Request for which you are attempting to find responses in the Cache. This can be a Request object or a URL. #### Return Value A `Promise` that resolves to the first `Response` that matches the request or to `undefined` if no match is found. #### Exceptions Can throw an exception if there is an issue accessing the underlying cache storage. #### Example ```typescript theme={null} const cache = await caches.open("cache:v1"); const hit = await cache.match(`${url.origin}/example.html`); if (hit) return hit; ``` See [Quickstart](./examples#quickstart) for a full handler that pairs a `match()` lookup with an `HTMLRewriter` transform. ### Put The `put()` method of the Cache allows key/value pairs to be added to the current Cache object. #### Syntax ```typescript theme={null} await cache.put(request, response); ``` #### Parameters `request` The Request object or URL that you want to add to the cache. `response` The Response you want to match up to the request. #### Return Value A `Promise` that resolves with `undefined`. #### Exceptions Can throw an exception if there is an issue accessing the underlying cache storage. #### Example ```typescript theme={null} const cache = await caches.open("cache:v1"); // clone() so the response body can still be returned to the caller. await cache.put(`${url.origin}/example.html`, response.clone()); ``` See [Updating a cache in middleware](./examples#updating-a-cache-in-middleware) for a full handler that populates the cache in response to origin requests. ## References * [Examples](./examples): end-to-end recipes for cache-aside, middleware writes, and refresh and purge. * [Managing caches](./managing-caches): the global `caches` object and named cache instances. * [Cache API overview](./index): regional behavior, limitations, and API surface. # Changelog Source: https://bunny.net/docs/scripting/changelog Latest updates and improvements to Edge Scripting. ## Connect a Bunny Database from the Edge Scripting creation flow You can now connect a Bunny Database directly from the Edge Scripting creation flow. Create a new database or pick an existing one, and we'll automatically add the connection credentials (`BUNNY_DATABASE_URL` and `BUNNY_DATABASE_AUTH_TOKEN`) as script secrets. There's also a new Database CRUD API script template available to get you started. Add Database ## Before cache execution (Preview) Edge Scripts can now run **before** the CDN cache layer, giving you full control over every request instead of only executing on cache misses. Enable it per Pull Zone under **General** > **Origin** > **Run script before cache**. Middleware scripts gain two new hooks: `onClientRequest` (intercept requests before the cache lookup) and `onClientResponse` (modify responses before they reach the client, including cached ones). Standalone scripts also support before cache execution, running on every request and deciding how responses are cached. [Learn more](/docs/scripting/before-cache) ## Symbolic links, timestamps, and permissions support The Node.js file system module now supports symbolic links (`symlink()`, `readlink()`, `lstat()`), returns correct file timestamps (`atime`, `mtime`, `ctime`, `birthtime`), and enforces owner-level file and directory permissions (`chmod()`, `access()`, `Stats.mode`). [Learn more](/docs/scripting/node-fs) ## Cache API documentation reorganized into dedicated section The Cache API documentation has been restructured from a single page into a dedicated multi-page section with separate pages for the [API overview and limitations](/docs/scripting/cache), [managing default and named caches](/docs/scripting/cache/managing-caches), [reading and writing cache entries](/docs/scripting/cache/reading-writing) (`match`, `put`, `delete`), and [end-to-end examples](/docs/scripting/cache/examples) including a new on-demand refresh and purge recipe. The [`waitUntil` runtime reference](/docs/scripting/runtime#waituntil) has also been expanded with a full API signature and a cache-population example. ## Runtime API and `waitUntil` New Runtime API reference documenting the `waitUntil` function, which extends script isolate lifetime after a request finishes. It is useful for keeping WebSocket connections alive and running background work. The WebSocket guide now includes a `waitUntil` usage example. [Learn more](/docs/scripting/runtime) ## Node.js file system API A Node.js-compatible file system API is now available in Edge Scripts, allowing you to read and write files directly at the edge. [Learn more](/docs/scripting/node-fs) ## Node.js file system API A Node.js-compatible file system API is now available in Edge Scripts, allowing you to read and write files directly at the edge. [Learn more](/docs/scripting/node-fs) # Deployments Source: https://bunny.net/docs/scripting/deployments Track the published versions of your script and roll back to an earlier one when you need to. A deployment is a published version of your script. Every time you publish, a new deployment is created, so you keep a record of what changed and can go back to an earlier version if a release causes problems. Your team can see the same history. ## Accessing the deployments view To view your deployments: 1. Log in to the [bunny.net dashboard](https://dash.bunny.net/auth/login?pk_buttonlocation=menu). 2. Select **Edge Platform**, click on **Scripting**, and select the script. 3. Open the **Deployments** tab and select **Latest**. The current deployment is listed alongside every previous one. ## Publishing previous deployments To revert to an earlier version: 1. Open the **Deployments** tab and select **Latest**. 2. Click **Publish** next to the version you want to deploy. # Environment Variables Source: https://bunny.net/docs/scripting/environment-variables Environment variables are used to store information or configuration data that your script requires at runtime. Environment variables hold configuration data that your script reads at runtime. Because the values live outside the code, they never end up committed to your script or your version control system. For sensitive data such as API keys, passwords, or tokens, use [environment secrets](/docs/scripting/secrets) instead. Secrets are encrypted and cannot be viewed once set. ## Adding environment variables To add a variable to your script: 1. Log in to the [bunny.net dashboard](https://dash.bunny.net/auth/login?pk_buttonlocation=menu). 2. Select **Edge Platform**, click on **Scripting**, and select the script. 3. Select **Env Configuration** and **Environment Variables**, and fill in the details of the variable you want to create. 4. Click **Save**. ## Using environment variables Read the value in your script with either the Node.js `process` module or `Deno.env`. This example uses the `process` module: ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; import process from "node:process"; BunnySDK.net.http.serve( async (request: Request): Response | Promise => { // Load environment variable named OriginUrl const originUrl = process.env.OriginUrl; // Parse initial url const url = new URL(request.url); // Rewrite and fetch request from origin return fetch(originUrl + url.pathname); }, ); ``` This one does the same thing with `Deno.env`: ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve( async (request: Request): Response | Promise => { // Load environment variable named OriginUrl const originUrl = Deno.env.get("OriginUrl"); // Parse initial url const url = new URL(request.url); // Rewrite and fetch request from origin return fetch(originUrl + url.pathname); }, ); ``` # Deploying your scripts with GitHub Actions Source: https://bunny.net/docs/scripting/github-actions Use the bunny.net deploy action to publish an Edge Script from a GitHub repository whenever your code changes. The `BunnyWay/actions/deploy-script` action uploads a script to Edge Scripting as part of a GitHub workflow, so a push to the branch you choose publishes the script without anyone having to upload it by hand. ## Example usage This workflow deploys a script whenever something is pushed to the main branch: ```yml theme={null} name: Deploy when pushing on main on: push: branches: - "main" jobs: publish: runs-on: ubuntu-latest name: "Upload script" steps: - name: Checkout repository uses: actions/checkout@v4 - name: Deploy Script to Bunny Edge Scripting uses: BunnyWay/actions/deploy-script@main with: script_id: ${{ secrets.SCRIPT_ID }} deploy_key: ${{ secrets.DEPLOY_KEY }} file: "script.ts" ``` ## Inputs The action takes three inputs: * `script_id` (required): the ID of the script you want to deploy. Store it as a GitHub secret (`SCRIPT_ID`). * `deploy_key` (required): the key that authorizes the deployment. Store it as a GitHub secret (`DEPLOY_KEY`). * `file` (required): the path of the script file to deploy. The file has to exist in your repository, or an earlier step in the workflow has to generate it. The script ID and deploy key are available in the dashboard under **Script** -> **Deployments** -> **Settings**. Place the workflow file in the `.github/workflows` folder in your repository, and adjust it to fit your own build and deployment pipeline. The [official GitHub documentation](https://docs.github.com/en/actions/writing-workflows) covers workflow syntax in full. ## Setting up secrets To store the `script_id` and `deploy_key` as secrets in your GitHub repository: 1. Navigate to your GitHub repository. 2. Click on **Settings**. 3. Select **Secrets and variables**. 4. Choose **Actions**. 5. Click **New repository secret**. 6. Add `SCRIPT_ID` and `DEPLOY_KEY` as the names of the secrets and provide their respective values. # GitHub Integration Source: https://bunny.net/docs/scripting/github-integration Keep your Edge Script code in a GitHub repository and deploy it with GitHub Actions. The GitHub integration lets you keep your Edge Script code in a GitHub repository instead of the dashboard editor. You get version control, and you can run the script through GitHub Actions before it ships: builds, tests, minification, security scanning, or anything else your pipeline needs. This suits larger applications, where the script is built from multiple files or prebuilt resources rather than written in a single editor tab. ## What you'll need * A [bunny.net](https://bunny.net/) account ([log in](https://dash.bunny.net/auth/login?pk_buttonlocation=menu) or sign up for a [free trial](https://dash.bunny.net/auth/register)). * A GitHub account. ## Creating a new repository Start here if the project is new and you want to base it on a bunny.net template. 1. Login to the bunny.net dashboard. 2. Navigate to **Edge Platform** > **Scripting** and click **Add Script**. 3. Choose where the code lives. **Deploy and edit on Bunny.net** lets you write code directly in the browser and suits simple scripts. Select **Deploy and edit with GitHub** to keep the code in a GitHub repository. 4. In the second step of script creation, fill in the **Script Name**. 5. Select between Standalone or Middleware script type. For more information, see our [Script Types documentation](/docs/scripting). 6. Use the Repository section to connect the script with the GitHub repository. If you haven't connected your GitHub account yet, click Connect GitHub Account. 7. You will be redirected to the GitHub application permission page, where you can select the repositories that bunny.net can access. 8. Select **New Repository** to create a preconfigured repository based on a template. 9. Choose a script template. Each template is a starting setup for a different scenario. For demo purposes, you can choose the **Default empty script**. 10. Click **Add Script**. A new GitHub repository is created from the template, along with a workflow pipeline that deploys the build output to bunny.net Scripting. The pipeline runs straight away. This takes a few seconds, and you can watch it in the repository's Actions tab. 11. Your script is now live and ready to be invoked. The provided templates are starting points for implementing your own application. Use the documentation available within the GitHub repository to learn how to extend it with your application logic, building custom functionality and features as needed. Once the script is live, the bunny.net dashboard shows how it is doing. You can see the number of requests it has served in real time, along with your production deployment: timestamps, version history, and deployment status. Execution logs are there too, which is where you go when something is failing or slower than you expected. Settings, script updates, and redeployment all happen from the same place. ## Using an existing repository Use this option when you already have a JavaScript or TypeScript project you want to deploy to Edge Scripting. The repository should be compatible with Edge Scripting, ideally built on Deno or Node.js, and structured so it can integrate with the Edge Scripting deployment GitHub action. Setup matches the previous section. **Follow the steps in Creating a new repository up to Step 7**, then continue here: 1. Select **Existing Repository**. 2. Choose a repository from the list of your existing GitHub repositories. If the repository is not listed, ensure that the bunny.net GitHub app has the appropriate permissions. 3. Next, select a project preset based on the framework used in your project. If needed, adjust the **install command, build command**, and **entry file parameters**. A deployment workflow file will be automatically created in your existing repository based on these parameters. If you prefer to create the deployment workflow file yourself, select the **I will create the GitHub workflow file myself** option. 4. Your script is now live and ready to be invoked. The dashboard gives you the same view as it does for a new repository: live request counts, deployment details and version history, execution logs, and the settings and redeploy controls. # HTMLRewriter Source: https://bunny.net/docs/scripting/html-rewriter Transform HTML responses on the edge with streaming, selector-based rewriting. ## Overview `HTMLRewriter` lets you modify HTML responses as they stream through your edge script. It parses the HTML on the fly and calls your handler functions when it meets matching elements, comments, or text, so the whole document never has to be buffered in memory. ## Quickstart A short middleware example: ```js theme={null} import * as BunnySDK from "https://esm.sh/@bunny.net/edgescript-sdk@0.11.2"; BunnySDK.net.http.servePullZone() .onOriginResponse(async ({ response }) => { return new HTMLRewriter() .on("h1", { element(el) { el.setInnerContent("Modified Title"); }, }) .transform(response); }); ``` ## Constructor ```js theme={null} new HTMLRewriter(options?) ``` When `true`, enables processing of ESI ([Edge Side Includes](https://www.w3.org/TR/esi-lang)) tags like ``. ## Methods ### `on(selector, handlers)` Registers handlers for elements matching a CSS selector. Returns `this` for chaining. ```js theme={null} rewriter.on("div.content", { element(el) { /* ... */ }, comments(comment) { /* ... */ }, text(text) { /* ... */ }, }); ``` A CSS selector. See [supported selectors](#supported-css-selectors) below. An object with optional handler functions. Each handler can be sync or async. Called when an opening tag matching the selector is encountered. Called for HTML comments within the matched element. Called for text content within the matched element. ### `onDocument(handlers)` Registers document-level handlers. Returns `this` for chaining. ```js theme={null} rewriter.onDocument({ doctype(doctype) { /* ... */ }, comments(comment) { /* ... */ }, text(text) { /* ... */ }, end(end) { /* ... */ }, }); ``` Called when the `` declaration is encountered. Called for document-level comments (outside any element scope). Called for document-level text. Called when the end of the document is reached. ### `transform(response)` Applies all registered handlers to the response body and returns a new `Response`. ```js theme={null} const transformed = rewriter.transform(response); ``` The HTTP response to transform. Must not be an error response. **Returns:** A new `Response` with: * The same headers (minus `Content-Length`, since the body length may change) * A streaming body with the transformed HTML *** ## Handler types ### Element Passed to `element` handlers. Represents an HTML opening tag. #### Properties | Property | Type | Description | | -------------- | ------------------------------------ | ------------------------------------------------ | | `tagName` | `string` | Tag name (lowercase). Readable and writable. | | `namespaceURI` | `string` | Namespace URI (readonly). | | `removed` | `boolean` | Whether the element has been removed (readonly). | | `attributes` | `IterableIterator<[string, string]>` | Iterable of `[name, value]` pairs (readonly). | #### Attribute methods ##### `getAttribute(name)` Returns the value of the attribute with the given name, or `null` if the attribute does not exist. The attribute name. **Returns:** `string | null` ##### `hasAttribute(name)` Returns whether the element has an attribute with the given name. The attribute name. **Returns:** `boolean` ##### `setAttribute(name, value)` Sets the value of the attribute with the given name. Adds the attribute if it does not exist. The attribute name. The attribute value. **Returns:** `Element`, the element itself, for chaining. ##### `removeAttribute(name)` Removes the attribute with the given name. No-op if the attribute does not exist. The attribute name. **Returns:** `Element`, the element itself, for chaining. Setters return the element itself, so calls can be chained: ```js theme={null} el.setAttribute("class", "new") .setAttribute("id", "main") .removeAttribute("style"); ``` #### Content mutation methods All content mutation methods accept `content` as a `string`, `ReadableStream`, or `Response`, and an optional `options` object. They all return `Element` for chaining. The content to insert. Strings are inserted directly. Streams and Response bodies are consumed and piped into the output. When `true`, content is inserted as raw HTML. When `false`, content is escaped as text. ##### `before(content, options?)` Inserts content immediately before the element's opening tag. **Returns:** `Element` ##### `after(content, options?)` Inserts content immediately after the element's closing tag. **Returns:** `Element` ##### `prepend(content, options?)` Inserts content at the beginning of the element, right after the opening tag. **Returns:** `Element` ##### `append(content, options?)` Inserts content at the end of the element, right before the closing tag. **Returns:** `Element` ##### `replace(content, options?)` Replaces the entire element (opening tag, content, and closing tag) with the provided content. **Returns:** `Element` ##### `setInnerContent(content, options?)` Replaces the element's inner content, keeping the opening and closing tags. **Returns:** `Element` ```js theme={null} el.before("
", { html: true }) .setInnerContent("Hello") .after("
", { html: true }); ``` #### Removal methods ##### `remove()` Removes the element and all of its content (opening tag, children, closing tag). **Returns:** `Element` ##### `removeAndKeepContent()` Removes the element's opening and closing tags but keeps the inner content in place. **Returns:** `Element` #### End tag handler ##### `onEndTag(handler)` Registers a handler that is called when the element's closing tag is encountered. A callback receiving the [`EndTag`](#endtag) object. Can be async. **Returns:** `void` ```js theme={null} el.onEndTag((endTag) => { endTag.before("
", { html: true }); }); ``` ### Comment Passed to `comments` handlers. #### Properties | Property | Type | Description | | --------- | --------- | ------------------------------------------------------------------ | | `text` | `string` | The comment text, without ``. Readable and writable. | | `removed` | `boolean` | Whether the comment has been removed (readonly). | #### Methods ##### `before(content, options?)` Inserts content immediately before the comment. The content to insert. When `true`, content is inserted as raw HTML. When `false`, content is escaped as text. **Returns:** `Comment` ##### `after(content, options?)` Inserts content immediately after the comment. The content to insert. When `true`, content is inserted as raw HTML. When `false`, content is escaped as text. **Returns:** `Comment` ##### `replace(content, options?)` Replaces the comment with the provided content. The content to replace with. When `true`, content is inserted as raw HTML. When `false`, content is escaped as text. **Returns:** `Comment` ##### `remove()` Removes the comment from the document. **Returns:** `Comment` ### TextChunk Passed to `text` handlers. Note: a single text node may be split across multiple chunks. #### Properties | Property | Type | Description | | ---------------- | --------- | ------------------------------------------------------------- | | `text` | `string` | The text content (readonly). | | `lastInTextNode` | `boolean` | `true` if this is the last chunk in the text node (readonly). | | `removed` | `boolean` | Whether this chunk has been removed (readonly). | #### Methods ##### `before(content, options?)` Inserts content immediately before the text chunk. The content to insert. When `true`, content is inserted as raw HTML. When `false`, content is escaped as text. **Returns:** `TextChunk` ##### `after(content, options?)` Inserts content immediately after the text chunk. The content to insert. When `true`, content is inserted as raw HTML. When `false`, content is escaped as text. **Returns:** `TextChunk` ##### `replace(content, options?)` Replaces the text chunk with the provided content. The content to replace with. When `true`, content is inserted as raw HTML. When `false`, content is escaped as text. **Returns:** `TextChunk` ##### `remove()` Removes the text chunk from the document. **Returns:** `TextChunk` ### EndTag Passed to `onEndTag` handlers. #### Properties | Property | Type | Description | | -------- | -------- | ---------------------------------------- | | `name` | `string` | The end tag name. Readable and writable. | #### Methods ##### `before(content, options?)` Inserts content immediately before the end tag. The content to insert. When `true`, content is inserted as raw HTML. When `false`, content is escaped as text. **Returns:** `EndTag` ##### `after(content, options?)` Inserts content immediately after the end tag. The content to insert. When `true`, content is inserted as raw HTML. When `false`, content is escaped as text. **Returns:** `EndTag` ##### `remove()` Removes the end tag from the document. **Returns:** `EndTag` ### Doctype Passed to `doctype` handlers. All properties are read-only. #### Properties | Property | Type | Description | | ---------- | ---------------- | --------------------------------- | | `name` | `string \| null` | The doctype name (e.g. `"html"`). | | `publicId` | `string \| null` | The PUBLIC identifier. | | `systemId` | `string \| null` | The SYSTEM identifier. | ### DocumentEnd Passed to `end` handlers. #### Methods ##### `append(content, options?)` Appends content at the end of the document. The content to append. Only accepts `string` (not streams or responses). When `true`, content is inserted as raw HTML. When `false`, content is escaped as text. **Returns:** `DocumentEnd` ```js theme={null} end.append("", { html: true }); ``` *** ## Supported CSS selectors The following CSS selectors are supported, based on the [W3C Selectors Level 4](https://www.w3.org/TR/selectors-4/) specification. | Selector | Description | Spec | | ------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------- | | `*` | Any element | [Universal selector](https://www.w3.org/TR/selectors-4/#the-universal-selector) | | `E` | Element of type `E` | [Type selector](https://www.w3.org/TR/selectors-4/#type-selectors) | | `E.class` | Element with class | [Class selector](https://www.w3.org/TR/selectors-4/#class-html) | | `E#id` | Element with ID | [ID selector](https://www.w3.org/TR/selectors-4/#id-selectors) | | `E:nth-child(n)` | The n-th child of its parent | [:nth-child()](https://www.w3.org/TR/selectors-4/#the-nth-child-pseudo) | | `E:first-child` | First child of its parent | [:first-child](https://www.w3.org/TR/selectors-4/#the-first-child-pseudo) | | `E:nth-of-type(n)` | The n-th sibling of its type | [:nth-of-type()](https://www.w3.org/TR/selectors-4/#the-nth-of-type-pseudo) | | `E:first-of-type` | First sibling of its type | [:first-of-type](https://www.w3.org/TR/selectors-4/#the-first-of-type-pseudo) | | `E:not(s)` | Element that does not match compound selector `s` | [:not()](https://www.w3.org/TR/selectors-4/#negation) | | `E[attr]` | Element with attribute `attr` | [Attribute selector](https://www.w3.org/TR/selectors-4/#attribute-selectors) | | `E[attr="value"]` | Attribute exactly equals `value` | [Attribute selector](https://www.w3.org/TR/selectors-4/#attribute-selectors) | | `E[attr="value" i]` | Case-insensitive attribute match | [Case sensitivity](https://www.w3.org/TR/selectors-4/#attribute-case) | | `E[attr="value" s]` | Case-sensitive attribute match | [Case sensitivity](https://www.w3.org/TR/selectors-4/#attribute-case) | | `E[attr~="value"]` | Whitespace-separated list containing `value` | [Attribute selector](https://www.w3.org/TR/selectors-4/#attribute-selectors) | | `E[attr^="value"]` | Attribute starts with `value` | [Attribute selector](https://www.w3.org/TR/selectors-4/#attribute-selectors) | | `E[attr$="value"]` | Attribute ends with `value` | [Attribute selector](https://www.w3.org/TR/selectors-4/#attribute-selectors) | | `E[attr*="value"]` | Attribute contains `value` | [Attribute selector](https://www.w3.org/TR/selectors-4/#attribute-selectors) | | `E[attr\|="value"]` | Hyphen-separated attribute starting with `value` | [Attribute selector](https://www.w3.org/TR/selectors-4/#attribute-selectors) | | `E F` | `F` descendant of `E` | [Descendant combinator](https://www.w3.org/TR/selectors-4/#descendant-combinators) | | `E > F` | `F` direct child of `E` | [Child combinator](https://www.w3.org/TR/selectors-4/#child-combinators) | *** ## Examples ### Rewrite links ```js theme={null} import * as BunnySDK from "https://esm.sh/@bunny.net/edgescript-sdk@0.11.2"; BunnySDK.net.http.servePullZone() .onOriginResponse(async ({ response }) => { return new HTMLRewriter() .on("a[href]", { element(el) { const href = el.getAttribute("href"); if (href?.startsWith("http://")) { el.setAttribute("href", href.replace("http://", "https://")); } }, }) .transform(response); }); ``` ### Inject a script ```js theme={null} import * as BunnySDK from "https://esm.sh/@bunny.net/edgescript-sdk@0.11.2"; BunnySDK.net.http.servePullZone() .onOriginResponse(async ({ response }) => { return new HTMLRewriter() .onDocument({ end(end) { end.append('', { html: true }); }, }) .transform(response); }); ``` ### Remove elements ```js theme={null} import * as BunnySDK from "https://esm.sh/@bunny.net/edgescript-sdk@0.11.2"; BunnySDK.net.http.servePullZone() .onOriginResponse(async ({ response }) => { return new HTMLRewriter() .on("script[src*='tracker']", { element(el) { el.remove(); }, }) .on(".cookie-banner", { element(el) { el.remove(); }, }) .transform(response); }); ``` ### Async handler Handlers can return a `Promise` for async operations like sub-requests. ```js theme={null} import * as BunnySDK from "https://esm.sh/@bunny.net/edgescript-sdk@0.11.2"; BunnySDK.net.http.servePullZone() .onOriginResponse(async ({ response }) => { return new HTMLRewriter() .on("include[src]", { async element(el) { const src = el.getAttribute("src"); const partial = await fetch(src); el.replace(partial.body, { html: true }); }, }) .transform(response); }); ``` ### Class-based handlers Instead of inline objects, you can define handler classes and pass instances to `.on()` or `.onDocument()`. ```js theme={null} import * as BunnySDK from "https://esm.sh/@bunny.net/edgescript-sdk@0.11.2"; class AttributeRewriter { #attrName; constructor(attrName) { this.#attrName = attrName; } element(el) { const value = el.getAttribute(this.#attrName); if (value) { el.setAttribute(this.#attrName, value.replace("http://", "https://")); } } } BunnySDK.net.http.servePullZone() .onOriginResponse(async ({ response }) => { return new HTMLRewriter() .on("a", new AttributeRewriter("href")) .on("img", new AttributeRewriter("src")) .transform(response); }); ``` #### Document handler class ```js theme={null} import * as BunnySDK from "https://esm.sh/@bunny.net/edgescript-sdk@0.11.2"; class StripComments { comments(comment) { comment.remove(); } end(end) { end.append("", { html: true }); } } BunnySDK.net.http.servePullZone() .onOriginResponse(async ({ response }) => { return new HTMLRewriter() .onDocument(new StripComments()) .transform(response); }); ``` #### Async class handler Class methods can be `async` just like inline handlers. ```js theme={null} import * as BunnySDK from "https://esm.sh/@bunny.net/edgescript-sdk@0.11.2"; class IncludeExpander { async element(el) { const src = el.getAttribute("src"); if (src) { const partial = await fetch(src); el.replace(partial.body, { html: true }); } } } BunnySDK.net.http.servePullZone() .onOriginResponse(async ({ response }) => { return new HTMLRewriter() .on("include[src]", new IncludeExpander()) .transform(response); }); ``` ### Multiple selectors Chain multiple `.on()` calls to handle different elements independently. ```js theme={null} import * as BunnySDK from "https://esm.sh/@bunny.net/edgescript-sdk@0.11.2"; BunnySDK.net.http.servePullZone() .onOriginResponse(async ({ response }) => { return new HTMLRewriter() .on("title", { element(el) { el.setInnerContent("My Site"); }, }) .on("meta[name='description']", { element(el) { el.setAttribute("content", "Custom description"); }, }) .on("img", { element(el) { el.setAttribute("loading", "lazy"); }, }) .transform(response); }); ``` ## References * [Edge Side Includes](https://www.w3.org/TR/esi-lang) - W3 Reference of ESI. * [W3C Selectors 4 Specification](https://www.w3.org/TR/selectors-4/) * [WASM HTMLRewriter Implementation](https://github.com/remorses/htmlrewriter) - If you want to use locally # HTTPS & SSL Certificates Source: https://bunny.net/docs/scripting/https-ssl-certificates How Edge Scripts verify TLS certificates when fetching external URLs and Pull Zone origins, and how to fix common certificate errors. ## Overview When your script calls `fetch()` on an `https://` URL, or when the runtime fetches your Pull Zone origin, it opens a fully verified TLS connection from the bunny.net edge to that host. It performs the same checks a browser does: the certificate must chain to a trusted public Certificate Authority, be valid for the hostname you connected to, and not be expired. Most of the time this needs no thought. Fetching an external HTTPS URL works out of the box: ```typescript theme={null} import * as BunnySDK from "npm:@bunny.net/edgescript-sdk@0.12.0"; BunnySDK.net.http.servePullZone(async (request: Request): Promise => { const res = await fetch("https://api.example.com/data.json"); return new Response(await res.text()); }); ``` Wrap the call in `try/catch` so that a certificate problem on the origin becomes a response you control instead of an unhandled error: ```typescript theme={null} try { const res = await fetch("https://api.example.com/data.json"); return new Response(await res.text()); } catch (err) { console.error(err); return new Response("Upstream unavailable", { status: 502 }); } ``` If a `fetch()` fails on the certificate, it rejects with a `TypeError` whose message names the problem, for example `invalid peer certificate: UnknownIssuer`. The [troubleshooting](#troubleshooting) section is organized by that message. ## Handled Automatically Some origins are misconfigured in ways that every browser tolerates. The runtime handles these for you so that a fetch behaves the way it does in a browser. None of them weaken verification: the certificate chain is still checked against trusted roots on every connection, and a certificate that fails for a real reason (untrusted issuer, expired, wrong hostname) still fails. * **Missing intermediate certificates.** Many servers send only their own leaf certificate and omit the intermediate that links it to the root CA. When a certificate fails *only* for this reason, the runtime completes the chain from a built-in list of publicly disclosed intermediates, sourced from the [Common CA Database (CCADB)](https://www.ccadb.org/), then verifies the completed chain against the trusted roots. * **bunny.net edge hostnames without their own certificate.** A hostname pointed at the bunny.net edge that has no SSL provisioned for that specific name (a white-label or alias domain, often reached after a redirect) is answered with one of bunny.net's own edge certificates, such as `*.b-cdn.net`. When verification fails *only* on the hostname check and the certificate provably belongs to bunny.net, the connection is accepted. A mismatched third-party certificate is never accepted this way. * **Older TLS 1.2 origins.** Legacy handshakes that some strict clients reject (for example an ECDSA P-256 certificate whose handshake is signed with SHA-384) connect normally. You do not need to configure anything for these. These conveniences apply to `fetch()` and origin connections only. The lower-level [`node:tls`](/docs/scripting/node-tls) module performs standard, stricter verification and rejects both cases. ## Private Certificate Authorities The built-in intermediate list only covers certificates disclosed in the CCADB. If your origin uses a private or internal Certificate Authority, trust its certificate for that fetch by passing it to `Deno.createHttpClient`: ```typescript theme={null} const client = Deno.createHttpClient({ caCerts: [ `-----BEGIN CERTIFICATE----- ...your CA certificate (PEM)... -----END CERTIFICATE-----`, ], }); const res = await fetch("https://internal.example.com/", { client }); ``` The certificate you supply is added to the trust store for that client only. Hostname and expiry checks still apply: this adds a trust anchor, it does not disable verification. ## Client Certificates (mTLS) Some origins require the client to present its own certificate. Pass the PEM-encoded certificate and private key to `Deno.createHttpClient` and use that client for the fetch. Store both as [environment secrets](/docs/scripting/secrets) so they never appear in your source code: ```typescript theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk@0.12.0"; import process from "node:process"; const client = Deno.createHttpClient({ cert: process.env.ClientCert, // certificate presented to the origin (PEM) key: process.env.ClientKey, // matching private key (PEM) // caCerts: [ ... ] // add if the origin uses a private CA }); BunnySDK.net.http.servePullZone(async (request: Request): Promise => { try { const res = await fetch("https://mtls.example.com/status", { client }); return new Response(await res.text(), { status: res.status }); } catch (err) { return new Response(`mTLS request failed: ${err.message}`, { status: 502 }); } }); ``` The origin's own certificate is still verified as usual. If you have trouble storing a multi-line PEM value in a secret, store it base64-encoded instead and decode it in the script with `atob(process.env.ClientCert)`. ## Your Pull Zone Origin Your pullzone origin fetch has their own configuration in the Pullzone. Those do not affects `fetch()` calls to other URLs. ## Troubleshooting The origin's certificate does not chain to a public Certificate Authority. The usual cause is a private or internal CA, or an intermediate that is not disclosed in the CCADB. Trust the CA with [`Deno.createHttpClient({ caCerts })`](#private-certificate-authorities). Public origins that merely omit a disclosed intermediate already work automatically. The certificate is valid, but not for the hostname you connected to. bunny.net's own edge certificates are accepted automatically. The origin's certificate has expired. Renew it on the origin. Expiry cannot be bypassed for external origins. An HTTPS origin reached by IP address received no matching server name. Browsers hide certificate-chain and hostname problems that a strict client will not. Check the origin's certificate with `openssl`: ```bash theme={null} openssl s_client -connect your-origin.example.com:443 \ -servername your-origin.example.com ``` `unable to verify the first certificate` means a missing intermediate: fix the origin to send its full chain, or use `caCerts` if the CA is private. `Verify return code: 0 (ok)` means the chain is fine and the problem lies elsewhere. Turn off **Verify Origin SSL** on the Pull Zone. This applies only to your configured origin. ## References * [`fetch()` on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch) * [`Deno.createHttpClient`](https://docs.deno.com/api/deno/~/Deno.createHttpClient) * [Common CA Database (CCADB)](https://www.ccadb.org/) * [Node:TLS](/docs/scripting/node-tls) # Bunny Edge Scripting Source: https://bunny.net/docs/scripting/index Run JavaScript and TypeScript at the edge. Build APIs, dynamic UIs, AI-powered functions, or extend Bunny CDN with middleware, all without managing servers. bunny.net Scripting Bunny Edge Scripting lets you deploy code directly to the edge, close to your users, without running your own servers. Edge Scripting runs on [Deno](https://deno.com/) and V8, the engine behind Chrome. Each script gets an isolated runtime with access to standard Web APIs. You can write the code in the browser or connect a GitHub repository for continuous deployment. ## Script types Independent scripts that handle HTTP requests directly. Build REST APIs, serve dynamic HTML, process data, or call external services like AI models, all without an origin server. Intercept and modify requests and responses as they flow through Bunny CDN. Add authentication, manipulate headers, transform content, or implement custom caching logic. ## Features * Write, test, and deploy scripts in the browser editor * Connect a GitHub repository for version control and automatic deployments through GitHub Actions * Store API keys and configuration as environment variables or secrets * Debug with live logs and track execution metrics ## Next steps Create and deploy your first edge script in minutes. See practical examples for standalone and middleware scripts. # Limits Source: https://bunny.net/docs/scripting/limits Execution limits for Edge Scripts, covering CPU time, memory, subrequests, script size, and environment variables. | Feature | Limit | Description | | -------------------------------- | ------ | ---------------------------------------------------- | | CPU Time per request | 30s | Maximum CPU time allocated per request. | | Active memory | 128 MB | Maximum memory available to isolate instance. | | Subrequests | 50 | Number of subrequests a script can make per request. | | Script size | 10 MB | Maximum size of the script code. | | Startup time | 500ms | Maximum time to startup the script. | | Environment variables per Script | 128 | Maximum number of environment variables per script. | | Environment variable size | 2KB | Maximum size of environment variable value. | These limits cover typical use under normal operating conditions, and they keep one script from taking resources away from everyone else's. Occasional spikes above a limit are tolerated. A script that consistently hits or exceeds one will be throttled or terminated. ## Staying within the limits * Review your script's code so it finishes within the time and memory limits. * Keep the number of subrequests down, and cache responses where you can. * Watch the logs and statistics for your script so you catch a growing problem before it turns into a limit violation. ## CPU Time CPU time is measured while the script is actually using the CPU to run your logic, and it directly affects how quickly a script can respond. It is allocated per request, so every execution gets the same share of processing time. Infrequent spikes in CPU usage are fine. Going over the limit again and again will lead to the script being terminated. # Logs Source: https://bunny.net/docs/scripting/logs See the messages and errors your script writes while it runs. Logs record the messages, errors, and other runtime information your script produces, which is usually the quickest way to work out why it behaved the way it did. To view the logs, select the **Logs** tab in the navigation pane: # Modify response body (Middleware) Source: https://bunny.net/docs/scripting/middleware/examples/modify-body Use a middleware script to change the response body coming back from the origin before it is cached. This middleware reads the HTML response from the origin and appends `- bunny.net` to the contents of the `` tag, so every page carries it. ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; /** * When a response is not served from the cache, you can use this event handler * to modify the response coming from the origin. This runs before the response * is cached. * * Returns an HTTP response. * @param {Context} context - The context of the middleware. * @param {Request} request - The current request done to the origin. * @param {Response} response - The HTTP response or string. */ async function onOriginResponse(context: { request: Request; response: Response; }): Promise<Response> | Response | void { let body = await context.response.text(); body = body.replace("", "- bunny.net"); // Remove the original Content headers so the response is returned correctly const headers = new Headers(context.response.headers); headers.delete("Content-Length"); headers.delete("Content-Encoding"); return new Response(body, { status: context.response.status, // Preserve the original status code headers, // Preserve the original headers }); } BunnySDK.net.http.servePullZone().onOriginResponse(onOriginResponse); ``` # Modify HTTP headers Source: https://bunny.net/docs/scripting/middleware/examples/modify-headers Reject requests that use the wrong method and set your own headers on the response. This script answers `GET` with a JSON body and rejects every other method with a 405 and an `Allow` header. ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve( async (request: Request): Response | Promise => { const url = new URL(request.url); if (request.method !== "GET") return new Response("Not Allowed", { status: 405, headers: { Allow: "GET", }, }); const responseObj = { hello: "world", }; return new Response(JSON.stringify(responseObj), { headers: { "Content-type": "application/json", }, }); }, ); ``` # Middleware scripts Source: https://bunny.net/docs/scripting/middleware/overview Middleware scripts allow you to transform and modify HTTP requests and responses as they flow through the CDN. Manipulate requests before they reach your origin server or the cache and alter responses before they are sent back to the client. Middleware scripts sit inside the CDN request flow. Your logic runs on requests on their way in and on responses on their way out, so you can change how traffic behaves without touching your backend, and the work happens at the edge rather than on your origin. ## Use cases * Verify credentials and manage session tokens at the edge * Add, modify, or remove HTTP headers on requests and responses * Rewrite HTML, inject scripts, or otherwise change the response body before delivery * Run A/B tests and feature flags, routing users based on headers or cookies * Route or redirect requests based on path, geolocation, or your own logic * Apply rate limiting, IP filtering, or bot protection ## The `servePullZone` function The `servePullZone` function creates a middleware handler that integrates with your Pull Zone. It returns a chainable object for adding request and response middleware. ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http .servePullZone({ url: "https://your-origin.com" }) .onOriginRequest((ctx) => { // Modify or intercept requests return Promise.resolve(ctx.request); }) .onOriginResponse((ctx) => { // Modify responses return Promise.resolve(ctx.response); }); ``` ### Function signature ```ts theme={null} servePullZone(options: { url: string }): PullZoneHandler ``` | Option | Type | Description | | ------ | -------- | --------------------------------------------------------------------------------------------------------------------- | | `url` | `string` | The origin URL to proxy requests to. Only used during local development; in production, the Pull Zone origin is used. | The `url` option is only used during local development. When deployed to bunny.net, requests are proxied to the origin configured in your Pull Zone settings. ## Middleware methods The `servePullZone` function returns a `PullZoneHandler` object with chainable middleware methods: ### `onClientRequest` Preview If your PullZone is configured to execute script before cache, you'll have access to this function. Intercepts requests before they are sent to the cache. You can modify the request or short-circuit by returning a response directly. ```ts theme={null} onClientRequest( middleware: (ctx: { request: Request }) => Promise | Response | Promise | Request | void ): PullZoneHandler ``` | Property | Type | Description | | ------------- | --------- | --------------------------- | | `ctx.request` | `Request` | The incoming request object | **Return value:** * Return `Promise` to continue to the origin with the (modified) request * Return `Promise` to short-circuit and respond immediately without hitting the cache ### `onClientResponse` Preview If your PullZone is configured to execute script before cache, you'll have access to this function. Intercepts responses before they are returned to the user, including responses served from the cache. ```ts theme={null} onClientResponse( middleware: (ctx: { request: Request; response: Response }) => Promise | Response ): PullZoneHandler ``` | Property | Type | Description | | -------------- | ---------- | ----------------------------------- | | `ctx.request` | `Request` | The original request object | | `ctx.response` | `Response` | The response from the origin server | **Return value:** * Return `Promise` or `Response` with the (modified) response to send to the client. ### `onOriginRequest` Intercepts requests before they are sent to the origin server. You can modify the request or short-circuit by returning a response directly. ```ts theme={null} onOriginRequest( middleware: (ctx: { request: Request }) => Promise | Promise ): PullZoneHandler ``` | Property | Type | Description | | ------------- | --------- | --------------------------- | | `ctx.request` | `Request` | The incoming request object | **Return value:** * Return `Promise` to continue to the origin with the (modified) request * Return `Promise` to short-circuit and respond immediately without hitting the origin ### `onOriginResponse` Intercepts responses from the origin server before they are sent to the client. Modifications occur before the response is cached. ```ts theme={null} onOriginResponse( middleware: (ctx: { request: Request; response: Response }) => Promise ): PullZoneHandler ``` | Property | Type | Description | | -------------- | ---------- | ----------------------------------- | | `ctx.request` | `Request` | The original request object | | `ctx.response` | `Response` | The response from the origin server | **Return value:** * Return `Promise` with the (modified) response to send to the client ## Enable before cache scripts Preview Before cache execution is turned on per Pull Zone. Navigate to your Pull Zone, then go to **General** > **Origin** and enable **Run script before cache**. Learn more about [before cache execution](/docs/scripting/before-cache). ## Workflow When a client makes a request to a Pull Zone, the request passes through middleware at different stages: If your PullZone is configured to execute script before cache, you'll run the `onClientRequest` and `onClientResponse` if those are registered. 1. **`onClientRequest`** - Called before the request is sent to the cache. Modify the request or return a response to short-circuit. 2. **`onOriginRequest`** - Called before the request is sent to the origin, so only when the cache returns a *MISS*. Modify the request or return a response to short-circuit. 3. **Origin fetch** - The request is sent to your origin server. 4. **`onOriginResponse`** - Called after the origin responds. Modify the response before it's sent to the client and cached. 5. **`onClientResponse`** - Called just before a response is sent to the client, unless you short-circuited at the `onClientRequest` layer. ```mermaid theme={null} sequenceDiagram participant Client participant Cache participant Origin Client->>Cache: onClientRequest Cache->>Origin: onOriginRequest Origin-->>Cache: onOriginResponse Cache-->>Client: onClientResponse ``` ## Example This example gates a route behind a feature flag and adds a custom header to responses: ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http .servePullZone({ url: "https://echo.free.beeceptor.com/" }) .onOriginRequest((ctx) => { const optFT = ctx.request.headers.get("feature-flags"); const featureFlags = optFT ? optFT.split(",").map((v) => v.trimStart()) : []; // Route-based matching and feature flag check const path = new URL(ctx.request.url).pathname; if (path === "/d") { if (!featureFlags.includes("route-d-preview")) { // Short-circuit: return response without hitting origin return Promise.resolve( new Response("You cannot use this route.", { status: 400 }), ); } } // Continue to origin with the request return Promise.resolve(ctx.request); }) .onOriginResponse((ctx) => { // Add custom header to response ctx.response.headers.append("X-Via", "MyMiddleware"); return Promise.resolve(ctx.response); }); ``` ### Local development You can run middleware scripts locally using Deno: ```bash theme={null} deno run -A script.ts ``` Test with curl: ```bash theme={null} # Blocked - missing required feature flag curl http://127.0.0.1:8080/d --header 'feature-flags: something-else' # Allowed - has required feature flag curl http://127.0.0.1:8080/d --header 'feature-flags: route-d-preview, something-else' ``` # Node:FS Source: https://bunny.net/docs/scripting/node-fs Read and write files in Edge Scripts using the Node.js-compatible file system API No breaking changes are expected, but additional features will be implemented soon. ## Overview Edge Scripting supports the Node.js file system API through the `node:fs` and `node:fs/promises` modules, so you can read, write, and manipulate files and directories from inside an Edge Script. The implementation sits on Deno's sandboxed environment with Node.js compatibility. Both the Promise-based API (`node:fs/promises`) and the older callback-based API (`node:fs`) work. We recommend the Promise-based one for the cleaner async/await syntax. ## Virtual file system Every script gets a virtual file system for file operations. It lives in the virtual memory available to the worker. The directory tree currently has two folders: ``` / # Root directory ├── home/ │ └── user/ # Your current working directory, R/W access └── tmp/ # Temporary directory, R/W access ``` You can create directories anywhere. Permissions are enforced for the owner (user) but not for group or others. All files are opened in `w+`. Avoid creating directories in the root directory `/`. We plan to add folders there for other functions, such as `/dev/*` and other read only directories. Paths in the virtual file system are limited to **4096 characters** (`foo/bar` counts as 7) and to **48 segments** (`a/b/c` is 3 segments). Capacity checks for creating, copying and moving happen before the operation runs. Copying a large file can therefore fail with an out of memory error even though there is still space to create new files or directories. ## Importing the module EdgeScripting supports both the Promise-based API and the callback-based API. ### Promise-based API (recommended) ```typescript theme={null} import * as fs from "node:fs/promises"; // Use with async/await const data = await fs.readFile("/tmp/file.txt", "utf-8"); ``` ### Callback-based API ```typescript theme={null} import * as fs from "node:fs"; // Use with callbacks fs.readFile("/tmp/file.txt", "utf-8", (err, data) => { if (err) { console.error("Error:", err); return; } console.log("Data:", data); }); ``` ## Limitations The file system runs in a sandbox, which constrains what it can do. The following sections cover where it differs from Node.js. Always handle file system errors gracefully. The sandboxed environment may behave differently from traditional Node.js environments. ### Symbolic links and hard links Symbolic links are supported. `symlink()`, `symlinkSync()`, `readlink()`, `readlinkSync()`, and `lstat()` work as expected. Hard links are not supported. `link()` and `linkSync()` will throw errors. ### File statistics and timestamps All `Stats` timestamp properties are supported and return correct values: **Reliable Stats properties:** * `size` - File size in bytes * `isFile()` - Check if entry is a file * `isDirectory()` - Check if entry is a directory * `atime` - Last access time * `mtime` - Last modification time * `ctime` - Last status change time * `atimeMs`, `mtimeMs`, `ctimeMs` - Millisecond timestamps * `birthtime` - File creation time ### File and directory permissions File and directory permissions are supported for the **owner (user)** bits. `chmod()` and `chmodSync()` work, and `Stats.mode` reflects the permissions you set. `access()` respects owner-level read, write, and execute bits. **Group and others** permission bits are accepted but have no effect: the sandboxed environment always runs as the file owner (uid=1000, gid=1000), so only the user/owner bits matter. `chown()` and `chownSync()` are implemented for compatibility, but ownership is not enforced. Changing the uid or gid has no effect on file access or permissions. ### File watching File system watching is not supported. The following operations are unavailable: * `watch()` - Watch for file changes * `watchFile()` - Poll for file changes * `unwatchFile()` - Stop watching * `FSWatcher` class **Workaround:** Use polling with `stat()` if you need to detect changes, but be mindful of performance implications and CPU time limits. ### Performance considerations **Memory limits:** The whole Virtual FileSystem lives inside your script memory. The current file system limits of your script is set up to 64MB. Reading large files may cause memory exhaustion if you store it in memory. Leverage streaming to avoid allocating too much memory at a time. **CPU time:** File I/O counts toward the 30-second CPU time limit per request. **Best practices:** * Stream large files instead of reading entirely into memory * Clean up temporary files to avoid storage bloat * See [Limits](/docs/scripting/limits) for more details on resource constraints ## Quickstart Common file system patterns in EdgeScripting. ### Example 1: Reading a file ```typescript theme={null} import * as fs from "node:fs/promises"; import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve(async (request: Request) => { try { // Read file as UTF-8 string const content = await fs.readFile("/tmp/data.txt", "utf-8"); return new Response(content, { headers: { "content-type": "text/plain" } }); } catch (error) { return new Response(`Error reading file: ${error.message}`, { status: 500 }); } }); ``` ### Example 2: Writing a file ```typescript theme={null} import * as fs from "node:fs/promises"; import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve(async (request: Request) => { const data = await request.json(); try { // Write JSON data to file await fs.writeFile( "/tmp/output.json", JSON.stringify(data, null, 2), "utf-8" ); return new Response("File written successfully", { status: 201 }); } catch (error) { return new Response(`Error writing file: ${error.message}`, { status: 500 }); } }); ``` ### Example 3: Working with directories ```typescript theme={null} import * as fs from "node:fs/promises"; import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve(async (request: Request) => { try { // Create directory (recursive option creates parent directories) await fs.mkdir("/tmp/myapp/data", { recursive: true }); // Write a file in the new directory await fs.writeFile("/tmp/myapp/data/config.json", "{}", "utf-8"); // List directory contents const files = await fs.readdir("/tmp/myapp/data"); return new Response(JSON.stringify({ files }), { headers: { "content-type": "application/json" } }); } catch (error) { return new Response(`Error: ${error.message}`, { status: 500 }); } }); ``` ### Example 4: Error handling patterns ```typescript theme={null} import * as fs from "node:fs/promises"; import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve(async (request: Request) => { const filepath = "/tmp/cache/data.txt"; try { // Check if file exists by trying to access it await fs.access(filepath); // File exists, read it const content = await fs.readFile(filepath, "utf-8"); return new Response(content, { headers: { "x-cache": "hit" } }); } catch (error) { if (error.code === "ENOENT") { // File doesn't exist, create it const defaultContent = "Default cached data"; await fs.writeFile(filepath, defaultContent, "utf-8"); return new Response(defaultContent, { status: 201, headers: { "x-cache": "miss" } }); } // Re-throw other errors throw error; } }); ``` ### Example 5: Using FileHandle for chunked reading This script is not optimized. It shows how chunked reading works, so please don't run anything like it in production. ```typescript theme={null} import * as fs from "node:fs/promises"; import * as BunnySDK from "@bunny.net/edgescript-sdk"; // Global promise to track the download operation let downloadPromise: Promise | null = null; const filePath = "/tmp/large-file.txt"; async function downloadLoremIpsum() { try { const response = await fetch('https://lorem-api.com/api/lorem', { method: 'GET', headers: { 'Content-Type': 'application/json' } }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const content = await response.text(); // Calculate how many times we need to repeat to reach ~10MB const targetSize = 10 * 1024 * 1024; // 10MB in bytes const contentSize = Buffer.byteLength(content, 'utf-8'); const repeatCount = Math.ceil(targetSize / contentSize); // Create the repeated content let finalContent = ''; for (let i = 0; i < repeatCount; i++) { finalContent += content; } // Write to file await fs.writeFile(filePath, finalContent, 'utf-8'); } catch (error) { // Clear the promise on error so it can be retried downloadPromise = null; throw error; } } async function ensureFileExists() { try { // Check if file exists await fs.access(filePath); } catch (error) { // File doesn't exist, initiate download if (!downloadPromise) { // Start download and store the promise downloadPromise = downloadLoremIpsum(); } // Wait for the download to complete (whether we started it or another request did) await downloadPromise; } } BunnySDK.net.http.serve(async (request: Request) => { try { // Ensure file exists (download if necessary, blocking until complete) await ensureFileExists(); // Create SSE stream const stream = new ReadableStream({ async start(controller) { const encoder = new TextEncoder(); let fileHandle; try { // Open file for reading fileHandle = await fs.open(filePath, "r"); // Get file stats to know the total size const stats = await fileHandle.stat(); const totalSize = stats.size; // Send initial event with file info controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: "start", totalSize, chunkSize: 1024, timestamp: new Date().toISOString() })}\n\n`)); const buffer = new Uint8Array(1024); let position = 0; let chunkNumber = 0; // Read file 1KB at a time while (position < totalSize) { const { bytesRead } = await fileHandle.read(buffer, 0, 1024, position); if (bytesRead === 0) break; // End of file // Convert to string const content = new TextDecoder().decode(buffer.slice(0, bytesRead)); // Send chunk as SSE event controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: "chunk", chunkNumber, bytesRead, position, content, progress: ((position + bytesRead) / totalSize * 100).toFixed(2) + "%", timestamp: new Date().toISOString() })}\n\n`)); position += bytesRead; chunkNumber++; // Small delay to prevent overwhelming the client await new Promise(resolve => setTimeout(resolve, 10)); } // Send completion event controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: "complete", totalChunks: chunkNumber, totalBytesRead: position, timestamp: new Date().toISOString() })}\n\n`)); } catch (error) { // Send error event controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: "error", message: error.message, timestamp: new Date().toISOString() })}\n\n`)); } finally { // Close file handle await fileHandle?.close(); controller.close(); } } }); return new Response(stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no" } }); } catch (error) { return new Response(`Error: ${error.message}`, { status: 500 }); } }); ``` ### Example 6: Symbolic links ```typescript theme={null} import * as fs from "node:fs/promises"; import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve(async (request: Request) => { const original = "/tmp/original.txt"; const link = "/tmp/link.txt"; try { await fs.writeFile(original, "hello from original", "utf-8"); // Create a symbolic link pointing to the original file await fs.symlink(original, link); // Reading through the symlink is transparent const content = await fs.readFile(link, "utf-8"); // lstat inspects the link itself, not the target const linkStats = await fs.lstat(link); const target = await fs.readlink(link); return new Response( JSON.stringify({ content, isSymlink: linkStats.isSymbolicLink(), target }), { headers: { "content-type": "application/json" } } ); } catch (error) { return new Response(`Error: ${error.message}`, { status: 500 }); } }); ``` ## References * [Node.js File System Documentation](https://nodejs.org/api/fs.html) - Complete Node.js fs module reference * [Node.js fs/promises API](https://nodejs.org/api/fs.html#promises-api) - Promise-based file system API * [Node.js FileHandle Class](https://nodejs.org/api/fs.html#class-filehandle) - Advanced file operations * [Node.js File System Flags](https://nodejs.org/api/fs.html#file-system-flags) - Available flags for file operations * [MDN File API](https://developer.mozilla.org/en-US/docs/Web/API/File) - Web standard File interface * [MDN Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) - Binary data objects * [MDN TextEncoder](https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder) - Encoding strings to bytes * [MDN TextDecoder](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder) - Decoding bytes to strings * [EdgeScripting Limits](/docs/scripting/limits) - Resource limits and quotas # Node:TLS Source: https://bunny.net/docs/scripting/node-tls Open low-level TLS client connections from Edge Scripts with the Node.js-compatible node:tls module, and learn when to use it instead of fetch(). ## Overview Edge Scripting supports the client side of the Node.js `node:tls` module, so a script can open an outbound TLS connection and speak any protocol over it, not just HTTP. It also gives you access to things `fetch()` hides: the peer certificate, the negotiated protocol and cipher, and the ability to present a client certificate or pin a specific server certificate. For HTTP and HTTPS endpoints, prefer `fetch()`. | Use `fetch()` when… | Use `node:tls` when… | | -------------------------------------- | ------------------------------------------------------------------------------------ | | Talking to an HTTP or HTTPS endpoint | Speaking a non-HTTP protocol over TLS | | You want browser-like handling origins | You need low-level control of the TLS connection, or need to inspect the certificate | | Recommended for almost all cases | Advanced use | ## Quickstart Opens an outbound TLS connection and reports whether the certificate was trusted. `tls.connect()` verifies the server certificate against the public root CAs; the callback runs once the handshake completes, and `socket.authorized` reports whether the chain was trusted: ```typescript theme={null} import * as BunnySDK from "npm:@bunny.net/edgescript-sdk@0.12.1"; import tls from "node:tls"; BunnySDK.net.http.serve(async (request: Request): Promise => { return new Promise((resolve) => { const socket = tls.connect( { host: "example.com", port: 443, servername: "example.com" }, () => { const info = `authorized=${socket.authorized} protocol=${socket.getProtocol()}`; socket.end(); resolve(new Response(info)); }, ); socket.on("error", (err) => resolve(new Response(`TLS error: ${err.message}`, { status: 502 })), ); }); }); ``` Always set `servername` so the server receives the SNI it needs to select the right certificate. ## Importing the module ```typescript theme={null} import tls from "node:tls"; ``` The module exposes the following client-side API: | Export | Description | | ---------------------------- | ------------------------------------------------------------------------------------------------------- | | `connect()` | Open an outbound TLS client connection. | | `TLSSocket` | The socket type a connection produces. | | `checkServerIdentity()` | Verify that a certificate matches a hostname. | | `createSecureContext()` | Build TLS options for a connection. | | `rootCertificates` | The built-in list of trusted root CAs. | | `setDefaultCACertificates()` | Override the default CA list. | | `getCiphers()` | Returns an array with the names of the supported TLS ciphers. The names are lower-case. | | Constants | `DEFAULT_MIN_VERSION` (`TLSv1.2`), `DEFAULT_MAX_VERSION` (`TLSv1.3`), and the other standard constants. | ## Certificate verification If you must use `node:tls` against an origin with a private CA or a missing intermediate, supply the certificate yourself with the connection's `ca` option: ```typescript theme={null} import tls from "node:tls"; const socket = tls.connect({ host: "internal.example.com", port: 8883, servername: "internal.example.com", ca: [ `-----BEGIN CERTIFICATE----- ...your CA or intermediate certificate (PEM)... -----END CERTIFICATE-----`, ], }); ``` ## Examples ### Example 1: SSL checker `fetch()` verifies certificates but never shows them to you. With `node:tls` you can open the connection yourself, wait for the handshake, and read what the server presented. This script exposes an endpoint that returns a JSON report about a host's certificate: who issued it, which names it covers, when it expires, and whether the chain is trusted. Point a monitor at it to be warned before a certificate runs out. ```typescript theme={null} import * as BunnySDK from "npm:@bunny.net/edgescript-sdk@0.12.1"; import tls from "node:tls"; const WARN_DAYS = 14; interface CertReport { host: string; port: number; authorized: boolean; authorizationError?: string; protocol: string | null; cipher: string; subject?: string; issuer?: string; altNames: string[]; validFrom?: string; validTo?: string; daysUntilExpiry?: number; fingerprint256?: string; warning?: string; } /** * Opens a TLS connection to host:port and resolves with a report about the * certificate once the handshake completes. */ function checkCertificate(host: string, port: number): Promise { return new Promise((resolve, reject) => { const socket = tls.connect( { host, port, servername: host, // Complete the handshake even if verification fails, so we can // report *why* instead of just failing. `authorized` tells us the result. rejectUnauthorized: false, }, () => { const cert = socket.getPeerCertificate(); const validTo = cert?.valid_to ? new Date(cert.valid_to) : undefined; const daysUntilExpiry = validTo ? Math.floor((validTo.getTime() - Date.now()) / 86_400_000) : undefined; const report: CertReport = { host, port, authorized: socket.authorized, authorizationError: socket.authorizationError?.toString(), protocol: socket.getProtocol(), // e.g. "TLSv1.3" cipher: socket.getCipher().name, subject: cert?.subject?.CN, issuer: cert?.issuer?.O ?? cert?.issuer?.CN, altNames: (cert?.subjectaltname ?? "") .split(", ") .filter(Boolean) .map((n) => n.replace(/^DNS:/, "")), validFrom: cert?.valid_from, validTo: cert?.valid_to, daysUntilExpiry, fingerprint256: cert?.fingerprint256, }; if (!socket.authorized) { report.warning = "Certificate chain is not trusted"; } else if (daysUntilExpiry !== undefined && daysUntilExpiry < 0) { report.warning = "Certificate has expired"; } else if (daysUntilExpiry !== undefined && daysUntilExpiry <= WARN_DAYS) { report.warning = `Certificate expires in ${daysUntilExpiry} days`; } socket.end(); resolve(report); }, ); socket.on("error", reject); socket.setTimeout(8000, () => { socket.destroy(); reject(new Error("TLS connection timed out")); }); }); } BunnySDK.net.http.serve(async (request: Request): Promise => { const url = new URL(request.url); const host = url.searchParams.get("host"); const port = Number(url.searchParams.get("port") ?? "443"); if (!host) { return new Response("Missing ?host= parameter", { status: 400 }); } try { const report = await checkCertificate(host, port); return new Response(JSON.stringify(report, null, 2), { // 200 when healthy, 503 when something needs attention, // so uptime monitors can alert on the status code alone. status: report.warning ? 503 : 200, headers: { "content-type": "application/json" }, }); } catch (err) { return new Response( JSON.stringify({ host, port, error: err.message }), { status: 502, headers: { "content-type": "application/json" } }, ); } }); ``` Calling `/?host=example.com` returns something like: ```json theme={null} { "host": "example.com", "port": 443, "authorized": true, "protocol": "TLSv1.3", "cipher": "TLS_AES_256_GCM_SHA384", "subject": "example.com", "issuer": "DigiCert Inc", "altNames": ["example.com", "www.example.com"], "validFrom": "Jan 15 00:00:00 2026 GMT", "validTo": "Jan 15 23:59:59 2027 GMT", "daysUntilExpiry": 151, "fingerprint256": "AB:CD:12:..." } ``` A few notes on the code: * `rejectUnauthorized: false` is used here on purpose. This endpoint only *reports* on a certificate and never sends data to the host, so completing the handshake for an untrusted certificate is safe and lets the report explain the failure through `authorized` and `authorizationError`. Do not use this option when you actually talk to the origin. * Always set `servername` so the server receives the SNI it needs to select the right certificate. Without it, hosts serving several sites return their default certificate. * The endpoint answers `503` when there is a warning, so an uptime monitor can alert on the status code without parsing the body. ### Example 2: Certificate pinning Standard verification accepts any certificate a public CA is willing to issue for a hostname. If you send credentials to a sensitive origin, you may want to go further and only accept the exact certificate you expect. `fetch()` gives you no access to the peer certificate, but `tls.connect()` lets you pass your own `checkServerIdentity` function, which runs during the handshake before any application data is sent. The function below first performs the standard hostname check, then compares the certificate's SHA-256 fingerprint to a pinned value stored as an [environment secret](/docs/scripting/secrets). Returning an `Error` aborts the connection: ```typescript theme={null} import * as BunnySDK from "npm:@bunny.net/edgescript-sdk@0.12.1"; import tls from "node:tls"; import process from "node:process"; const ORIGIN_HOST = "api.example.com"; // SHA-256 fingerprint of the expected certificate, e.g. // "AB:CD:12:...". Get it with: // openssl s_client -connect api.example.com:443 /dev/null \ // | openssl x509 -noout -fingerprint -sha256 const PINNED_FINGERPRINT = process.env.PinnedFingerprint; const API_TOKEN = process.env.ApiToken; function pinnedRequest(path: string): Promise { return new Promise((resolve, reject) => { const chunks: Uint8Array[] = []; const socket = tls.connect( { host: ORIGIN_HOST, port: 443, servername: ORIGIN_HOST, checkServerIdentity(hostname, cert) { // Keep the standard hostname check... const err = tls.checkServerIdentity(hostname, cert); if (err) return err; // ...and additionally require the exact certificate we pinned. if (cert.fingerprint256 !== PINNED_FINGERPRINT) { return new Error( `Certificate fingerprint mismatch: got ${cert.fingerprint256}`, ); } }, }, () => { // Only reached when the pin matched. Safe to send the token. socket.write( `GET ${path} HTTP/1.1\r\n` + `Host: ${ORIGIN_HOST}\r\n` + `Authorization: Bearer ${API_TOKEN}\r\n` + `Connection: close\r\n\r\n`, ); }, ); socket.on("data", (chunk) => chunks.push(chunk)); socket.on("end", () => { const raw = new TextDecoder().decode(concat(chunks)); resolve(raw.split("\r\n\r\n")[1] ?? ""); // body only }); socket.on("error", reject); socket.setTimeout(8000, () => { socket.destroy(); reject(new Error("TLS connection timed out")); }); }); } function concat(chunks: Uint8Array[]): Uint8Array { const out = new Uint8Array(chunks.reduce((n, c) => n + c.length, 0)); let offset = 0; for (const c of chunks) { out.set(c, offset); offset += c.length; } return out; } BunnySDK.net.http.serve(async (request: Request): Promise => { if (!PINNED_FINGERPRINT || !API_TOKEN) { return new Response("Pin or token not configured", { status: 500 }); } try { const body = await pinnedRequest("/v1/account"); return new Response(body, { headers: { "content-type": "application/json" } }); } catch (err) { return new Response(`Pinned request failed: ${err.message}`, { status: 502 }); } }); ``` Pinning trades flexibility for assurance: when the origin rotates its certificate, the pin must be updated or requests start failing. Pin the fingerprint of a longer-lived intermediate CA (compare `cert.issuerCertificate.fingerprint256`) if the origin renews its leaf certificate often. ### Example 3: Client certificates (mTLS) If the server requires a client certificate, pass the PEM-encoded `cert` and `key` in the connection options, read from [environment secrets](/docs/scripting/secrets): ```typescript theme={null} import process from "node:process"; const socket = tls.connect({ host: "mqtt.example.com", port: 8883, servername: "mqtt.example.com", cert: process.env.ClientCert, key: process.env.ClientKey, }); ``` If you have trouble storing a multi-line PEM value in a secret, store it base64-encoded instead and decode it in the script with `atob(process.env.ClientCert)`. For HTTPS origins that require mTLS, use `fetch()` with `Deno.createHttpClient` instead. See [Client Certificates on the HTTPS page](/docs/scripting/https-ssl-certificates#client-certificates-mtls). ## References * [`node:tls` on nodejs.org](https://nodejs.org/api/tls.html) * [HTTPS & SSL Certificates](/docs/scripting/https-ssl-certificates) # Pricing Source: https://bunny.net/docs/scripting/pricing How Edge Scripting is billed: CPU time, requests, increments, and worked examples. ## Pricing components Edge Scripting pricing is based on two components: 1. **CPU Time:** CPU Time refers to the actual time the CPU spends processing your script code. This measurement excludes any waiting time for input/output operations, such as fetch requests. CPU Time is measured in milliseconds (ms) and billed in increments of 1 million milliseconds (1000s). 2. **Requests**: This component tracks the number of requests that your script executes. Requests are billed in increments of one million requests. | CPU Time | Requests | | ----------------------- | ------------------------- | | \$0.02 / 1000s CPU time | \$0.20 / million requests | Increments are applied based on the total usage across all scripts, rather than for each individual script. Billing begins at the first increment, not at the first million. As soon as a script serves a single request, the first increment of each component is charged in full, and all remaining usage within that increment is already covered. The smallest monthly Scripting charge is \$0.22. That is the first CPU Time increment (\$0.02) plus the first Requests increment (\$0.20). You will see it as soon as you start testing a script. It stays at \$0.22 until you pass 1,000,000 requests or 1000s of CPU time. See [Example 1: Minimum usage](#example-1-minimum-usage). CDN bandwidth is charged at the normal rate and billed separately. It is not included in the CPU Time or Requests charges. ## Example pricing scenarios ### Example 1: Minimum usage A newly deployed script that serves a single request, consuming 12 milliseconds of CPU time, would have the following estimated costs: | | Monthly cost | Calculation | | -------- | ------------ | ---------------------------------------------------------------------- | | CPU Time | \$0.02 | 12 ms falls within the first 1,000,000 ms increment \* \$0.02 | | Requests | \$0.20 | 1 request falls within the first 1,000,000 request increment \* \$0.20 | **Total Monthly Cost: \$0.22** The rest of both increments is already paid for. Every further request that month adds nothing to this total until you pass 1,000,000 requests or 1000s of CPU time, at which point the next increment begins. ### Example 2: Moderate usage A script that handles 10 million requests per month, with each request consuming an average of 10 milliseconds of CPU time, would have the following estimated costs: | | Monthly cost | Calculation | | -------- | ------------ | ------------------------------------------------------------------- | | CPU Time | \$2.00 | 0.01s CPU time per request \* 10,000,000 requests / 1000s \* \$0.02 | | Requests | \$2.00 | 10,000,000 requests / 1,000,000 \* \$0.20 | **Total Monthly Cost: \$4.00** ### Example 3: High usage A script that handles 100 million requests per month, with each request consuming an average of 7 milliseconds of CPU time, would have the following estimated costs: | | Monthly cost | Calculation | | -------- | ------------ | --------------------------------------------------------------------- | | CPU Time | \$14.00 | 0.007s CPU time per request \* 100,000,000 requests / 1000s \* \$0.02 | | Requests | \$20.00 | 100,000,000 requests / 1,000,000 \* \$0.20 | **Total Monthly Cost: \$34.00** Because billing works in whole increments, your cost stays flat within an increment and steps up only when you cross into the next one. # Quickstart Source: https://bunny.net/docs/scripting/quickstart Create and deploy your first Edge Script in minutes. Login to [bunny.net dashboard](https://dash.bunny.net/auth/login?pk_buttonlocation=menu), then navigate to **Edge Platform** > **Scripting** and click **Add Script**. Provide a name for your script and select a script type: Select **Standalone** type and then **Default standalone** template. Standalone scripts handle HTTP requests directly at the edge, without an origin server behind them. Use them for REST APIs, UI applications, and data processing. [Learn more about standalone scripts](/docs/scripting/standalone/overview) Select **Middleware** type and then **Default middleware** template. Middleware scripts intercept and modify requests and responses as they flow through the CDN. Use them for authentication, header manipulation, and content transformation. [Learn more about middleware scripts](/docs/scripting/middleware/overview) Click **Add script**. A pull zone is created for your script, so it is reachable from the outside through a unique URL. The script starts with a placeholder function that returns "Hello from" when you open that URL. The bottom left panel is a preview pane where you can enter a URL and run the script currently open in the editor. Running it here doesn't change the deployed script, so you can test your code before publishing it. On the bottom right, the dashboard includes a logs pane. This pane captures and displays output from any `console.log` calls made by the script when it is run in the preview pane. Use it to trace what the script did on the last run and to confirm it behaved the way you expected. Replace the placeholder with your own code, then open the script URL to check that your changes work. When you are happy with your script, click **Save** and **Publish**. Published changes take effect immediately. # Runtime Source: https://bunny.net/docs/scripting/runtime The runtime APIs available on the bunny.net platform. The bunny.net EdgeScript Runtime is based on Deno, so you can use a subset of what is available from Deno or Node. On top of that, we provide functions that change how the script behaves in our environment or bind it to other bunny.net services. ## waitUntil The `waitUntil` function extends the life of the isolate running a request. Use it when a script needs to keep working after the request it answered has finished. Even when no other requests are routed to the script, the invocation stays alive. It is useful for holding [WebSocket](./websockets) connections open, refreshing a cache entry in the background, or firing off telemetry once the response has gone back to the client. ### Signature ```typescript theme={null} Bunny.v1.waitUntil(promise: Promise): void; ``` ### Parameters A promise representing background work. The isolate will stay alive until this promise settles (resolves or rejects). ### Returns `void`. `waitUntil` does not return a value. You can call `waitUntil` multiple times; the script will only be evicted once every given promise has been resolved. ### Example Return the response to the client immediately while a slower task, in this case populating the cache, finishes in the background. ```typescript theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve(async (request: Request): Promise => { const url = new URL(request.url); const cache = caches.default; const cacheKey = new Request(url.toString(), { method: "GET" }); const hit = await cache.match(cacheKey); if (hit) { return hit; } const fresh = Response.json( { generatedAt: new Date().toISOString(), random: Math.random() }, { headers: { "Cache-Control": "s-maxage=60" } }, ); // Don't block the response on the cache write. Let it finish after we // return; the isolate stays alive until cache.put() resolves. Bunny.v1.waitUntil(cache.put(cacheKey, fresh.clone())); return fresh; }); ``` ## References * [WebSocket](./websockets) * [Cache API](./cache) # Secrets Source: https://bunny.net/docs/scripting/secrets Environment secrets are sensitive configuration settings, such as API keys, passwords, or tokens, that are securely stored and used by your script. Environment secrets hold sensitive configuration values, such as API keys, passwords, or tokens. Your script reads them at runtime, so they never have to appear in your source code. Unlike [environment variables](/docs/scripting/environment-variables), secrets cannot be viewed once they are set. You can only update or delete them, which keeps the value hidden even from people with dashboard access. ## Adding environment secrets To add a secret to your script: 1. Log in to the [bunny.net dashboard](https://dash.bunny.net/auth/login?pk_buttonlocation=menu). 2. Select **Edge Platform**, click on **Scripting**, and select the script. 3. Select **Env Configuration** and **Environment Secrets**, and fill in the details of the secret you want to create. 4. Click **Save Secret**. ## Using environment secrets Environment secrets are accessed in the same way as environment variables, using either the Node.js process module or `Deno.env`. ```javascript theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; import process from "node:process"; BunnySDK.net.http.serve(async (request: Request): Response | Promise => { // Load secret named ApiKey const apiKey = process.env.ApiKey; // Use the secret in an API request const response = await fetch("https://api.example.com/data", { headers: { "Authorization": `Bearer ${apiKey}` } }); return new Response(await response.text()); }); ``` You can also use `Deno.env.get()`: ```javascript theme={null} const apiKey = Deno.env.get("ApiKey"); ``` Names of environment variables and secrets on the same script must be unique. You cannot use the same name for both a variable and a secret. # Fetch url Source: https://bunny.net/docs/scripting/standalone/examples/fetch-url Proxy a request to another URL with fetch, keeping the original path. This script appends the incoming request path to another domain and fetches the content from there. ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve( async (request: Request): Response | Promise => { const url = new URL(request.url); const fetchUrl = "https://example.com"; return fetch(fetchUrl + url.pathname); }, ); ``` # Route by country Source: https://bunny.net/docs/scripting/standalone/examples/geo-routing Send visitors from specific countries to a different origin using the CDN-RequestCountryCode header. bunny.net adds the [`CDN-RequestCountryCode`](/docs/cdn/vary-cache) header to every request. This script reads it and routes the listed countries to a second origin. ```typescript theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; const PRIMARY = "https://primary.example.com"; const ALTERNATE = "https://alternate.example.com"; // Countries routed to the alternate origin const ALTERNATE_COUNTRIES = new Set(["NZ", "FI"]); BunnySDK.net.http.serve(async (request: Request): Promise => { const country = request.headers.get("CDN-RequestCountryCode") ?? ""; const variant = ALTERNATE_COUNTRIES.has(country) ? ALTERNATE : PRIMARY; const target = new URL(request.url); const origin = new URL(variant); target.protocol = origin.protocol; target.host = origin.host; return fetch(new Request(target, request)); }); ``` Add or remove entries in `ALTERNATE_COUNTRIES` to change which countries get the alternate origin. # Redirect to another domain Source: https://bunny.net/docs/scripting/standalone/examples/redirect-domain Send every request to a different domain with Response.redirect. This script redirects all incoming requests to one URL with a permanent redirect (HTTP status code 301). ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve( async (request: Request): Response | Promise => { const destinationURL = "https://example.com"; const statusCode = 301; return Response.redirect(destinationURL, statusCode); }, ); ``` # Return HTML Source: https://bunny.net/docs/scripting/standalone/examples/return-html Build an HTML document in the script and include data from the request, such as the path. This script returns an HTML page and drops the request path into the markup. ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve( async (request: Request): Response | Promise => { const url = new URL(request.url); const html = ` HTML Edgescript example

Hello

there

Lorem ipsum bla bla
from ${url.pathname}
`; return new Response(html, { headers: { "content-type": "text/html", }, }); }, ); ``` # Return JSON Source: https://bunny.net/docs/scripting/standalone/examples/return-json Respond with JSON, for an API endpoint or for configuration data. This script returns a fixed JSON object with the right `content-type` header. ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve( async (request: Request): Response | Promise => { const data = { weather: "sunny", temperature: 27, windspeed: 0, uvindex: 7, }; const json = JSON.stringify(data); return new Response(json, { headers: { "content-type": "application/json", }, }); }, ); ``` # Send an email Source: https://bunny.net/docs/scripting/standalone/examples/send-email Send emails from the edge using popular email providers like SendGrid or Resend. To send an email using Resend, use the `resend` SDK. Set your Resend API key as an environment secret named `RESEND_API_KEY`. ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; import { Resend } from "https://esm.sh/resend"; import process from "node:process"; const resend = new Resend(process.env.RESEND_API_KEY); BunnySDK.net.http.serve(async (req) => { try { await resend.emails.send({ from: "sender@yourdomain.com", to: "recipient@example.com", subject: "Welcome to Bunny Edge Scripting!", text: "This email was sent directly from the edge.", }); return new Response("Email sent successfully!"); } catch (error) { console.error(error); return new Response("Failed to send email.", { status: 500 }); } }); ``` To send an email using SendGrid, use the `@sendgrid/mail` SDK. Set your SendGrid API key as an environment secret named `SENDGRID_API_KEY`. ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; import sgMail from "https://esm.sh/@sendgrid/mail"; import process from "node:process"; sgMail.setApiKey(process.env.SENDGRID_API_KEY); BunnySDK.net.http.serve(async (req) => { const msg = { to: "recipient@example.com", from: "sender@yourdomain.com", subject: "Welcome to Bunny Edge Scripting!", text: "This email was sent directly from the edge.", }; try { await sgMail.send(msg); return new Response("Email sent successfully!"); } catch (error) { console.error(error); return new Response("Failed to send email.", { status: 500 }); } }); ``` # Weighted traffic splitting Source: https://bunny.net/docs/scripting/standalone/examples/weighted-routing Send a set percentage of visitors to a second origin and keep each visitor on the same origin across requests. This script hashes a stable key (the client IP) into a 0-99 bucket and routes by percentage. The same key always lands in the same bucket, so a visitor stays on one origin across requests. ```typescript theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; const ORIGINS = { a: "https://origin-a.example.com", b: "https://origin-b.example.com", }; const B_PERCENTAGE = 10; // send 10% of visitors to origin B // Polynomial string hash (Java String.hashCode style), mapped into a 0-99 bucket function bucket(key: string): "a" | "b" { let h = 0; for (let i = 0; i < key.length; i++) { h = (h * 31 + key.charCodeAt(i)) >>> 0; } return h % 100 < B_PERCENTAGE ? "b" : "a"; } BunnySDK.net.http.serve(async (request: Request): Promise => { const ip = request.headers.get("x-forwarded-for") ?? "0.0.0.0"; const variant = bucket(ip); const target = new URL(request.url); const origin = new URL(ORIGINS[variant]); target.protocol = origin.protocol; target.host = origin.host; return fetch(new Request(target, request)); }); ``` Adjust `B_PERCENTAGE` to change the split. To pin on something other than the client IP, such as a cookie or a header, hash that value instead. # Standalone scripts Source: https://bunny.net/docs/scripting/standalone/overview Standalone scripts take the place of an origin server and handle HTTP requests directly on the bunny.net CDN network. A standalone script handles the request itself. It serves dynamic content, runs your logic, and shapes the response at the edge, which in many cases means you don't need an origin server at all. Running that close to the user keeps latency down, and a script handles many requests at once. ## Use cases * Serve REST APIs and microservices straight from the edge * Generate dynamic UIs and landing pages based on user interactions or geolocation * Build AI-powered endpoints that process images, run chat completions, or generate embeddings * Back a static frontend with contact forms, webhooks, and data submissions * Call external APIs to send emails, transform data, or integrate with third-party services ## The `serve` function The `serve` function starts an HTTP server that listens for incoming requests and handles them using the provided handler function. ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve(async (request: Request) => { return new Response("Hello from the edge!"); }); ``` ### Function signature ```ts theme={null} serve(handler: (request: Request) => Response | Promise) ``` ### Handler The handler function receives a standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object and must return a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) or `Promise`. ## Before cache execution Preview Standalone scripts act as the origin for their Pull Zone, so by default they only execute when the cache is missed. If your Pull Zone is configured to execute scripts before cache, your script runs on **every request**, before the cache lookup. You can enable it in your Pull Zone under **General** > **Origin** > **Run script before cache**, or directly in the script settings. Learn more about [before cache execution](/docs/scripting/before-cache). ## Example This example runs an A/B test based on a custom header: ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve(async (req) => { console.log(`[INFO]: ${req.method} - ${req.url}`); const origin = "bunny.net"; const newOrigin = "dash.bunny.net/auth/register"; // Retrieve scenario header value const scenario = req.headers.get("X-AB"); const url = new URL(req.url); url.port = "443"; url.protocol = "https"; if (scenario === "new") { url.host = newOrigin; } else { url.host = origin; } return fetch(url); }); ``` ### Local development You can run standalone scripts locally using Deno: ```bash theme={null} deno run -A script.ts ``` Test with curl: ```bash theme={null} # Case A - routes to new origin curl http://127.0.0.1:8080/ --header 'X-AB: new' # Case B - routes to default origin curl http://127.0.0.1:8080/ ``` # Statistics Source: https://bunny.net/docs/scripting/statistics See how often your script runs and how much CPU time it uses. The statistics section shows: * The number of times your script has been executed. * The total CPU time consumed by your script, measured in milliseconds. To view the statistics, select the **Statistics** tab in the navigation pane: # WebSockets Source: https://bunny.net/docs/scripting/websockets Use WebSockets in an Edge Script for two-way, low-latency communication between your script and connected clients. ## Enabling WebSockets To use WebSockets in your scripts, you'll first need to [enable it for your Pull Zone](/docs/cdn/websockets#enabling-websockets) in the Dashboard. 1. Navigate to your Pull Zone -> General -> WebSockets 2. Toggle the "WebSockets" switch 3. Call `request.upgradeWebSocket()` in your script to upgrade incoming connections ## Quickstart This example creates a WebSocket server that echoes messages back to the client. ```typescript theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve(async (request: Request) => { const { response, socket } = request.upgradeWebSocket(); socket.addEventListener("open", () => { console.log("Client connected"); }); socket.addEventListener("message", (event) => { console.log("Received:", event.data); socket.send(`Echo: ${event.data}`); }); const closed = new Promise((resolve) => { socket.addEventListener("close", (event) => { console.log("Client disconnected:", event.code, event.reason); resolve(); }); }); // It is important to call waitUntil; otherwise the script may be evicted // while the WebSocket connection is still open. Bunny.v1.waitUntil(closed); return response; }); ``` ## upgradeWebSocket Upgrades an incoming HTTP request to a WebSocket connection. ```typescript theme={null} const { response, socket } = request.upgradeWebSocket(); // With options const { response, socket } = request.upgradeWebSocket({ protocol: "graphql-ws", idleTimeout: 60, }); ``` ### Parameters The WebSocket subprotocol to use for the connection. The number of seconds to wait for a pong response before closing the connection. If the client does not respond within this timeout, the connection is deemed unhealthy and closed, emitting the `close` and `error` events. If no data is transmitted from the client for 2 minutes, the connection will be closed regardless of this configuration. ### Returns The response to send back to the client to establish the upgrade. The WebSocket interface to communicate with the client. ## Methods ### close Closes the WebSocket connection, optionally providing a close code and reason. ```typescript theme={null} // Close normally socket.close(); // Close with a code socket.close(1000); // Close with a code and reason socket.close(1000, "Session ended"); ``` #### Parameters A standardized [WebSocket close code](https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.5). If unset, defaults to `1000` for normal closure or `1001-1015` for error conditions. A human-readable [close reason](https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.6) explaining why the connection was closed. ### send Transmits data to the connected client. ```typescript theme={null} // Send a string socket.send("Hello, client!"); // Send JSON socket.send(JSON.stringify({ type: "message", content: "Hello" })); // Send binary data const buffer = new ArrayBuffer(8); socket.send(buffer); // Send a Blob const blob = new Blob(["Hello"], { type: "text/plain" }); socket.send(blob); ``` #### Parameters The data to transmit. Can be a string, ArrayBufferLike, Blob, or ArrayBufferView. ### addEventListener Registers an event listener for WebSocket events. ```typescript theme={null} socket.addEventListener("open", () => { console.log("Connection established"); }); socket.addEventListener("message", (event) => { console.log("Message received:", event.data); }); socket.addEventListener("close", (event) => { console.log("Connection closed:", event.code, event.reason); }); socket.addEventListener("error", (event) => { console.error("WebSocket error occurred"); }); ``` #### Parameters The event type to listen for. One of: `open`, `message`, `close`, or `error`. The callback function invoked when the event is dispatched. Optional configuration for the event listener. ## Events ### open Fired when the WebSocket connection is successfully established. ```typescript theme={null} socket.addEventListener("open", (event) => { console.log("Connected to client"); socket.send("Welcome!"); }); ``` A standard Event object with no additional properties. ### message Fired when a message is received from the client. ```typescript theme={null} socket.addEventListener("message", (event) => { const data = event.data; // Handle string messages if (typeof data === "string") { const parsed = JSON.parse(data); console.log("Received:", parsed); } }); ``` The message data sent by the client. The origin of the message. The last event ID string. The message source. Any transferred ports. ### close Fired when the WebSocket connection is closed. ```typescript theme={null} socket.addEventListener("close", (event) => { if (event.wasClean) { console.log(`Connection closed cleanly: ${event.code} ${event.reason}`); } else { console.log("Connection terminated unexpectedly"); } }); ``` The WebSocket close code provided by the server. The close reason provided by the server. Whether the connection closed cleanly. ### error Fired when an error occurs on the WebSocket connection. ```typescript theme={null} socket.addEventListener("error", (event) => { console.error("WebSocket error occurred"); }); ``` A standard Event object with no additional properties. ## References * [WebSocket Protocol](https://www.rfc-editor.org/rfc/rfc6455.htm) * [WebSocket mdn Specification](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/) * [Runtime - waitUntil](./runtime#waituntil) # Access Lists Source: https://bunny.net/docs/shield/access-lists Access Lists give you precise control over who can and cannot reach your applications. From blocking malicious proxies and anonymized botnets to allowing trusted infrastructure or internal tools, Access Lists provide real-time traffic filtering that is enforced instantly across our global edge. Access Lists decide which clients can reach your origin based on identifiers like IP, CIDR range, ASN, or country. Curated threat feeds and your own custom rules run together at the edge, so policy changes apply globally without touching your backend. ## Key Features * **Flexible Controls**: Allow, block, challenge, or log traffic based on IPs, CIDRs, ASNs, or entire countries. * **Curated Threat Lists**: Continuously updated feeds from leading reputation sources, available on Advanced, Business, and Enterprise plans. * **Custom Lists**: Build your own access policies, from trusted networks through to high-risk regions. * **Instant Enforcement**: Rules propagate globally within seconds and are applied directly at the edge without adding latency or backend complexity. ## Multi-layer Access Control Access Lists combine curated intelligence with your own rules: * **Curated threat feeds** cover VPNs, Tor nodes, abusive datacenters, and active attack sources. * **Custom rules** allow you to define access logic for IPs, CIDRs, ASNs, or countries. * **ASN-based filtering** gives you broad control over provider networks to stop botnets hiding behind hosting providers. * **Country-level rules** make it easy to allow or block entire regions. ## Access List Modes Each Access List rule gives you full control over how Bunny Shield should handle matched traffic: * **Allow**: Pass trusted traffic through without restriction. * **Block**: Drop malicious requests at the edge instantly. * **Challenge**: Trigger browser verification for suspicious traffic without blocking it outright. * **Log**: Record traffic patterns for analysis without applying enforcement. ## Action Precedence When multiple rules apply to the same request, Bunny Shield follows a defined order of precedence: 1. **Bypass**: Always takes priority. Requests that match a Bypass skip further evaluation from Bunny Shield. 2. **Allow**: Trusted traffic is passed through without restriction. 3. **Block**: Requests are denied immediately if they match a block rule. 4. **Challenge**: Suspicious requests are challenged if no higher-priority action applies. 5. **Log**: If no other action matches, requests can be logged for monitoring and analysis. This order keeps trusted traffic from being blocked and applies security actions consistently across your rules. ## Curated Threat Intelligence Bunny Shield maintains a library of continuously updated reputation feeds that are deployed instantly across the edge. These feeds protect against evolving threats without manual upkeep: * **Advanced**: VPNs, common datacenters, Tor exit nodes, FireHOL Level 1, AbuseIPDB, NetMountains * **Business**: Includes all Advanced feeds plus Blocklist DE, ThreatFox, StopForumSpam, SpamHaus DROP, and more * **Enterprise**: Includes all Advanced and Business feeds, plus support for custom feed integrations ## Custom Access Lists Custom Access Lists give you complete control over your traffic: * Allow internal ASNs or office IP ranges * Block abusive regions during sensitive launches * Challenge suspicious networks while monitoring behavior * Apply unique lists to different zones or endpoints All custom rules update globally within seconds. ## Logging and Observability Access Lists provide visibility into every decision, so you always know what is being allowed, blocked, or challenged: * **Logged events**: Requests matched by access rules without being blocked * **Actioned events**: Requests that were blocked or challenged * **Curated feed hits**: Requests flagged by built-in reputation lists ## Configuring via API Access Lists can be managed through the Bunny Shield API. This makes it simple to automate access policies, integrate with existing tools, or apply consistent controls across your infrastructure. # API Guardian Source: https://bunny.net/docs/shield/api-guardian Schema-aware API protection. Bunny Shield enforces your OpenAPI contract at the edge so requests and responses match what your application expects. API Guardian uses your OpenAPI specification as the source of truth for what your API accepts. At the edge, every request is validated against the schema, authenticated against the configured security scheme, and counted against per-endpoint rate limits before reaching your origin. ## Key Features * **Schema-Based Validation**: Upload your OpenAPI specification and turn it into enforceable validation rules that run directly at the edge. * **Full Request Coverage**: Validate path parameters, query strings, headers, cookies, and JSON request bodies against your API contract. * **Edge Authentication Enforcement**: Enforce API keys, Bearer tokens, OAuth2, and OpenID Connect validation before requests reach your origin. * **Response Validation**: Ensure responses match your schema to prevent accidental data exposure or inconsistent output. * **Per-Endpoint Rate Limiting**: Apply granular rate limits aligned with your API structure, including support for dynamic path templates. * **Integrated Protection**: Combine schema validation with targeted WAF-style inspection for injection attacks on selected parameters. ## Schema-driven request validation API Guardian starts with your OpenAPI schema and transforms it into a real-time enforcement layer at the edge. When a specification is uploaded: * The schema is normalized and processed * Internal references are resolved * Validation rules are compiled into native Bunny Shield protections Every incoming request is checked against this contract before reaching your origin. Only requests matching your defined API structure are allowed through. Validation includes: * **Path parameters** to ensure correct structure and types * **Query parameters** to validate allowed fields and formats * **Headers and cookies** to enforce required values * **JSON request bodies** (`application/json` and `*+json`) to match schema definitions ## Handling non-conforming requests API Guardian provides two layers of control over how requests that don’t match your schema are handled. ### Rule execution mode Execution mode controls how API Guardian enforces its decisions globally: * **Log** to observe validation results without blocking traffic * **Block** to enforce validation decisions at the edge This setting applies to all validation outcomes and overrides any individual blocking decision, allowing you to safely test API Guardian in log mode before enforcing it. ### Unmatched path handling Not all requests will match a defined path in your schema. The **Unmatched Path Action** controls how these requests are treated: * **Block** to reject requests targeting undefined endpoints (subject to execution mode) * **Log** to record unmatched requests while still forwarding them * **Ignore** to bypass API Guardian entirely for unmatched paths Only **Ignore** lets an unmatched request reach the Managed WAF ruleset; under **Block** and **Log**, Managed WAF is skipped. Other Shield features such as Rate Limiting and Bot Detection apply regardless of this setting. Requests that **do match** a declared path are always handled by API Guardian’s schema validation and path enforcement, instead of the Managed WAF ruleset, to avoid false positives common with structured API traffic. ### Authentication-aware path handling Unmatched path handling is not limited to literal path definitions. API Guardian automatically allows endpoints referenced by your authentication schemes, so login and token-exchange flows work without manual schema duplication. For OpenID Connect: * The provider’s discovery metadata is fetched * Advertised endpoints (authorization, token, userinfo, JWKS, etc.) are automatically allowed * Keys are kept up to date as they rotate This allows login and token exchange flows to function correctly while still enforcing schema validation across your API. ## Response validation APIs don’t just define what they accept, but also what they return. API Guardian can validate responses against your schema on a per-endpoint basis. This helps prevent: * Unexpected fields being exposed * Incorrect response formats * Edge-case behavior leaking unintended data Responses are validated before leaving your infrastructure, ensuring consistency between your API contract and real-world behavior. ## Authentication enforcement at the edge API Guardian enforces authentication requirements defined in your OpenAPI schema before requests reach your origin. Supported authentication methods include: * **API Keys** (headers, query parameters, cookies) * **HTTP Authentication** (Bearer and Basic) * **OAuth2** * **OpenID Connect** Requests missing required credentials are rejected immediately, filtering out a large portion of automated and low-effort attack traffic. ### Token validation For Bearer tokens and OpenID Connect: * **JWT structure and expiration** are validated for `bearerFormat: jwt` * **Signature verification** is performed for OpenID Connect providers using public-key algorithms * **JWKS endpoints** are automatically fetched and refreshed * Tokens must match the expected **issuer** and **signing algorithm** Supported algorithms include RSA and ECDSA (256, 384, 512-bit). Invalid, expired, unsigned, or tampered tokens are rejected at the edge before reaching your application. ## Per-endpoint rate limiting Not all endpoints behave the same, and API Guardian reflects that. You can define rate limits directly per endpoint: * Supports dynamic paths like `/users/{id}` * Applies limits at the template level across all variations * Configurable per endpoint or per IP address * Time windows from **1 second to 1 hour** Versioned endpoints (e.g., `/v1`, `/v2`) are treated independently, allowing precise control as your API evolves. ## Targeted injection protection Schema validation ensures requests are structurally correct, but some attacks rely on valid structure with malicious content. API Guardian allows you to combine schema enforcement with targeted deep inspection: * Select specific **query**, **path**, **header**, or **cookie** parameters for inspection * Detect patterns like **SQL injection** and **cross-site scripting (XSS)** * Apply deep inspection only where needed, reducing unnecessary overhead ### Schema-level inspection with `x-bunny-shield` You can also define inspection rules directly in your OpenAPI schema using the `x-bunny-shield` extension. This allows you to attach detection logic to specific fields: ```json theme={null} { "properties": { "comment": { "type": "string", "x-bunny-shield": "detectxss,detectsqli" } } } ``` * Applies to `string` schema types * Accepts a comma-separated list of detectors (`detectxss`, `detectsqli`) * Enables field-level inspection without additional dashboard configuration ## Built into the edge pipeline API Guardian runs natively inside Bunny Shield’s request processing pipeline. * No additional network hops * No external validation services * Minimal latency impact Validation happens alongside WAF, Bot Detection, and Rate Limiting, ensuring invalid traffic is filtered as early as possible. ## Limits and requirements API Guardian includes safeguards to ensure consistent performance: * **Maximum OpenAPI spec size**: 2 MB * Limits on schema complexity, nesting depth, and regex usage * Supports **OpenAPI 3.0.x** specifications Endpoint limits vary by plan: * **Advanced**: up to 10 endpoints * **Business**: up to 50 endpoints * **Enterprise**: customizable These limits help focus protection on critical and high-traffic endpoints. ## Logging & Observability API Guardian provides full visibility into validation activity: * **Validation events** logged for mismatched requests and responses * Clear insight into why a request was flagged or blocked * Integrated into Bunny Shield’s event logs for real-time monitoring This makes it safe to start in **log mode**, observe real traffic patterns, and switch to **blocking mode** once you're happy with the results. ## Configuring via API You can manage API Guardian configurations through the Bunny Shield API. This allows you to: * Automate schema uploads * Manage validation rules programmatically * Integrate API protection into CI/CD workflows By treating your OpenAPI schema as the source of truth, API Guardian enables consistent, automated security across all environments. # API Reference Source: https://bunny.net/docs/shield/api-reference Complete API reference for configuring Bunny Shield security features programmatically. # Bot Detection Source: https://bunny.net/docs/shield/bot-detection Identify and block malicious bots, including headless browsers, impersonators, and scrapers, without affecting legitimate automation or user experience. Bunny Shield identifies automated traffic using behavioral fingerprinting, header anomalies, and IP reputation. Detection rules and sensitivity profiles propagate across the edge within seconds, so policy changes take effect in near real time. ## Key Features * **Behavioral Fingerprinting**: Distinguish real users from automated traffic using fingerprinting, header anomaly detection, and request behavior modeling. * **Real-Time Response**: Detection logic and mitigation rules propagate across the edge within seconds, so policy changes take effect almost immediately. * **Customizable Rules**: Set detection conditions based on HTTP headers, IP reputation, user-agent anomalies, cookie presence, request frequency, and other signals. * **False Positive Resistant**: Allow-listing, ASN validation, and crawl integrity filters keep legitimate crawlers and automation from being blocked. ## Multi-layer request analysis Every request that reaches Bunny Shield is evaluated through multiple layers of analysis, each targeting a different way automation tries to hide: * **Request integrity checks** analyzes headers, query structures, and protocol patterns to detect spoofed or malformed requests. * **Request body inspection** for applicable methods, which will inspect payload structure and behavior to spot signs of scripted abuse. * **External intelligence** uses IP and ASN reputation, rate patterns, and global behavior history to flag known abuse sources. ## Sensitivity profiles You can choose from predefined detection profiles, each tuned to different use cases: * **Low** (*default*) catches basic bots with minimal overhead using lightweight IP and header analysis. * **Medium** applies balanced checks across IPs, headers, and fingerprint signals to detect common automation. * **High** enables strict fingerprint validation, request integrity analysis, and IP behavior scoring to stop advanced or evasive bots. * **Custom** lets you configure individual detection components for total control. ## Granular detection toggles With Custom mode enabled, you can adjust: * **Request integrity** looks for anomalies in headers, protocol usage, and request structure. * **IP address** scores requests based on IP reputation, behavior, and known rate patterns. * **Fingerprint sensitivity** determines how assertively Bunny Shield should treat unusual browser fingerprints as bots. * **Complex fingerprinting** (*Business or above*) combines advanced entropy analysis and cross-session consistency. These options let you tailor detection to match your traffic profile and risk tolerance. And with Edge Rules, you can disable bot detection dynamically based on headers, cookies, IP addresses, or specific endpoints, giving you full control over when and where protection applies. ## Logging & Observability Bot detection isn’t a black box. Bunny Shield shows you exactly what it’s seeing and doing, in real time: * **Logged requests**: Number of requests identified as bots but not challenged. * **Challenged requests**: Number of requests that triggered browser validation. We give you the full picture, with clear metrics and event logs that show what’s being flagged and how it’s being handled. No guesswork required. ## Configuring via API You can utilize the Bunny Shield API to automate Bot detection configurations or integrate them into your continuous integration and continuous deployment (CI/CD) pipelines. This capability allows you to manage your security settings efficiently and consistently across different environments. # Cookies Source: https://bunny.net/docs/shield/cookies Bunny Shield relies on cookies to support its abuse mitigation features, protect against automated bots, and maintain legitimate user access. These cookies enable the service to validate users efficiently while maintaining security and performance. Bunny Shield sets a small number of short-lived, first-party cookies that back its challenge flow, session validation, and bot detection. ## Cookie Usage Each cookie employed by Bunny Shield serves the essential purpose of ensuring that legitimate users can interact with websites protected by this service. At the same time, these cookies are instrumental in preventing abuse and minimizing the impact of bot traffic or other malicious automated behavior. As these cookies focus exclusively on verifying the authenticity of user interactions, they contribute to a secure and trusted browsing experience without overstepping into the collection of unnecessary or personal data. ## Privacy and Compliance All the cookies listed above are strictly necessary to deliver the services provided by bunny.net to our customers unless otherwise specified. bunny.net encourages customers to disclose the use of these cookies to their end users. Depending on regional legal requirements, you may need to inform your users about these cookies and the reasons behind their presence, in line with local regulations. * These cookies never track user behavior beyond verifying the browser request. They contain no personally identifiable information and are short-lived, first-party cookies used only for verifying legitimate access. ## Cookies `bunny_shield` * A temporary indicator that the user has completed the JavaScript Proof-of-Work (PoW) challenge. Once set, it lets Bunny Shield recognize validated users without re-running the challenge on every visit. `bunny_shield_chk` * A checksum or validation token that verifies the integrity of the `bunny_shield` cookie. This prevents tampering, forgery, or unauthorized changes to the validation data. `bunny_shield_id` * A persistent, first-party identifier used by Bunny Shield’s bot detection. It assigns a unique ID to each browser session so the system can distinguish legitimate users from malicious actors over time. No personally identifiable information is collected. `bunny_shield_bd` * Captures a non-invasive fingerprint of the user's browser environment, including rendering behavior, device attributes, and feature support. The fingerprint helps Bunny Shield identify automation patterns and spoofed or emulated clients while preserving user anonymity. # Creating a rate limit rule Source: https://bunny.net/docs/shield/custom-rate-limit-rule Customize Bunny Shield’s protections to throttle abuse, slow malicious bots, and absorb traffic spikes without affecting legitimate users. Rate limit rules cap how many requests a client can make to your application within a defined window. Combine them with Shield's targeting controls to throttle abuse on specific endpoints without affecting legitimate traffic. ## What you'll need Before you dive in, make sure you have the following prerequisites in place: * A [bunny.net](https://bunny.net/) account ([Log in](https://dash.bunny.net/auth/login?pk_buttonlocation=menu) or sign up for a [free trial](https://dash.bunny.net/auth/register)). * An existing Shield Zone. * Advanced Plan or above on the existing Shield Zone (if creating more than 2 rate limit rules\*). ## Creating a rate limit rule To create an effective rate limit rule, it's important to understand the fundamentals of how to build a Custom WAF Rule on Bunny Shield. Familiarize yourself with our [Rule Engine](/docs/shield/rule-engine) documentation to gain insights into how rules are structured and processed within the system. This rule processes each HTTP request by extracting only the REQUEST\_URI (**Variable**), converting it to lowercase, and removing whitespaces (**Transformations**). It then verifies if the transformed REQUEST\_URI matches exactly (**Operator**) with '/blockedpath’ (**Operator Value**). If a match is found, we increment the global rate limit counter. If the RequestCount (10 requests) is exceeded within the defined 1-second Timeframe, then our WAF Engine will block (**Response Action**) the request for the defined 30-second BlockTime, halting further rule processing and intercepting the request. With the basics covered, you can write rate limit rules that mitigate the abuse patterns specific to your site. ## Examples ### Rate limit request if a cookie is set with a specific value and exceeds a defined limit If you want to rate limit requests when a specific cookie is set to a certain value and the request rate exceeds a defined limit, you can use the following configuration: ### Rate limit request if User-Agent is a known crawler and exceeds a defined limit If you need to rate limit requests where the User-Agent header contains a known crawler identifier and the request rate exceeds a defined limit, use the following rule configuration: ## Need help or encountering issues? If you encounter any difficulties or have questions, our support team is here to assist you. Please don't hesitate to contact us via [support request form](https://bunny.net/contact/) for prompt assistance. Our dedicated support team is ready to help you resolve any issues you might face during the deployment process, provide additional guidance, or answer your questions. # Custom Response Pages Source: https://bunny.net/docs/shield/custom-response-pages Customize the challenge, block, and rate limit response pages shown to visitors when Bunny Shield takes action. Create fully branded experiences that match your website while maintaining Bunny Shield protection. Custom Response Pages allow you to fully customize the content displayed to visitors when Bunny Shield issues a challenge, blocks a request, or enforces a rate limit. Instead of generic protection pages, you can present visitors with branded experiences that match the look and feel of your website, helping maintain a consistent user experience even during security events. Custom Response Pages are available on Advanced and higher Bunny Shield plans. ## Overview By default, Bunny Shield serves clean, generic response pages whenever a request is challenged, blocked, or rate limited. With Custom Response Pages enabled, you can replace the contents of the page body with your own HTML, allowing you to: * Match your website branding and styling * Provide additional support or contact information * Explain why access was restricted * Offer troubleshooting instructions for legitimate visitors * Create localized or multilingual response pages The response status codes and Bunny Shield protection logic remain unchanged. Only the page content presented to visitors is customized. ### Supported Response Types You can customize the following Bunny Shield response pages: * **Challenge Pages** * Displayed when visitors must complete a verification challenge. * **Block Pages** * Displayed when Bunny Shield blocks a request due to security policies. * **Rate Limit Pages** * Displayed when a visitor exceeds configured rate limits. ## How It Works Bunny Shield provides a customizable HTML editor for each response page type. For security and reliability reasons, only the contents within the page's `` element can be customized. The editor already wraps your code in a `` element, so you only need to provide the content that goes inside it. Within the body, you can include your own HTML, CSS, JavaScript, and external assets to fully customize the visitor experience. ```html theme={null}
Company Logo

Security Verification Required

Please complete the verification below to continue.

``` The surrounding document structure, response handling, challenge functionality, and security controls remain managed by Bunny Shield to ensure the page continues to function correctly, while giving you complete control over the visitor-facing content and styling. ## Configuring Custom Response Pages To configure custom response pages: 1. Open your **Shield Zone**. 2. Navigate to **Settings** -> **Response Pages**. 3. Enable **White-label block pages**. 4. Click **Create custom page** for the response you'd like to customize. 5. Edit the HTML body content. 6. Save your changes. ### Response Page Configuration ### HTML Editor ## Example Use Cases ### Branded Challenge Page Add your company logo, colors, and messaging to create a seamless verification experience. ```html theme={null}
Company Logo

Security Verification Required

Please complete the verification below to continue.

``` ### Custom Block Page Provide additional information for visitors who may have been blocked unintentionally. ```html theme={null}

Access Restricted

Your request was blocked by our security systems. If you believe this is an error, please contact support.

``` ### Custom Rate Limit Page Help users understand temporary access restrictions. ```html theme={null}

Too Many Requests

You have exceeded the allowed request rate. Please wait a few minutes and try again.

``` ## Fallback Behavior If a custom response page is not configured, Bunny Shield automatically serves a generic, non-branded response page. These default pages provide a clean and professional experience without displaying Bunny.net branding. ## Best Practices When creating custom response pages, we recommend: * Keeping messaging clear and concise. * Explaining why the visitor is seeing the page. * Providing support or contact information where appropriate. * Ensuring branding matches your website. * Avoiding excessive scripts or external dependencies. * Testing pages on desktop and mobile devices. ## Frequently Asked Questions ### Can I modify the entire HTML document? No. Bunny Shield only allows customization of the content within the page's `` element. This includes any HTML, CSS, and JavaScript you wish to add, including ` ``` New (Bunny Stream Player): ```html theme={null} ``` ### 2) Rounded player and poster styling Legacy (Plyr): ```html theme={null} ``` New (Bunny Stream Player): ```html theme={null} ``` ### 3) Hide specific controls (PiP and seek) Legacy (Plyr): ```html theme={null} ``` New (Bunny Stream Player): ```html theme={null} ``` ### 4) Captions styling Legacy (Plyr): ```html theme={null} ``` New (Bunny Stream Player): ```html theme={null} ``` ## **Migration checklist** 1. Search your custom snippet for `plyr`, `.plyr__`, and `data-plyr`. 2. Replace selectors using the mapping table. 3. Search for jQuery usage `($(, jQuery) `and rewrite to vanilla JavaScript. 4. Verify on desktop and mobile, especially controls and captions.   Migrating from Plyr-specific customizations to media-chrome element-based styling gives you a cleaner and more future-friendly setup. For the best results during migration: * Use your browser’s standard developer tools (Inspect Element, computed styles, and DOM viewer) to explore the current player structure. * Use stable element selectors and CSS variables over deep internal hooks. * Validate each customization incrementally so you can quickly isolate issues. # Dashboard Source: https://bunny.net/docs/stream/dashboard The Bunny Stream dashboard is divided into several key areas, each dedicated to a specific aspect of video content management and delivery. The dashboard is the easiest way to upload videos, configure your library, customise the player, and monitor delivery. The sections below walk through each area you'll find under Stream > Your Library. ## Manage library The foundation of the dashboard, this section enables the uploading, organization, and management of video content. You can add new videos, create collections for better organization, and manage existing content with ease. **Key Features:** * **Uploading and Managing Videos** - You can upload Videos directly to the platform, allowing for immediate management and organization. Here you can also create collections to categorize content, facilitating easier access and navigation. * **Content Updates and Modifications**- You can update or remove content as necessary, ensuring the video library remains current and relevant. ## Player section This area focuses on tailoring the video player's look and functionality to match your branding or specific preferences. **Key Features:** * **Visual and Functional Customization** - Options include adjustments to the player's appearance, such as colors and fonts, and functionality, like autoplay settings and looping. * **Playback Speed** - Set custom playback speed options within your video player, giving you flexibility for custom default playback speeds and additional options. * **Brand Integration** - You can add your logo and incorporate branding elements directly into the player, promoting brand consistency and recognition. * **Watchtime Heatmap** - Enable the watchtime heatmap within your player so viewers can view the most popular parts of your video from the transport bar. * **Resumable Player** - Enable your video player to remember and resume playback where the video was last left when viewers leave and return to your video experience. ## Advertising The Advertising section allows you to monetize your content by displaying VAST-compatible ads within your videos. **Key Features:** * **Ad Setup and Configuration** - You can easily embed advertisements, customize their appearance and timing, and adjust settings to fit your monetization strategy. ## Encoding options In the Encoding section, you can manage the process of converting your uploaded videos into different formats and resolutions to ensure optimal playback across devices and bandwidths. **Key Features:** * **Automated Encoding and Customization** - Automatic conversion of uploaded videos is supported, along with manual settings for resolution and bitrate adjustments. * **Watermark feature** - You can apply watermarks to your videos for branding purposes. * **Keep Original Files** - This is the original file of your video, which is useful if you want to store your original files with us as well, along with the encoded ones. * **Enable Early-Play** - Enabling Early-Play can come in handy if you wish to play your video before all the resolutions are done transcoding. Using this option will use the resolution that your original video comes with and will play the original file, which in turn, will expose the original file to the public. Do not toggle this on if you do not wish the original file to be accessible. * **Enable Content Tagging** - Content tagging is still a preview at this point and will only tag videos at the moment with their associated content. ## Transcribing and accessibility The Transcribing feature offers a suite of tools for automatically generating and managing subtitles and captions. This functionality not only improves accessibility for a wider audience but also boosts viewer engagement by providing a textual representation of the audio content. **Key Features:** * **Automatic Transcription** - Automatically generates subtitles and captions for videos, eliminating the need for manual transcription efforts. * **Transcription Translation** - Offers the capability to automatically translate the generated subtitles and captions into multiple languages. ## Security features Ensure the protection of your video content through the Security section, which encompasses a range of features designed to control access and secure your videos. **Key Features:** * **Token Authentication** - Use token-based authentication to secure your videos. This method ensures that only viewers with a valid token can access your content, providing an additional layer of security. * **Domain Restriction** - Control where your video content can be played by specifying allowed domains. This feature prevents your videos from being embedded or played on unauthorized websites. * **Block Direct URL File Access** -You can enable this setting to prevent direct access to your video files without a proper referer header. This measure blocks attempts to access and download your content directly via its URL, offering an additional layer of protection against unauthorized distribution. These settings combine. Enabling **Block Direct URL File Access** without populating **Allowed domains** will return a 403 on thumbnails, previews, and direct file URLs even when the embed iframe still works. Each library's security settings are independent — duplicating a library does not copy its security config. See [Security Options](/docs/stream/security-options) for the full breakdown. ## Delivery optimization The Delivery section of the Bunny Stream Dashboard is designed to help you manage how your video content is distributed and accessed worldwide, ensuring optimal performance and cost-efficiency. **Tier Selection** **Standard Tier**: This option is tailored for smaller websites where performance is paramount. It's designed to ensure every millisecond counts, making it ideal for scenarios where high-speed content delivery is critical. **High Volume Tier**: Suited for delivering large files, this tier is targeted at users who require a cost-effective solution for global high-bandwidth distribution. It offers the best price-to-performance ratio, making it an optimal choice for extensive video libraries needing wide-reaching access. **Key Features**: * **Routing Filters** - Routing Filters provide a means to precisely control the distribution pathway for your video content across Bunny.net's Content Delivery Network (CDN). ## Storage management In the Storage section you can manage your video storage, keeping track of your usage and optimizing storage allocation. **Key Features:** * **Efficient Use of Resources** - You can manage the storage space and implement the geo-replication to ensure content is stored and accessed effectively. ## Performance statistics The Statistics section offers a comprehensive suite of analytics tools designed to provide insights into the performance of your video content. It allows you to measure viewer engagement and the overall reach of your content, allowing for informed decisions to enhance your video strategy. **Viewer Analytics** * **Views Tracking** - Monitor the number of views your videos receive to gauge their popularity and reach. * **Watch Time** - Assess the total time viewers spend watching your videos, providing insights into engagement levels. * **Viewer Demographics**-  Understand your audience by analyzing demographic data, such geographic location.  **Performance Metrics** * **Video Delivery Performance**- Evaluate the efficiency and speed of your video content delivery. This metric helps identify potential bottlenecks or issues affecting viewer experience. * **Optimization Opportunities**- Analyze performance data to uncover opportunities for improving content delivery and optimizing viewer engagement. This could involve adjustments to video quality, encoding settings, or content distribution strategies. ## API and webhook integration The API section is designed to provide you the access to the Bunny Stream API, facilitating the integration and automation of video streaming functionalities within applications. **Key Features:** * **Access Credentials**- Here you can find information about your Video library ID, CDN Hostname, Pull Zone, and Api Key. * **Webhook URL**- You can set up a Webhook URL to receive automatic callback notifications. *** [Tutorials](/docs/stream) [Upload video from URL](/docs/stream/url-fetch) * [Table of Contents](#) # Data and Privacy Source: https://bunny.net/docs/stream/data-and-privacy At bunny.net, we take data privacy seriously. We’re committed to being fully transparent about the information we collect, how we use it, and how we keep it secure. ## Bunny Stream & Bunny Player Bunny Stream and Bunny Player are designed to avoid collecting or permanently storing user-identifying information for analytics or playback functionality. The player does not set cookies or transmit personally identifiable information (PII) as part of its playback or analytics features. To ensure optimal performance and reliability, the player may send anonymised technical telemetry, such as stream quality metrics, error reports, and buffering statistics. In addition, the embedded player (iframe) may communicate with bunny.net CDN endpoints worldwide to collect anonymous performance statistics. This data is used exclusively to improve routing decisions and automatically optimise streaming performance. ## Resumable Playback Resumable playback is managed entirely within the user's browser using Browser Local Storage on the user's device. No playback information is shared with or stored by bunny.net . Stored playback data is retained locally for up to seven (7) days, after which it is automatically deleted. The playback position is updated every second during viewing and is also saved when the user closes the browser tab or the page is unloaded. The playback position is not saved if the video is within three (3) seconds of completion. This design allows users to resume playback while ensuring that all playback data remains stored exclusively on the user's device. Playback data is never transmitted to or stored by bunny.net . Any technical telemetry transmitted by the player is anonymized and used solely to monitor service health and optimize streaming performance. It is never used for user tracking or identification. For more information check out our [GDPR](https://bunny.net/gdpr/) and [Privacy & Data](https://bunny.net/privacy/) policies. # Delivery Tiers Source: https://bunny.net/docs/stream/delivery-tiers From a configuration and technical perspective, there is practically no difference between Standard and Volume zones. Where Standard and Volume tiers do differ, is the network performance and the amount of PoPs used when routing your users. Bunny Stream's delivery is split into two tiers — Standard and Volume — that differ only in the routing network used to reach viewers. Pick the tier that matches your latency and cost requirements. ## Standard Tier Standard tier offers exceptionally low global latency aimed at high-performance solutions where every millisecond matters, such as website acceleration or ad delivery. The Standard tier gives you access to the full Bunny.net network and is one of the best performing networks around the globe. We bill this tier based on the region you serve traffic in, please see the diagram below: ## Volume Tier The Volume tier uses a smaller number of PoPs, that were carefully selected and optimized to offer a good performance at a fraction of the cost. The Volume tier is perfect for large file delivery, such as video, software delivery or other downloads. We are constantly monitoring the network for performance and make adjustments as needed in order to provide the best possible speeds on the Volume tier. Please see our [Network](https://bunny.net/network) page for a full listing of both of our Standard and Volume PoPs. We offer a flat rate of \$5 per TB served on this tier. # MediaCage DRM Source: https://bunny.net/docs/stream/drm/index MediaCage DRM (Digital Rights Management) solutions are designed to safeguard your digital content and protect it from unauthorized access and distribution. Bunny Stream offers two distinct media content protection tiers: **MediaCage Basic DRM** and **MediaCage Enterprise DRM**. Each product is tailored to meet different security needs, providing you with the flexibility to choose the level of protection that aligns with your specific requirements. | Feature | MediaCage Basic DRM | MediaCage Enterprise DRM | | ----------------------- | ------------------- | ------------------------ | | Browser support | ✅ | ✅ | | Native platform support | | ✅ | | Clear key encryption | ✅ | | | Secure key management | | ✅ | | Download protection | ✅ | ✅ | | Screen grab protection | | ✅ | | Custom player support | | ✅ | \**Screen Grab Protection is not available on Widevine clients with L3 level of security. For a deeper understanding, refer to [Widevine DRM Security Levels](/docs/stream/widevine-security-levels).* ## MediaCage Basic DRM **MediaCage Basic DRM** is a basic protection solution and it serves as an entry-level digital content protection solution. It employs innovative dynamic encryption, ensuring that your content is encrypted on demand. This encryption process occurs internally, securing your content as it traverses the network to reach your users. ### Key features * **Dynamic encryption**: Utilizes an innovative dynamic clear key encryption for on-demand content protection. * **Download prevention**: MediaCage Basic DRM employs a special mechanism to prevent download of media files. * **Session-based content key protection**: The content is encrypted with a unique key for each play session. **MediaCage Basic DRM** is suitable for scenarios where basic content protection is required. It is an innovative solution for users seeking entry-level security measures for their digital content. For premium, industry or pay-per view video applications and platforms we recommend Mediacage Enterprise DRM for full protection. ## MediaCage Enterprise DRM **MediaCage Enterprise DRM** is an advanced protection feature which represents our advanced digital content protection solution, offering an elevated level of security for enterprise-level requirements. It incorporates underlying industry standard technologies ([FairPlay Streaming](https://developer.apple.com/streaming/fps/) and [Widevine](https://www.widevine.com/)), providing enhanced encryption and protection features. **Enterprise Media Cage DRM** is designed to meet the stringent security needs of organizations that demand the highest level of content protection. In comparison to our MediaCage Basic DRM, the MediaCage Enterprise DRM protects the content also on client devices using hardware vendor support for protecting content key during the playback. ### Key features * **Automatic encryption during packaging**: Content is automatically protected during the transcoding process. * **Advanced encryption technologies**: Utilizes FairPlay (Apple) and Widevine (Google) for superior content encryption. * **Hardware-based key exchange**: Ensures secure content key is not exposed on client devices. * **Screen grab protection**: Prevents screenshots and screen recordings, so that content cannot be copied. Bear in mind that Screen Grab Protection is not available on Widevine clients with L3 level of security. For a deeper understanding, refer to [Widevine DRM Security Levels](/docs/stream/widevine-security-levels). * **Premium content licensing**: Ensures that a valid play license is issued only to authenticated clients that meet protection level requirements. * **Adherence to industry standards**: Meets advanced industry standards for digital content protection. Tailored for Enterprise Security: Specifically designed to address the security needs of enterprise-level users. * **Multi-key DRM**: Multi-key DRM licensing is implemented on a per-playback-device basis, allowing multiple licenses to be issued for a single piece of video content. For example, a single playback device may receive multiple separate licenses. One for the video stream, 2 for the accompanying translated stereo audio tracks and these licenses may then renew due to total video duration; resulting in 6 DRM licenses being issued and billed for one video playback. **Media Cage Enterprise DRM** is the ideal choice for organizations with high-security demands, seeking advanced protection mechanisms and adherence to industry standards. It is suitable for scenarios where content integrity is of utmost importance, and enterprise-level security is a priority. **Supported browsers for Widevine and Fairplay:** | Browser | Widevine | FairPlay Streaming | | -------------- | -------- | ------------------ | | Microsoft Edge | ✅ | | | Chrome | ✅ | | | Firefox | ✅ | | | Safari | | ✅ | | Android | ✅ | | | iOS | | ✅ | ### FairPlay streaming **FairPlay streaming**, a technology developed by Apple, is an underlying component of our MediaCage Enterprise DRM solution. It provides a highly secure mechanism for content protection on Apple devices. This hardware-centric approach significantly enhances the overall security of your content, limiting key exchanges to Apple devices, including Macs, iPhones, iPads, and Apple TV. To learn more about FairPlay streaming, see the official [FPS website](https://developer.apple.com/streaming/fps/). To use FairPlay Streaming to deliver your content you have to request the FPS Deployment Package from Apple. You can get more information about the process in our FairPlay Streaming Certificate Registration guide. ### Widevine Widevine provides digital media solutions for the delivery of digital entertainment to a wide range of device platforms. Widevine is integrated into our MediaCage Enterprise DRM solution. The technology is built-in a number of device platforms such as Windows, Android, ChromeOS and browsers such as Chrome, Firefox, Edge... To learn more about Widevine technology, see the [official Widevine documentation portal](https://developers.google.com/widevine). ### Pricing #### One simple plan: \$99/month plus DRM-license fees Our unique pricing model consists of a base fee of \$99 per month, coupled with additional costs for DRM-license fees. This approach allows you to pay only for what you use, ensuring that your costs are always aligned with your actual consumption of DRM licenses. This means you only pay for the licenses you use, in addition to the monthly base fee. We employ a tiered pricing strategy for DRM licenses to provide you with the most cost-efficient solution possible. The cost per license decreases as your volume of licenses increases, as detailed below: | Licenses/Month | Cost per license | | ------------------- | ------------------------------------ | | Up to 20,000: | \$0.005 | | 20,001 to 100,000: | \$0.004 per license | | 100,001 to 500,000: | \$0.003 per license | | Over 500,000: | Please contact us for custom pricing | This usage-based model allows for significant flexibility and control over your costs, making it easier to budget and plan for your DRM needs. Enterprise DRM ensures that videos are encrypted during the transcoding process and stored securely in the storage. **Multi-key DRM** For example, a single playback device may receive multiple separate licenses. One for the video stream, 2 for the accompanying translated stereo audio tracks and these licenses may expire and issue new licenses due to the total video duration; resulting in 6 DRM licenses being issued and billed for one video playback. #### What counts as a DRM-license? A DRM license is issued each time a user initiates playback of a video that requires a DRM (Digital Rights Management) license for decryption. Our system is designed to count licenses in a manner that ensures fairness and cost-effectiveness: * Multi-key DRM licensing is implemented on a per-playback-device basis, allowing multiple licenses to be issued for a single piece of video content per playback device. The video and audio tracks can be issued a separate license. * Each unique VoD (Video on Demand) content playback on a device counts as one DRM license. * SD videos or audio only streams do not incur DRM license fees. **Examples:** * **Multiple browser playback**: If a user plays back a video in HD and then restarts the VoD session in a different browser, that could count as 4 licenses. * **Playlist viewing**: Watching a playlist with 5 HD VoD videos results in 5 DRM licenses, one for each video. * **Resolution upgrade**: A video played in UHD typically incurs 2 DRM license requests if the player initially starts in SD or HD before switching to UHD. * **SD resolution**: Watching a video in SD resolution does not count towards DRM license usage. #### Monthly cost calculation example To illustrate, if 10,000 users each watch 15 HD videos in a month, this results in 150,000 DRM licenses. Applying the tiered pricing model, the cost for 150,000 licenses amounts to \$669. **Example:** (20,000 x 0.005) + (80,000 x 0.004) + (50,000 x 0.003) = 100 + 320 + 150 = \$570 + \$99 base fee = **\$669** Customizing or modifying the Bunny Player does not weaken core DRM protection. Encryption, key exchange, and download prevention are enforced by our back-end and the underlying DRM (Widevine/FairPlay) regardless of the player used. **Screen capture (screenshots/screen recording) cannot be fully prevented on lower-security devices** e.g. Widevine L3. As is standard across the industry, DRM limits these devices to a lower resolution (480p), so any capture is capped at SD quality if set by the User. Available security levels are determined by the client and User, which is outside of Bunny's control. See [Widevine DRM Security Levels](/docs/stream/widevine-security-levels). # Embedding videos Source: https://bunny.net/docs/stream/embedding Bunny Stream is designed for developers and content creators to easily upload, process, and display videos within any application or website. Once your videos have been processed, you can embed them by using our lightweight embed player, enabling seamless playback across all devices and browsers. Once a video has finished processing, you can embed it on any page using the Bunny Stream player. This page covers the iframe URL pattern, sizing tips, and code samples for common frameworks. ## How to embed a video? Embedding a video involves placing an ` ``` ```javascript JavaScript theme={null} ``` The embed code for a particular video can be generated on the video details page in the dashboard. ## Parameters By default, many of the player’s controls and behaviors, such as captions, autoplay, and preload, can be configured on a global scale within the Player tab of your Bunny Stream library settings. Adjusting these default configurations ensures that any video embedded from that library inherits the same baseline behavior, providing a consistent user experience across all embedded instances. However, you can also fine-tune or override these defaults on a per-embed basis by adding query parameters directly to the embed or direct play URL. This flexibility allows you to tailor the playback experience for specific pages or use-cases without affecting the global settings of your library. ## Supported Parameters Below is a concise table summarizing each parameter, its possible values, a brief description, and the default value. | **Parameter** | **Values** | **Description** | **Default Value** | | ---------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | | **`autoplay`** | `true`, `false` | Controls whether the video should start playing automatically. Due to browser restrictions on auto-play, this may not always work. | `true` | | **`captions`** | `caption short-code string`
`-auto` | Controls the default captions file that will be shown. By passing the caption code, the player will automatically load these captions. By default, the player remembers the user's last selection or does not load captions. Use in combination with `-auto`after the country code for the captions to display automatically on playback. | *N/A* | | **`preload`** | `true`, `false` | Controls whether the video files are preloaded. When set to `true`, the player immediately starts downloading the video so playback begins more quickly. | `true` | | **`t`** | `Xs` ,`1h20m45s` , `hh:mm:ss`
, `numeric` | Sets the video start time. Supports multiple formats: hours/minutes/seconds notation, `hh:mm:ss` format, or a simple numeric value interpreted as seconds. | *N/A* | | **`chromecast`** | `true`, `false` | Enables or disables Chromecast support within the player. | *N/A* | | **`disableAirplay`** | `true`, `false` | Disables AirPlay support when set to `true`. | *N/A* | | **`disableIosPlayer`** | `true`, `false` | Disables the native iOS player (used typically for fullscreen handling on iOS). | *N/A* | | **`showHeatmap`** | `true`, `false` | Displays a heatmap on the progress bar when set to `true`, highlighting viewer engagement at different points in the video. | *N/A* | | **`muted`** | `true`, `false` | If set to `true`, the player starts in mute mode (no sound). This is often used for autoplay scenarios. | *N/A* | | **`loop`** | `true`, `false` | Replays the video automatically after it ends, creating a continuous loop. | *N/A* | | **`playsinline`** | `true`, `false` | Allows the video to play inline on mobile devices rather than forcing fullscreen playback. | *N/A* | | **`showSpeed`** | `true`, `false` | Shows a speed control within the player, allowing playback rate adjustments. | *N/A* | | **`rememberPosition`** | `true`, `false` | Controls whether the player remembers the last playback position for the viewer within their browser. If set to `true`, the player stores the last position and attempts to resume from that point when the viewer returns. When `false`, the video always starts from the beginning. | `false` | | `compactControls` | `true`, `false` | Enables compact UI view for the bunny player, allowing more space for video on desktop and mobile browsers. | `false` | ## Direct play URL query parameter example ```bash theme={null} https://player.mediadelivery.net/play/{video_library_id}/{video_id}?t=30s ``` This example uses a query parameter which means the direct play URL will start video playback at 30 seconds. ## Player keyboard shortcuts Bunny Stream player has support for the following keyboard shortcuts. | Key | Action | | ------ | ------------------------------------ | | 0 to 9 | Seek from 0 to 90% respectively | | space | Toggle playback | | K | Toggle playback | | ← | Seek backward by the seekTime option | | → | Seek forward by the seekTime option | | ↑ | Increase volume | | ↓ | Decrease volume | | M | Toggle mute | | F | Toggle fullscreen | | C | Toggle captions | | L | Toggle loop | ## Custom integration If you prefer to use your own player or a fully custom video integration, you can directly access the raw video files from Bunny Stream’s infrastructure. Please refer to our [Video Storage Structure](/docs/stream/storage-structure) documentation for details on how to retrieve and utilize video files directly. # Encoding Source: https://bunny.net/docs/stream/encoding bunny.net Stream platform provides users with powerful and, most of all, free transcoding of their videos. It allows users to convert their videos into multiple desired resolutions for maximum compatibility with the end-user who watches them, as our player intelligently selects the best resolution to play to the user, therefore minimalizing buffering on unstable and/or poor connections. Encoding aside, we also provide an abundance of highly customizable options with which you can further tweak the end result of the videos. This document describes the configuration settings for Bunny Stream video encoding, and helps you configure them for the best results. ## Where can I find these settings? The Bunny Stream Video Library encoding settings can be found within the Bunny Dashboard. It is under Stream > Your Library > Encoding. ## Understanding the options offered ## Enabled resolutions Configures the list of resolutions that the uploaded videos will be transcoded to. Each enabled resolution will provide the user with the ability to switch the playback into that resolution. Enabling a wide range of resolutions gives your end user a better experience by adapting to various connection speeds and player configurations. However, each additional resolution will also consume additional storage to hold the transcoded video files. ## Resolution bitrates Configures the list of resolutions that the uploaded videos will be transcoded to. Each resolution comes with a pre-tuned bitrate that provides a great ratio of picture quality and bandwidth usage, but you can fine-tune this by clicking on the bitrate number in the Enabled Resolutions list. Then, you can configure the specific bitrates in which your videos will be encoded in. Each enabled resolution is a separate encoding job, so a library with five resolutions enabled takes proportionally longer to process than one with two. The same applies if MP4 fallback or multiple output codecs are turned on. The standard transcoding queue is shared and fairly scheduled, so encoding may also be briefly delayed during peak periods — this is normal. If a video stays in **Processing** for more than a few hours, contact support. For predictable, priority encoding times, see [Premium Encoding](/docs/stream/premium-encoding). ## Keep original files This is the original file of your video, which is useful if you want to store your original files with us as well, along with the encoded ones. In addition, the re-encoding of existing videos can only be performed if Original Files are kept with us. It also allows you to download the original files of your videos again, if you accidentally deleted them locally on your computer or server. Please remember that for an original file to be kept, this option must be enabled before you upload a video. ## Enable Early-Play Enabling Early-Play can come in handy if you wish to play your video before all the resolutions are done transcoding. Using this option will use the resolution that your original video comes with and will play the original file, which in turn, will expose the original file to the public. Do not toggle this on if you do not wish the original file to be accessible. ## Enable Content Tagging Content tagging is still a preview at this point and will only tag videos at the moment with their associated content. The current existing categories that it can assign a video with are the following; * Adult * Animals Birds * Animals Cats * Animals Dogs * Animated * Anime * Gaming * Graphics * Illustrations * Movie * Other * Other People * Sports Basketball * Sports Hockey * Sports Other * Sports Soccer * Sports Tennis Please keep in mind that for a video to be tagged, this option **has to be enabled** before you upload a video. In the case that you are curious about the more specific details of how Content Tagging works, we would recommend reading the [Tagging](/docs/stream/tagging) article. ## Enable MP4 Fallback Generates MP4 videos up to 240p, 360p, 720p & 1080p in resolution. Note that this option must be enabled before uploading a video, as the MP4 video is only generated during encoding. [For more information more about how to construct an MP4 URL](/docs/stream/storage-structure#mp4-fallback-url). ## Enabled Resolutions The following option allows you to decide which resolutions you want your video to be encoded in. This is extremely useful when it comes to maximizing compatibility with your users, as you can provide them with several resolutions to choose from depending on their internet connection speed and quality. Just like with the other options, the resolutions need to be configured **before a video is uploaded.** In addition to being able to choose resolutions to be encoded in your videos, you can also tweak around with the bitrate of the videos themselves by clicking on the bitrate value next to the resolution. Bitrate allows you to control the video image quality, the higher it is, the better the quality will be and the more space on storage will the videos will consume. ## Watermark This allows you to set the watermark for the video library. A watermark will be applied to each video that is uploaded. This option must be enabled before you upload a video for the watermark to appear on the video itself. ## Video storage usage You can view how much storage your encoded video is using via clicking the (i) information icon found on your video summary within your video library, or alternatively via the video info (i) information icon found on the video page. A summary will appear with a breakdown of your video storage usage giving per-video breakdown of your storage consumption: * Codec (H264, H265, AV1, VP9) * MP4 (MP4 fallback) * Original (Keep original files) * Other (Preview animations, thumbnails, watermarks and captions) * Total [Video storage size info can also be provided over API.](/docs/api-reference/stream/manage-videos/get-video-storage-size-info) ## Why are my videos taking up more space than they should? You may notice that once a video is uploaded, it consumes a larger amount of storage space than it is using on your local machine. This would be due to the encoding options that you've set under the Encoding Tab of your Video Library, you can find the options that increase the size below; `MP4 Fallback` `Keep Original Files` `Enabled Resolutions` If you wish to reduce the size of the final encoded video, you may want to look at which resolutions and/or options you need, whilst disabling the ones you don't need. Please keep in mind that videos only uploaded after the changes will be affected by the change. # Apple FairPlay Streaming Source: https://bunny.net/docs/stream/fairplay-deployment FairPlay Streaming is Apple's robust digital rights management (DRM) solution, ensuring secure and protected content distribution for your applications on Apple devices. In this guide, we will walk you through the process of obtaining the FPS Deployment Package. FPS Deployment Package consists of: * FPS Certificate file (.der or .cer) * Private key file (.pem) * Private key password string * Application secret key (ASK) string ## Signing up for an Apple developer account To get the FPS Deployment Package you will need to create an Apple Developer account. If you don't have one, follow the steps below: 1. Open your web browser and go to the [Apple Developer enrollment page](https://developer.apple.com/support/enrollment/). 2. Click on the **Enroll** button to begin the account creation process. 3. Fill in the required information, including your Apple ID or create a new one, and follow the on-screen instructions to complete the account setup. ## Requesting deployment package To request the Deployment Package, follow the steps below: 1. Navigate to the [FairPlay Streaming website](https://developer.apple.com/streaming/fps/). 2. Scroll down to the bottom of the page and click on the **Request Deployment Package** link. 3. You will be redirected to a login page. Log in with your Apple Developer account credentials. Note that to request the deployment package, you need to be an account holder. 4. Fill out the deployment package request form with the necessary information. During the application process, you may be asked about the implementation and testing of the Key Server Module (KSM). Answer accordingly, stating that you are using a 3rd party DRM company and that the company has already built and tested KSM. 5. Submit the deployment package request and wait for Apple's confirmation, usually taking few days. 6. Once your request is confirmed, Apple will issue a package containing the FPS Credential Creation Guide document. ## Creating a private key and Certificate Signing Request (CSR) If you haven't already installed OpenSSL, download and install it on your PC or server. You can find OpenSSL packages for various operating systems on the official OpenSSL website. 1. Open a terminal or command prompt and run the following command to generate a private key. You will be prompted to enter a password. Choose a password (shorter than 32 characters) and make a note of it for future reference. ```text theme={null} openssl genrsa -aes256 -traditional -out privatekey.pem 1024 ``` 2. Now, you can create a CSR file using the private key generated in the previous step. Run the following command: ```text theme={null} openssl req -new -sha1 -key privatekey.pem -out certreq.csr -subj "/CN=SubjectName/OU=OrganizationalUnit/O=Organization/C=US" ``` Replace the values in the `/CN=SubjectName/OU=OrganizationalUnit/O=Organization/C=US` field with your specific organization details (this information will be embedded in the CSR). 3. During the CSR generation process, you will be prompted to enter the password for the private key created earlier. Enter the password that you set in Step 2. 4. Verify that the private key (privatekey.pem) and the certificate signing request (certreq.csr) files have been successfully created in the current directory. You have now successfully created a private key and a CSR using OpenSSL. These files are essential for obtaining a digital certificate from a Certificate Authority. Keep the private key secure and share the CSR with the CA to complete the certificate issuance process. ## Creating FPS certificate at Apple developer portal The next step is to create FPS certificate. Follow the steps below: 1. Log in to Apple Developer Portal. 2. After logging in, locate and click on the **Certificate, IDs & Profiles** menu. 3. On the **Certificate, IDs & Profiles** menu, click the "+" button. This will take you to the **Create a New Certificate** screen. 4. In the **Create a New Certificate** screen, select **FairPlay Streaming Certificate** from the available options, and click the **Continue** button to proceed. 5. Click on the **Choose File** button and select the certreq.csr file that was created in the previous step, and click **Continue**. 6. The screen will display the Application Secret Key (ASK) string. Copy this key, pase it in a secure location for future reference, and click **Continue**. 7. A pop-up will appear to confirm that you have recorded the ASK string separately. Confirm that you have recorded the ASK string, and then click the **Generate** button. 8. Once the generation is complete, the FairPlay Streaming certificate will be displayed in the Certificate list. Locate the certificate and click the **Download** button to save the FPS certificate file (fairplay.cer) to your computer. If asked to select a Fairplay Streaming SDK version during the above steps, select SDK 4.x. We currently only support Apple FairPlay SPC v1 (Server Playback Context). If you encounter any errors during this process, please reach out to Apple support for assistance. *** [MediaCage Enterprise DRM](/docs/stream/mediacage-enterprise) [Google Widevine DRM](/docs/stream/widevine-security-levels) * [Table of Contents](#) # HTTP Source: https://bunny.net/docs/stream/http-api Upload videos to Bunny Stream using the HTTP API. From creating the video object to sending the actual video content, you’ll learn how to complete the process smoothly. ## What You’ll Need Before you begin, ensure you have: * A [bunny.net](https://bunny.net) account ([Log in](https://dash.bunny.net/auth/login?pk_buttonlocation=menu) or sign up for a [free trial](https://dash.bunny.net/auth/register)). A video library already created. It can be done **via Dashboard** (Follow "Step 1" in the [Stream Quickstart](/docs/stream/quickstart) to create a video library. An incorrect library ID will result in upload failure) or **via API** (use the "Add Video Library" HTTP call explained in the [Stream API Reference](/docs/api-reference/stream) guide). Have your Stream API key prepared. To learn how to obtain your Stream API key, see [Obtaining your Stream API key](/docs/stream/authentication). ## Understanding the Upload Process Uploading a video through the HTTP API involves **two main steps:** 1. **Create the video object** in your library (metadata and title). 2. **Upload the video file** in **binary format** using a PUT request. ## Creating a Video Before uploading any video content, you must first **create a video object** in your library. This step is often overlooked, so please ensure it’s completed before proceeding with the upload. > This step tells Bunny Stream that you’re about to upload a new video and gives it a title and optional metadata. It doesn’t upload the video yet, just sets up the “slot” for it. You can find full details about the available parameters in the [Stream API Reference](/docs/api-reference/stream). ## How to Create a Video: Make an authenticated POST request to the following endpoint: ```bash theme={null} POST https://video.bunnycdn.com/library/:libraryId/videos ``` **Headers:** * `AccessKey`: Your **Stream API key** * `Accept: application/json` * `Content-Type: application/json` **Body Parameters:** * `title`: The name of your video. This will appear in the library UI. * `collectionId` (optional): The ID of a collection in your library where this video should be placed. * `thumbnailTime` (optional): The time (in milliseconds) to use when extracting the main video thumbnail. **Example request:** ```bash theme={null} curl --request POST --url https://video.bunnycdn.com/library/:libraryId/videos --header 'AccessKey: ' --header 'accept: application/json' --header 'content-type: application/json' --data '{"title":"test"}' ``` **Example Response:** The response will return a new video object, including: * A unique videoId (guid) * The libraryId * Basic metadata such as title and creation date At this stage, most fields (like encoding status or view count) will be empty or zero since the file hasn’t been uploaded yet. You’ll need the `videoId` from this response in the next step. ## Uploading the Video File Once the video object is created, you can proceed to upload the actual video file. Full API reference: [Stream API Reference](/docs/api-reference/stream). **Upload Endpoint:** ```bash theme={null} PUT https://video.bunnycdn.com/library/{libraryId}/videos/{videoId} ``` **Required Parameters:** libraryId: Your video library ID (retrieved as before). videoId: The unique ID returned when you created the video object. You can also find this ID in the Bunny dashboard under the “Video ID” field for any listed video. **Example curl request:** ```bash theme={null} curl --request PUT --url /videos/ --header 'AccessKey: ' --header 'accept: application/json' --data-binary '@path/to/your/video.mp4' ``` **Example Response:** ```json theme={null} { "success": true, "message": "OK" } ``` This response indicates that the video was uploaded successfully. A **401 Unauthorized** on create or upload usually means the `AccessKey` header is missing, contains a non-Stream key (e.g. an account-level API key), or is set against the wrong library — each Stream library has its own API key, found under Stream > Your Library > API. Upload calls authenticate per-library, so a key from a different library will be rejected even if it's valid. For files over **2 GB** or any upload over an unstable connection, prefer [TUS resumable uploads](/docs/stream/tus-resumable-uploads) instead of this direct PUT. The HTTP API has no resume support — if the connection drops partway, the whole upload restarts. If a dashboard upload appears stuck, the "Uploading" state reflects your client's upload progress, not server-side processing; check your upload speed and try renaming the file before re-uploading to reset the upload context. **File Format Requirements:** When uploading your video file, it must be sent as **raw binary data,** not encoded or wrapped in JSON. Don’t base64 encode the file Send the video as binary in the body of the request **cURL Tip:** Use the "--data-binary" flag to send the file correctly ```bash theme={null} \--data-binary '@video.mp4' ``` If this step is skipped or misconfigured, the upload may silently fail or the video will not process correctly. ## Fetching Videos from a Remote URL (Optional) If you already have a video file hosted on another platform or server, [bunny.net](https://bunny.net/) allows you to fetch it directly using a remote URL, saving you the trouble of downloading and re-uploading manually. The documentation link to this can be found in the [Stream API Reference](/docs/api-reference/stream). The URL you provide must be accessible by bunny.net's fetch system. You can use the optional `headers` parameter to include custom HTTP headers with the fetch request, enabling authentication methods such as: * **Basic Authentication** (via the `Authorization` header) * **Static API keys** (via custom headers like `X-API-Key` or `Authorization: Bearer `) However, more advanced authentication mechanisms such as signed URLs, expiring tokens, or time-limited access links are not supported, as the fetch is performed as a single server-side request and cannot handle dynamic token generation or multi-step authentication flows. See the [Fetch Video API Reference](/docs/api-reference/stream/manage-videos/fetch-video) for details on the `headers` parameter. **Endpoint:** ```bash bash theme={null} POST ``` **Example Request:** ```bash theme={null} curl --request POST --url /videos/fetch --header 'AccessKey: ' --header 'accept: application/json' --header 'content-type: application/json' --data '{"url": ""}' ``` Once fetched, the video will be automatically added to your library and begin processing like a normal upload. ### Queued Fetch Limit The fetch API supports a limited number of queued fetch requests per user at a time; this limit may vary by region, time, current load, and other factors. If you exceed the limit before previous fetches complete, the API will return **HTTP 429 (Too Many Requests)**. To avoid this, either wait for in-progress fetches to finish before submitting new ones, or implement retry logic with backoff when you receive a 429 response. Ensure any `AuthorizationExpire` timestamp is at least 1 hour (3600 seconds) or longer to make sure uploads are completed before authorization expires. # Bunny Stream Source: https://bunny.net/docs/stream/index Video streaming platform with global delivery, adaptive streaming, and robust security. bunny.net Stream Bunny Stream is a feature-rich video streaming platform that allows content creators, website owners, and businesses to easily upload, manage, and stream video content to audiences worldwide. Built on top of bunny.net's robust content delivery network (CDN), Bunny Stream ensures that your video content is delivered efficiently and smoothly, regardless of the viewers' location. ## Key features Bunny Stream is designed to offer a seamless video streaming experience. Here are some of its key features: * **Global delivery**: Utilizes bunny.net's extensive CDN to stream videos with low latency and high transfer speeds globally. * **Adaptive streaming**: Automatically adjusts video quality based on the viewer's internet speed, ensuring uninterrupted playback. * **Video management**: Offers a user-friendly dashboard for uploading, organizing, and managing video content. * **Encoding and transcoding**: Automatically converts videos into multiple resolutions and formats to ensure compatibility across devices and browsers. * **Customizable video player**: Provides a customizable video player that can be tailored to match your branding and website design. * **Security**: Features security measures such as token authentication and domain restrictions to protect your video content from unauthorized access. * **DRM**: Implements DRM solutions to further secure your video content against piracy and unauthorized distribution, ensuring that your content is viewed only by authorized users in authorized regions. For more information, see our [MediaCageDRM overview](/docs/stream/drm). * **Statistics**: Track engagement and analyze viewer behavior with detailed analytics and reporting tools. # MediaCage Basic DRM Source: https://bunny.net/docs/stream/mediacage-basic MediaCage DRM Basic is a user-friendly feature that empowers every user to safeguard their video content effortlessly. It is designed with simplicity in mind, allowing users to enable DRM protection with just a few clicks through the dashboard. This feature ensures that your videos are protected from unauthorized access, providing you with the peace of mind that your valuable content remains secure. Follow the steps below to enable MediaCage Basic DRM on a Bunny Stream library and protect your videos from casual downloads. ## What you'll need Before you dive in, make sure you have the following prerequisites in place: * A [bunny.net](https://bunny.net/) account ( [Log in](https://dash.bunny.net/auth/login?pk_buttonlocation=menu) or sign up for a [free trial](https://dash.bunny.net/auth/register)). ## Enabling MediaCage Basic DRM Follow the steps below to easily enable the MediaCage Basic DRM feature and ensure seamless protection for your videos: 1. Login to [bunny.net dashboard](https://dash.bunny.net/auth/login?pk_buttonlocation=menu). 2. Click on **Stream** and select desired video library. 3. Go to **Security tab**. 4. In the **Security tab**, you will find the DRM toggle switch. Simply toggle it on to enable DRM protection for the selected video library. After MediaCage Basic DRM is enabled on video library, the media files are instantly protected by employing the dynamic encryption when accessing the media files. ## Limitations MediaCage DRM is designed to provide a robust security framework for your digital media content. However, to maintain the highest level of protection, there are certain functional limitations that you need to be aware of: ### Embed view only MediaCage strictly enforces content playback through its proprietary Embed View. This means that any attempts to access or play videos directly from the file storage or through third-party video players are not supported and will be automatically disabled. This restriction ensures that all content remains within the secure environment of MediaCage, preventing unauthorized access and distribution. ### No MP4 fallback and Early-Play support MediaCage does not support MP4 fallback and Early-Play features when activated on a video library. These features are often susceptible to security vulnerabilities, including unauthorized downloads and premature content access. By disabling these options, MediaCage enhances the security of the media content, ensuring that all playback adheres strictly to the established DRM protocols and cannot be circumvented. These limitations are put in place to ensure that MediaCage can offer the most secure and controlled environment for hosting and sharing digital media content. # Understanding MediaCage basic DRM Source: https://bunny.net/docs/stream/mediacage-basic-guide Learn how MediaCage basic DRM protects your video content from unauthorized downloads. MediaCage is the free, built-in DRM layer for Bunny Stream. This guide explains how it works, what it does and doesn't protect against, and when you should consider stepping up to MediaCage Enterprise instead. ## What is MediaCage? MediaCage is a basic free DRM system designed to prevent most attempts at downloading your video content. MediaCage is tightly integrated into Bunny CDN to dynamically encrypt the video content. It was designed to operate as a device-agnostic system and does not require any special hardware or software support on the client. ## Clear-Key Encryption Unlike enterprise-style DRM systems, MediaCage uses clear-key based keys to encrypt the video files. While the system was designed to make it extremely difficult to download and decrypt the video content, the keys are transferred to the client in a clear format, making them potentially vulnerable to a sophisticated attack. ## Dynamic Key Generation To prevent unauthorized access, MediaCage uses dynamic key generation. This means each video session generates a unique set of keys that can only be used once with that specific session. This prevents users from sharing or attempting to decrypt the video streams. ### Security Considerations While MediaCage was designed to prevent a vast majority of download attempts and successfully tested with well-known download plugins it is not a replacement for an enterprise-grade DRM system such as PlayReady or Widevine. If you host premium, industry or pay-per view video applications or platforms please get in touch with our support team to discuss enterprise DRM. ## Limitations MediaCage is a powerful system. However, to provide the best security possible, it comes with a couple of feature limitations listed below. ## Embed View Only To offer a secure environment, MediaCage requires playback to be made through our Embed View only. Trying to access the videos directly or within a third-party video player will be disabled. ## No MP4 Fallback & Early-Play Support Due to lack of security and data protection, MP4 fallback and Early-Play are disabled if MediaCage is active on the video library. # MediaCage Enterprise DRM Source: https://bunny.net/docs/stream/mediacage-enterprise MediaCage Enterprise DRM provides advanced digital content protection for enterprise-level requirements. It integrates cutting-edge technologies such as FairPlay and Widevine to deliver an elevated level of security and enhanced encryption features. Please note that MediaCage Enterprise DRM is a premium feature and requires coordination with our sales team for activation. **Multi-key DRM support** Multi-key DRM licensing is implemented on a per-playback-device basis, allowing multiple licenses to be issued for a single piece of video content. For example, a single playback device may receive multiple separate licenses. One for the video stream, 2 for the accompanying translated stereo audio tracks and these licenses may expire and issue new licenses due to the total video duration; resulting in 6 DRM licenses being issued and billed for one video playback. ## What you'll need Before you dive in, make sure you have the following prerequisites in place: * A [bunny.net](https://bunny.net/) account ( [Log in](https://dash.bunny.net/auth/login?pk_buttonlocation=menu) or sign up for a [free trial](https://dash.bunny.net/auth/register)). * **Apple FairPlay deployment package** - To use FairPlay protection, you'll need specific details obtained from your Apple Developer account. These details include: * FPS Certificate file (.der or .cer) * Private key file (.pem) * Private key password string * Application secret key (ASK) string ### FairPlay streaming details Explore our [specialized guide](/docs/stream/fairplay-deployment) for step-by-step instructions on retrieving FairPlay Streaming deployment package. ## Enabling the MediaCage Enterprise DRM To gain access to **MediaCage Enterprise DRM** and safeguard your digital content, follow the step-by-step procedure outlined below: 1. Contact Support Team. Reach out to our support team to express your interest in enabling MediaCage Enterprise DRM for your account. 2. Provide the Support Team with the **FPS Certificate file** (.der or .cer), **Private key file** (.pem), **Private key password string**, and **Application Secret Key** (ASK) string obtained from your Apple Developer account. 3. The support team will review the provided details and initiate the activation process for the DRM Enterprise feature on your account. Once activated, you will receive confirmation from the sales team, and the feature will be added to your video library. # MetaTags Source: https://bunny.net/docs/stream/metatags How to add metaTags to your video You can add metaTag's to your video via the 'Update video' API endpoint. * metaTags can be used as metadata for your video * metaTags are publicly exposed as 'meta' tags if you embed the Bunny player via iframe. See the [Stream API Reference](/docs/api-reference/stream) for full details on updating videos. *** [Webhooks](/docs/stream/webhooks) # Mobile SDK Source: https://bunny.net/docs/stream/mobile-sdk Stream Mobile SDK Libraries The following SDK libraries allow for quick integration of Bunny Stream player on both iOS and Android mobile platforms. ## Key Features * Complete API Integration: Full support for Bunny REST Stream API * Efficient Video Upload: TUS protocol implementation for reliable, resumable uploads * Advanced Video Player: Custom-built player with full Bunny CDN integration * Camera Upload Support: Built-in capabilities for recording and uploading videos directly from device camera * Type-Safe API: Fully typed Swift API for compile-time safety * Background Processing: Support for background uploads and downloads * Comprehensive Error Handling: Detailed error information and recovery options ## What is Bunny Stream Mobile SDK iOS? Bunny Stream is a comprehensive Swift Package Manager (SPM) package designed to seamlessly integrate Bunny's powerful video streaming capabilities into your iOS applications. The package provides a robust set of tools for video management, playback, uploading, and camera-based video uploads, all through an intuitive **Swift API**. ## What is Bunny Stream Mobile SDK Android? Bunny Stream is an Android library designed to seamlessly integrate Bunny's powerful video streaming capabilities into your Android applications. The package provides a robust set of tools for video management, playback, uploading, and camera-based video uploads, all through an intuitive **Kotlin API**. # Token authentication with Mobile SDKs Source: https://bunny.net/docs/stream/mobile-sdk-token-authentication When using the Stream Mobile SDKs, **two authentication layers exist**: * **Embed View Token Authentication**: controls access to the video * **CDN Token Authentication**: protects delivery from Bunny CDN and is handled automatically by the SDK This document focuses on **Embed View Token Authentication**, which must be handled by a **customer-managed backend or Edge Script** containing custom business logic that decides whether a certain client is allowed to play back a video by returning an embedded view token. ## Embed View Token Authentication ### Purpose * Authorizes a viewer to play a specific video * Enforced at the **Stream API level** * Required for private or restricted videos ### Mobile SDK Responsibility * **The customer backend generates the token** * The mobile app requests the token and passes it to the SDK player element * The SDK uses the token for playback; **CDN token signing happens automatically** ## Supported Authentication Methods | Method | Mobile SDK | | ------------------------------- | ------------------------------ | | Embed View Token Authentication | Supported via customer backend | | CDN Token Authentication | Automatic | | Client-side token signing | Not supported | ## Backend Requirements The backend (or Edge Script) must: * Securely store the **Video Library API Key** as it serves as a secret that must not be stored in the mobile app * Authenticate the mobile app user with custom business logic * Generate embed view token by following the [token authentication signing procedure](/docs/stream/token-authentication#signing-procedure) (token security key is your Video Library API Key) * Return a `token` and `expires` values in response: ```json theme={null} { "token": "SIGNED_EMBED_VIEW_TOKEN", "expires": 1710000000 } ``` Tokens should be **short-lived** (1–5 minutes recommended), unless you have a specific use-case for which longer expiration could be used. ### Edge Script Example Below is an example Edge Script that generates embed view tokens. Store `VIDEO_LIBRARY_API_KEY` as an **Edge Script Secret**. ```typescript theme={null} BunnySDK.net.http.serve(async (request: Request): Promise => { const url = new URL(request.url); const apiKey = BunnySDK.env.VIDEO_LIBRARY_API_KEY; const videoId = url.searchParams.get("videoId"); const expires = Math.floor(Date.now() / 1000) + 300; // 5 minutes or adjust if needed if (!videoId) { return new Response(JSON.stringify({ error: "Missing videoId" }), { status: 400, headers: { "Content-Type": "application/json" }, }); } /* * ============================================================ * Custom authentication / authorization logic * ------------------------------------------------------------ * Perform your business checks here: * - Validate user identity (e.g. by using JWT, API key, or some other auth headers) * - Verify entitlement to this video * - Apply subscription / access rules * * Only generate a token if access is allowed. * ============================================================ */ // Example: // if (!isUserAuthorized(request, videoId)) { // return new Response( // JSON.stringify({ error: "Unauthorized" }), // { status: 403, headers: { "Content-Type": "application/json" } } // ); // } const token = generateEmbedViewToken(apiKey, videoId, expires); return new Response(JSON.stringify({ token, expires }), { headers: { "Content-Type": "application/json" }, }); }); /** * Embed View Token generation (per Bunny Stream docs): * * Token data sequence: * Video Library API Key + videoId + expires * * Steps: * 1. Concatenate the values in the order above (no separators) * 2. Generate HMAC-SHA256 using the Video Library API Key * 3. Base64 encode: : */ function generateEmbedViewToken( apiKey: string, videoId: string, expires: number, ): string { // @ts-ignore - crypto is available in the Edge runtime const crypto = require("crypto"); const data = apiKey + videoId + expires; const signature = crypto .createHmac("sha256", apiKey) .update(data) .digest("hex"); return Buffer.from(`${signature}:${expires}`).toString("base64"); } ``` ## Android SDK Usage The `PlayVideo` call supports token parameters: ```kotlin theme={null} PlayVideo( ... videoId = "abc123", token = token, expires = expires ... ) ``` **Flow:** 1. Request embed token from backend 2. Receive `{ token, expires }` 3. Pass values to `PlayVideo` ## iOS SDK Usage The `BunnyStreamPlayer` initializer supports token parameters: ```swift theme={null} BunnyStreamPlayer( ... videoId: "abc123", token: token, expires: expires ... ) ``` **Flow:** 1. Request embed token from backend 2. Receive `{ token, expires }` 3. Initialize player with token data ## Important Notes * The **Video Library API Key** must **never** be included in mobile apps * Embed View Token Authentication is **required** if you don't want to publicly expose your videos * CDN Token Authentication is applied to CDN URLs automatically if turned on in the video library # MP4 Fallback Source: https://bunny.net/docs/stream/mp4-downloads The bunny.net Stream platform offers MP4 URL access to some resolutions of video enabled, which may be useful in an environment where HLS playback or using our player is not possible (e.g. for legacy devices). MP4 Fallback exposes a direct MP4 URL for each video alongside the default HLS stream — useful for legacy devices, environments without a player, or downloadable content. The sections below cover how to enable it, how to construct the URLs, and the security considerations to be aware of. ## Prerequisites * You must have a [Video Library created](/docs/stream/quickstart#creating-a-video-library). * You need to have MP4 Fallback enabled under the encoding tab of the created video library Enable MP4 Fallback * **Only videos uploaded after enabling MP4 Fallback will generate the MP4 file. Please keep in mind that the fallbacks go up to a maximum of 1080P in quality if the original permits.** * In addition to the above, we do not upscale videos. A 1080P fallback for example cannot be generated if the maximum quality of the original video is 480P, but in that case then play\_480p.mp4 would work. ## How to retrieve the MP4 URL? Navigate to your video in the Stream dashboard. MP4 URLs will appear on your video page - click to expand. Expand MP4 URLs You can now copy each MP4 resolution URL link or download the MP4 locally. MP4 URLs Assuming that the above eligibility criteria have been satisfied, you can generate an MP4 URL employing the subsequent structure: ```bash theme={null} https://pull_zone_url.b-cdn.net/video_id/play_{ResolutionHeight}p.mp4 ``` While producing the above mentioned URL, it is critical to bear in mind that the fallback selections are only up to 1080p. It may be easier to copy/paste the HLS URL and then change the file on the end to the MP4 URL. Thus, any resolution that is higher than 1080p, such as 2160p, will trigger a 404 error because it is not present. If you only have one resolution enabled (for example, 2160p only (and nothing else) then we will generate an MP4 URL for that resolution). The final link should appear as follows: ```bash theme={null} https://pull_zone_url.b-cdn.net/video_id/play_1080p.mp4 ``` ## Troubleshooting Review your security settings, as they are likely the cause: * Token authentication is enabled, but you are not producing a token or creating it incorrectly. * Allowed domains are specified, restricting video playback to only authorized domains. * Direct URL file access is denied. If you type the URL into your browser's address bar, you'll get a 403 error since you're attempting to access it directly. This is common on Apple devices, which do not send the correct referrer headers. This is most likely due to one of two things: * An incorrect URL construction. * The fallback resolution you want to use does not exist. # Multi-audio Source: https://bunny.net/docs/stream/multi-audio Multi-audio track support allows you to make your videos accessible to a broader audience by incorporating additional audio tracks into your video player. This new capability enables you to include multi-language or descriptive audio, commentary tracks, and more, further customizing the viewer experience and reaching more people. ## Why Multiple Audio Track Support Multiple audio track support opens up a myriad of possibilities for video content creators. By adding different audio tracks, you can: * Reach a Global Audience by including multiple language tracks to cater to a diverse audience. * Enhance Accessibility by providing descriptive audio for visually impaired viewers. * Add Versatilityby including commentary tracks or alternate audio for special editions or director’s cuts. ## How it works ### Separate audio streams When encoding your video, our system allows the audio stream to be encoded as a separate stream of chunks. This approach is highly recommended as it ensures the audio is in a separate stream and is loaded independently. This separation not only improves loading times but also ensures a seamless and flexible viewer experience. ### Respecting original files If your original file already contains multiple audio streams, our system will respect this and display the available audio tracks in the player. This means viewers can easily switch between different audio tracks, such as different languages or dubbing options, enhancing the flexibility and accessibility of your content. ### Automatic HLS output format The entire process, from encoding to playback, is streamlined and efficient. All audio tracks and their associated descriptions are automatically transferred to the HLS (HTTP Live Streaming) output format, which is the format we support. This ensures a smooth and consistent viewer experience across all devices and platforms. ## Enabling multi-audio support You can enable multiple audio track support on encoding tab of library settings by toggling **Enable Multi Audio Track Support**: ## Need help or encountered issues? If you encounter any difficulties or have questions while following this guide, our support team is here to assist you. Please don't hesitate to contact us via the [support request form](https://bunny.net/contact/) for prompt assistance. Our dedicated support team is ready to help you resolve any issues you might face during the deployment process, provide additional guidance, or answer your questions. # Playback control API Source: https://bunny.net/docs/stream/playback-api The Bunny Stream Player offers a rich set of events and methods through its embeddable iframe player, enabling external programmatic interaction with playback sessions. To ensure standardization and ease of integration, the embeddable iframe player employs the player.js JavaScript library. For more information, see detailed documentation available at [player.js website](https://github.com/embedly/player.js#playerjs). Player.js library is hosted on Bunny.net CDN so you can easily integrate it in your website by including the following script block in your HTML: ```bash theme={null} ``` ## Quickstart After loading the library and embedding the Player iframe, you can create a player object and interact with it using various events and methods. The following example illustrates the basic usage: ```js theme={null} const player = new playerjs.Player("iframe"); player.on("ready", () => { player.on("play", () => { console.log("play"); }); player.getDuration((duration) => console.log(duration)); if (player.supports("method", "mute")) { player.mute(); } player.play(); }); ``` ## Methods The Bunny Stream Player provides the following methods for controlling playback: `play`: Play the media. Example: ```js theme={null} player.play(); ``` `pause`: Pause the media. Example: ```js theme={null} player.pause(); ``` `getPaused`: boolean - Determine if the media is paused. Example: ```js theme={null} player.getPaused((value) => console.log("paused", value)); ``` `mute`: Mute the media. Example: ```js theme={null} player.mute(); ``` `unmute`: Unmute the media. Example: ```js theme={null} player.unmute(); ``` `getMuted`: boolean - Determine if the media is muted. Example: ```js theme={null} player.getMuted((value) => console.log("muted", value)); ``` `setVolume`: Set the volume. Value needs to be between 0-100. Example: ```js theme={null} player.setVolume(50); ``` `getVolume`: number - Get the volume. Value will be between 0-100. Example: ```js theme={null} player.getVolume((value) => console.log("getVolume", value)); ``` `getDuration`: number - Get the duration of the media is seconds. Example: ```js theme={null} player.getDuration((value) => console.log("getDuration", value)); ``` `setCurrentTime`: number - Perform a seek to a particular time in seconds. Example: ```js theme={null} player.setCurrentTime(50); ``` `getCurrentTime`: number - Get the current time in seconds of the video. Example: ```js theme={null} player.getCurrentTime((value) => console.log("getCurrentTime", value)); ``` `off`: Remove an event listener. If the listener is specified it should remove only that listener, otherwise remove all listeners. Example: ```js theme={null} player.off("play"); player.off("play", playCallback); ``` `on`: Add an event listener. Example: ```js theme={null} player.on("play", () => console.log("play")); ``` `supports`: \['method', 'event'], methodOrEventName - Determines if the player supports a given event or method. Example: ```js theme={null} player.supports("method", "getDuration"); player.supports("event", "ended"); ``` ## Events The Bunny Stream Player provides a set of events that can be used to enhance and customize your media playback experience. These events can be attached using the `on`method in the player.js script. ### Event list The Bunny Stream Player emits the following player.js events that can be attached using the `on`method: `ready`- Fired when the media is ready to receive commands. As outlined in the PlayerJs Spec, there may be inconsistencies if multiple players on the page have the same source `src`. To address this, append a UUID or a timestamp to the iframe's `src`to ensure all players on the page have a unique source. fired when the media is ready to receive commands. This is fired regardless of listening to the event. Note: As outlined in the PlayerJs Spec, you may run into inconsistencies if you have multiple players on the page with the same `src`. To get around this, simply append a UUID or a timestamp to the iframe's src to guarantee that all players on the page have a unique `src`. `progress`- Fires when the media is loading additional media for playback. Example: ```js theme={null} { "percent": 0.8 } ``` `timeupdate` - Fires during playback. Example: ```js theme={null} data: { seconds: 10, duration: 40 } ``` `play`- Fires when the video starts to play. `pause`- Fires when the video is paused. `ended`- Fires when the video is finished. `seeked` - Fires when the video has been seeked by the user. `error` - Fires when an error occurs. # Player Source: https://bunny.net/docs/stream/player Welcome to our Bunny Stream video player with modern interface and improved performance. The new Bunny Stream video player is ready to power your video experiences! It's enabled by default for all new video libraries. ## Quickstart **How to enable the video player** for existing video libraries from your Bunny Stream dashboard: 1. Log into your **Bunny.net** account 2. Navigate to **Stream** from the side bar 3. Select the **Video Library** you would like to utilize the new player on 4. Click on **Player Settings** 5. Toggle off **'Enable legacy player'** Enable legacy player toggle in PlayerSettings Enable legacy player toggle in PlayerSettings 6. Your video library **Videos and embed codes** will now reflect the new video player 7. **Update your embed URLs to use the new player endpoint:**
player.mediadelivery.net/embed/ 8. **Enjoy!** ## API You can also activate the new player over API on your video library using the following **cURL commands** via the [Update Video Library](/docs/api-reference/core/stream-video-library/update-video-library) endpoint. ```bash theme={null} curl https://api.bunny.net/videolibrary/ \ -X POST \ -H 'authorization: ' \ -H 'content-type: application/json' \ -d '{"PlayerVersion": }' ``` | Value | Description | | ----- | --------------------------------------- | | `2` | Enable the new video player | | `1` | Disable and revert to the legacy player | **Note:** Updating the video library via the dashboard or API does not automatically update your existing embed URLs. To use the new player endpoint, update your embed URLs to: player.mediadelivery.net/embed/ ## About **Why we built the new video player** The player uses a modern and flexible technology that helps us deliver a better video experience for you. Here’s what that means: **A custom, modern Interface** The player offers a clean, intuitive interface that works seamlessly across desktops, tablets, and mobile devices. It’s fully customizable, allowing us to deliver a user experience that matches the quality of our content **Fast and responsive performance** The player is optimized for performance. Video controls feel smooth and responsive, and playback starts quickly without delays or glitches. **A consistent experience across browsers** No matter what browser you use; whether it’s Chrome, Safari, Firefox, or Edge you’ll get a consistent and reliable experience. The interface and functionality remain the same everywhere **Designed for accessibility** The player is built to be inclusive. It supports screen readers, keyboard navigation, and other accessibility features so that everyone can enjoy our content with ease ## Notes * The new video player uses a new player URL for both Embed URL iframe and Direct Play URLs [https://player.mediadelivery.net](https://player.mediadelivery.net)  * Your pre-existing player Embed URL iframe and Direct Play URLs will still work [https://iframe.mediadelivery.net](https://iframe.mediadelivery.net) * You can still swap your video library back to the previous player by enabling the legacy player within player settings If you use [Custom Head HTML within the legacy player - check out our guide for migrating.](/docs/stream/custom-head-html-migration-guide) # Overview Source: https://bunny.net/docs/stream/player-settings This area focuses on tailoring the Bunny Player's look and functionality to match your branding or specific preferences. **Key Features:** * **Visual and Functional Customization** - Options include adjustments to the player's appearance, such as colors and fonts, and functionality, like autoplay settings and looping. * **Playback Speed** - Set custom playback speed options within your video player, giving you flexibility for custom default playback speeds and additional options. * **Brand Integration** - You can add your logo and incorporate branding elements directly into the player, promoting brand consistency and recognition. * **Watchtime Heatmap** - Enable the watchtime heatmap within your player so viewers can view the most popular parts of your video from the transport bar. * **Resumable Player** - Enable your video player to remember and resume playback where the video was last left when viewers leave and return to your video experience. * **Compact Controls** - Maximize screen real estate with a streamlined video player view, ideal for mobile and embedded players. * **Chromecast** - Cast videos to Chromecast-enabled devices, with a custom cast experience that syncs player colors and captions styling, supports audio track selection and playback speed control, and works with eDRM-protected content. * **AirPlay Support** - Stream videos to Apple TV via AirPlay using the Bunny Player. # Bitmovin player integration Source: https://bunny.net/docs/stream/players/bitmovin This documentation expands on our to guide you through the integration of with Bunny Stream’s video streams and MediaCage Enterprise DRM solution into your web application. If your use case doesn’t require DRM, you can follow the same steps provided below, omit the DRM configuration part, and follow our [video storage structure guide](/docs/stream/storage-structure), which describes how to access your Bunny Stream videos programmatically. ## What you’ll need Before you dive in, make sure you have the following prerequisites in place: * A [bunny.net](https://bunny.net/) account ([Log in](https://dash.bunny.net/auth/login?pk_buttonlocation=menu) or sign up for a [free trial](https://dash.bunny.net/auth/register)) . * Ensure that the Media Cage DRM Enterprise feature is enabled in your account. If you need guidance on how to enable this feature, please refer to our [Media Cage DRM Enterprise quickstart guide](/docs/stream/quickstart-mediacage-enterprise). * A [Bitmovin](https://bitmovin.com/) account. ## Setting up Bitmovin player Bitmovin offers an HTML web player that can be embedded into your site using JavaScript. You can read official [Bitmovin documentation](https://developer.bitmovin.com/playback/docs/getting-started-web) on how to get started with the player or copy the code sample below. You are required to use your own license key for the player to ensure domain whitelisting for production environments. The demo code provided below can be run in localhost without issues.The JavaScript changes below are required to ensure compatibility with our license endpoints. ### HTML configuration Incorporate the following HTML code into your web page: ```html theme={null} Bitmovin Player Demo
``` ### JavaScript configuration Replace `[ENTER_YOUR_BITMOVIN_KEY]` with your actual Bitmovin license key. The JavaScript configuration below includes necessary modifications to ensure compatibility with the license endpoints, as well as DRM configurations for both Widevine and FairPlay: ```javascript theme={null} var conf = { key: "[ENTER_YOUR_BITMOVIN_KEY]", // change key here logs: { level: "debug", }, network: { preprocessHttpResponse: function (type, response) { switch (type) { case "manifest/hls/variant": response.body = response.body.replace( /.*KEYFORMAT="com.apple.streamingkeydelivery"\n/, "", ); break; default: break; } return Promise.resolve(response); }, }, }; var source = { title: "New Dashboard", hls: "{{playlistUrl}}", drm: { widevine: { LA_URL: "https://video.bunnycdn.com/WidevineLicense/{{videoLibraryId}}/{{videoId}}", maxCertificateRequestRetries: 0, maxLicenseRequestRetries: 0, }, fairplay: { LA_URL: "https://video.bunnycdn.com/FairPlay/{{videoLibraryId}}/license/?videoId={{videoId}}", certificateURL: "https://video.bunnycdn.com/FairPlay/{{videoLibraryId}}/certificate", maxCertificateRequestRetries: 0, maxLicenseRequestRetries: 0, useUint16InitData: true, prepareContentId: (url) => { return url.substring(url.indexOf("skd://")); }, }, }, }; const player = new bitmovin.player.Player( document.getElementById("player"), conf, ); player.load(source); ``` Do not set `licenseResponseType: "json"` or override `prepareMessage`/`prepareLicense` to wrap the SPC/CKC in JSON. That format belongs to the deprecated `/FairPlayLicense/{library_id}/{video_id}` endpoint. The current `/FairPlay/{library_id}/license` and `/FairPlay/{library_id}/certificate` endpoints expect and return raw binary data, which is Bitmovin's default `licenseResponseType`/message handling — no overrides needed. ## Finalizing integration After embedding the HTML and JavaScript code into your web application, the Bitmovin Player should load and be capable of playing DRM-protected content from Bunny Stream. Test the setup in various environments to ensure compatibility and performance across different devices and browsers. # Fairplay HTML5 player integration Source: https://bunny.net/docs/stream/players/fairplay This documentation will guide you through the integration of the FairPlay DRM solution into your web application, ensuring secure and compliant playback of DRM-encrypted content. This guide walks through configuring an HTML5 video player with Bunny Stream's FairPlay license server so encrypted videos can be played back securely on Safari and Apple devices. ## What you'll need Before you dive in, make sure you have the following prerequisites in place: * A [bunny.net](https://bunny.net/) account ( [Log in](https://dash.bunny.net/auth/login?pk_buttonlocation=menu) or sign up for a [free trial](https://dash.bunny.net/auth/register)). * Ensure that the Media Cage DRM Enterprise feature is enabled in your account. If you need guidance on how to enable this feature, please refer to our [Media Cage DRM Enterprise quickstart guide](/docs/stream/quickstart-mediacage-enterprise). ## Playing DRM content Once video content is DRM encrypted, every play session needs a valid play license. Play license can be obtained from a License Server URL. > Note: Certificate and license server apply the same security mechanisms as player embed view. If referrer protection and/or token authentication is enabled in library settings, valid referrer header and/or token query parameters should also be provided to Fairplay DRM endpoints. ### Certificate URL To use Fairplay, player needs to obtain the Fairplay certificate first. It is accessible with the URL of the following pattern: ``` https://video.bunnycdn.com/FairPlay/{library_id}/certificate ``` ### License server URL Fairplay key server is accessible with the URL of the following pattern: ``` https://video.bunnycdn.com/FairPlay/{library_id}/license/?videoId=${video_id} ``` * `library_id`: A unique identifier for the library or content collection. * `video_id`: The specific identifier for the FairPlay encrypted video. ## Fairplay integration with Safari Safari browsers on macOS and iOS inherently support the playback of HLS streams encrypted with Fairplay. To facilitate Fairplay integration on Safari, use the provided sample code: ````bash bash // MODIFY: You need to replace {library_id} and {video_id} with theme={null} actual values window.fp_certificate_url = "https://video.bunnycdn.com/FairPlay/{library_id}/certificate"; window.fp_license_server_url = "https://video.bunnycdn.com/FairPlay/ {library_id}/license/?videoId=${video_id}"; // MODIFY: You need to replace{" "} {pull_zone_url} and {video_id} with actual URL window.playback_url = "https:// {pull_zone_url}.b-cdn.net/{video_id}/playlist.m3u8"; ``` Ensure to replace `library_id`, `pull_zone_url`, and `video_id` with valid values corresponding to the specific video player. The next step is to create a function to download and store certificates, which will be used later: ```bash bash async function loadFpCertificate() { try { let response = await fetch(window.fp_certificate_url); window.fp_certificate = await response.arrayBuffer(); } catch(e) { window.console.error(`Could not load certificate at ${window.fp_certificate_url}`); } } ```` Write a function to call the certificate-loading function and initiate video playback: ```bash bash theme={null} async function startVideo() { await loadFpCertificate(); // get html5 video element let video = document.querySelector('video'); // hook up the encrypted event video.addEventListener('encrypted', onFpEncrypted); // set the source property of the player. video.src = window.playback_url; } ``` Define the `onFpEncrypted` function to handle the 'encrypted' event. It fetches the license, passes it to secure session, and enables Safari to decode the encrypted content: ```bash bash theme={null} async function onFpEncrypted(event) { try { let initDataType = event.initDataType; if (initDataType !== 'skd') { window.console.error(`Received unexpected initialization data type "${initDataType}"`); return; } let video = event.target; if (!video.mediaKeys) { let access = await navigator.requestMediaKeySystemAccess("com.apple.fps", [{ initDataTypes: [initDataType], videoCapabilities: [{ contentType: 'application/vnd.apple.mpegurl', robustness: '' }], distinctiveIdentifier: 'not-allowed', persistentState: 'not-allowed', sessionTypes: ['temporary'], }]); let keys = await access.createMediaKeys(); // The FairPlay certificate we fetched earlier is used here. await keys.setServerCertificate(window.fp_certificate); await video.setMediaKeys(keys); } let initData = event.initData; let session = video.mediaKeys.createSession(); session.generateRequest(initDataType, initData); let message = await new Promise(resolve => { session.addEventListener('message', resolve, { once: true }); }); // license_server_url we set earlier is used here. let response = await getResponse(message, window.fp_license_server_url); await session.update(response); return session; } catch(e) { window.console.error(`Could not start encrypted playback due to exception "${e}"`) } } ``` Implement the `getResponse` function to send the SPC message to the FairPlay server and obtain the CKC for the encrypted session. The license endpoint expects the raw SPC bytes as the request body (no JSON wrapper, no `Content-Type` override) and returns the raw CKC bytes directly: ```bash bash theme={null} async function getResponse(event, license_server_url) { let licenseResponse = await fetch(license_server_url, { method: 'POST', body: event.message, }); return await licenseResponse.arrayBuffer(); } ``` Do not set a `Content-Type: application/json` header or wrap the SPC in a `{"spc": "..."}` JSON payload — that format belongs to the deprecated `/FairPlayLicense/{library_id}/{video_id}` endpoint and will cause the current `/FairPlay/{library_id}/license` endpoint to fail with an internal server error, since it expects and returns raw binary data. Place the following script block in the 'head' section or in a separate JavaScript file. This code ensures that video playback starts when the page is loaded. ```bash bash theme={null} ``` Place the HTML5 video element on desired place in the html document. ```bash bash theme={null} ``` ## Authentication The license service endpoint employs referrer protection and embed token authentication to secure requests, similar to our player Embed View. That means if token authentication is enabled it should also be present in requests going to DRM license endpoint ( the same is for referrer protection and token authentication). These security rules ensure that only requests from authorized sources with valid tokens are processed if configured. For the authentication to work, it has to be enabled in dashboard: For more detailed guidance on these security mechanisms, please visit our [Embed View Token Authentication page](/docs/stream/token-authentication). # Introduction Source: https://bunny.net/docs/stream/players/index MediaCage Enterprise DRM offers out-of-the-box integration with our own Bunny Stream player, making the implementation process simple and efficient. Additionally, for users with custom players, we offer the flexibility to integrate MediaCage Enterprise DRM with your own solution using provided code samples. ## Automatic integration Our default integration with Bunny Stream player guarantees immediate compatibility. As soon as MediaCage Enterprise DRM is activated, the player automatically applies the protection based on the platform used, either Fairplay and Widevine. ## Integration with custom players MediaCage Enterprise DRM offers integration with custom build video players. Please see our guides for more information: * **[FairPlay integration guide](/docs/stream/players/fairplay)**- Explore seamless integration with Apple's FairPlay DRM platform using our detailed guide. Follow step-by-step instructions and best practices to ensure secure and reliable content protection for iOS and macOS devices. * **[Widevine integration guide](/docs/stream/players/widevine)**- Discover the integration possibilities with Google's Widevine DRM platform. Our guide provides detailed instructions and insights into integrating DRM Enterprise with Widevine, ensuring compatibility across a wide range of devices and platforms. * **[Bitmovin integration guide](/docs/stream/players/bitmovin)**- Learn how to integrate Bitmovin's advanced video player technology with our platform. This guide offers comprehensive steps and tips for leveraging Bitmovin's features to enhance your streaming experience, ensuring optimal performance and user engagement. * [**Shaka Player integration guide**](/docs/stream/players/shaka)-Learn how to seamlessly integrate Shaka Player with bunny.net for optimized video streaming in this comprehensive guide. This guide covers basic setup instructions, from HTML and JavaScript configurations to handling DRM with Widevine and Fairplay. * [Bitmovin player integration](/docs/stream/players/bitmovin) * [Widevine HTML5 integration](/docs/stream/players/widevine) * [Fairplay HTML5 player integration](/docs/stream/players/fairplay) * [Shaka Player integration](/docs/stream/players/shaka) * [Table of Contents](#) * * [Automatic integration](#automatic-integration) * [Integration with custom players](#integration-with-custom-players) # Shaka Player integration Source: https://bunny.net/docs/stream/players/shaka This guide will help you integrate Shaka Player with bunny.net to stream video content. Shaka Player is a free, open-source media player that plays adaptive media formats such as DASH and HLS. bunny.net is a content delivery network (CDN) service that provides fast and secure video delivery. This guide provides a basic setup for integrating Shaka Player with bunny.net. For advanced configurations and customizations, refer to the [Shaka Player documentation](https://shaka-player-demo.appspot.com/docs/api/tutorial-drm-config.html). ## What you'll need Before you dive in, make sure you have the following prerequisites in place: * A [bunny.net](https://bunny.net/) account ( [Log in](https://dash.bunny.net/auth/login?pk_buttonlocation=menu) or sign up for a [free trial](https://dash.bunny.net/auth/register)). ## HTML setup First, create an HTML file and add a `