The Problem: Every Page View Hit Our API
We had a JavaScript widget that customers embedded on their websites. The embed code was simple—a single script tag that loaded our functionality. But under the hood, every page view on every customer site triggered a request to our Laravel API running on Kubernetes.
Each request meant:
- Full Laravel framework bootstrap
- License validation queries against the database
- Dynamic JavaScript generation based on customer configuration
- A 2.7MB JavaScript file sent over the wire
At ~3,000 requests per second to our API, 99.9% of our Kubernetes cluster traffic was just serving this embedded JavaScript. The infrastructure was wildly over-provisioned for what was essentially a static file serving problem.
The Hidden Cost Multiplier
It got worse. Each JavaScript load also triggered two additional requests to fetch supplementary JSON configuration files from S3 via CloudFront CDN. So those ~25-30 million monthly API requests translated to 80-120 million CDN requests for the JSON files.
| Metric | Before | After |
|---|---|---|
| API Requests/sec | ~3,000 | <30 |
| CDN Requests/month | 80-120M | 220M+ (with caching) |
| JS Bundle Size | 2.7MB | <600KB |
| CDN Costs/month | ~$4,000 | <$100 |
| JS Load Time | ~1.2 seconds | 20-50ms (cached) |
| K8s Traffic Reduction | — | 99% |
The Architecture Before
The original flow looked like this:
Customer Website → API (Laravel/K8s)
↓
License Validation (MySQL)
↓
Generate JS dynamically
↓
Return 2.7MB JavaScript
↓
Browser loads JS
↓
2x JSON requests → CloudFront → S3 Every single page view repeated this entire process. With millions of daily visitors across all customer sites, the costs were staggering.
The Solution: Pre-Build to S3 + Smart CDN
The insight was simple: the JavaScript was only "dynamic" based on the license configuration. But license configurations don't change on every request—they change maybe once a month. We were doing dynamic generation for what was effectively static content.
Step 1: Pre-Build Per-License JavaScript Files
Instead of generating JavaScript on every request, we pre-built customer-specific bundles and pushed them to S3:
// Build pipeline pseudocode
foreach ($licenses as $license) {
$config = $license->getConfiguration();
$bundle = $this->bundler->build($config);
Storage::disk('s3')->put(
"embeds/{$license->key}/widget.js",
$bundle,
['CacheControl' => 'max-age=31536000'] // 1 year
);
} When a license configuration changed, we simply rebuilt and replaced that specific file.
Step 2: CDN Cost Analysis
With files now on S3, we needed a CDN. But not all CDNs are created equal, especially at high volume.
AWS CloudFront was our initial choice since we were already on AWS. The problem? Dual bandwidth charges. You pay for data transfer out of S3 to CloudFront, then again for data transfer from CloudFront to users. At 80M+ requests per month, this added up fast.
After evaluating alternatives, we switched to Bunny.net:
- Flat-rate pricing per GB instead of per-request fees
- No origin transfer fees when pulling from S3
- Global edge network with PoPs in 100+ locations
- Perma-Cache feature for truly static assets
Step 3: Aggressive Browser Caching
The final piece was browser-level caching. Since our JavaScript bundles were now versioned and immutable, we could set aggressive cache headers:
Cache-Control: public, max-age=604800 One week was the sweet spot—long enough to dramatically reduce repeat requests, short enough that users would see updates within a reasonable timeframe without intervention.
CDN Cache Purging for Instant Updates
Since the CDN respected our browser cache headers, we needed a way to force updates when customers changed their widget configuration or when licenses expired. We integrated cache purging into our build pipeline:
// Triggered when license config changes
public function rebuildAndPurge(License $license): void
{
// Rebuild the bundle with new config
$bundle = $this->bundler->build($license->getConfiguration());
Storage::disk('s3')->put(
"embeds/{$license->key}/widget.js",
$bundle
);
// Purge CDN cache immediately
$this->cdn->purge("embeds/{$license->key}/*");
} This meant when a customer updated options on their dashboard or when a license expired, end-users would receive the updates on their next page load rather than waiting for the cache to naturally expire.
The impact was immediate. Our 80M monthly CDN requests dropped to approximately 30M—the rest were served directly from browser cache. As traffic grew, the caching efficiency meant we could handle 220M+ monthly requests with the same infrastructure. Without browser caching, we estimated this would have been 600-800M requests hitting our CDN.
Step 4: JavaScript Bundle Optimization
While rearchitecting the delivery, we also tackled the bundle size. 2.7MB for a widget was excessive. Through aggressive optimization:
- Tree shaking: Removed unused code paths
- Code splitting: Lazy-loaded features only when needed
- Dependency audit: Replaced heavy libraries with lighter alternatives
- Minification + Brotli compression: Maximum compression for wire transfer
The result: 2.7MB → <600KB, a 78% reduction in bundle size.
Handling License Expiration
One challenge with pre-built files: what happens when a license expires? We implemented a cleanup system:
// License expiration handler
public function handleExpiration(License $license): void
{
// Delete the pre-built bundle
Storage::disk('s3')->delete("embeds/{$license->key}/widget.js");
// Purge CDN cache for this path
$this->cdn->purge("embeds/{$license->key}/*");
// Replace with "expired" placeholder if needed
Storage::disk('s3')->put(
"embeds/{$license->key}/widget.js",
$this->getExpiredPlaceholder()
);
} The New Architecture
Customer Website → CDN (Bunny.net)
↓
Edge cache hit? → Return cached JS (99% of requests)
↓
Cache miss → S3 origin fetch
↓
Return pre-built JS (<600KB)
↓
Browser caches for 1 year
License Update → Rebuild bundle → Push to S3 → Purge CDN Cost Breakdown
The numbers tell the story:
| Component | Before | After |
|---|---|---|
| Kubernetes (API servers) | Heavily utilized | 99% reduction |
| CloudFront CDN | ~$4,000/mo | Eliminated |
| Bunny.net CDN | — | <$100/mo |
| S3 Storage | Minimal | ~$5/mo |
| Total Monthly Savings | — | ~$3,900+ |
Key Takeaways
This project reinforced several important lessons:
- Question "dynamic" requirements: Just because content can be dynamic doesn't mean it should be. If data changes rarely, pre-build it.
- CDN pricing varies wildly: At scale, the difference between CDN providers can be thousands of dollars monthly. Evaluate based on your actual usage patterns.
- Browser cache is free: Aggressive caching with proper cache-busting is the cheapest CDN there is.
- Bundle size matters: A 78% reduction in JS size means 78% less bandwidth, 78% faster loads, and happier users.
- Measure before optimizing: We only discovered 99.9% of our K8s traffic was embeddable JS by actually measuring traffic patterns.
The best request is the one you never have to serve. The second best is one you serve from cache.
The Performance Win: 1.2 Seconds to 20ms
Beyond cost savings, the user experience improvement was dramatic. The original architecture required:
- PHP bootstrap (~200ms)
- Laravel framework initialization (~150ms)
- Database queries for license validation (~100ms)
- Dynamic JavaScript generation (~50ms)
- Transfer of 2.7MB unoptimized payload (~700ms on average connections)
Total: ~1.2 seconds before the widget even started executing.
After optimization:
- First load (CDN cache miss): 200-300ms — S3 origin fetch + CDN caches it
- Subsequent loads (CDN cache hit): 20-50ms — served from edge
- Repeat visitor (browser cache): <5ms — no network request at all
That's a 96% reduction in load time for first-time visitors and effectively instant loading for returning visitors. For a widget that loads on every page view, this compounds into a significantly better user experience across millions of page loads.
Results at Scale
After implementing these changes, the system scaled effortlessly. What started as 80M monthly requests (with ~$4k/month in CDN costs) grew to 220M+ monthly requests—and costs stayed under $100/month. Without the browser caching layer, that 220M would have been an estimated 600-800M requests hitting our infrastructure.
More importantly, the Kubernetes cluster that was previously dedicated to serving JavaScript could be rightsized for actual application logic. The 99% reduction in traffic meant we could reduce nodes, save on compute costs, and improve reliability for the endpoints that actually needed dynamic processing.