---
title: "Stop Bloating Your Database: The S3 Pointer Pattern"
description: "Why 10MB JSON blobs kill your buffer pool, and how to keep MySQL lean with S3 + presigned URLs."
pubDate: "2023-05-22"
lang: "en"
slug: "database-bloat-s3-pointer"
url: "https://hoangyell.com/database-bloat-s3-pointer/"
author: "HoangYell"
categories: ["System Design"]
tags: ["mysql", "s3"]
heroImage: "https://raw.githubusercontent.com/HoangGeek/store/main/webp/photo-1732173201326-bd094e09b0e9.webp"
---

**The day I almost bricked production.**

It started with a feature request that sounded innocent enough:

> *"We need to show the raw transaction logs in the admin panel. Just dump the JSON into the DB so we can list them."*

I was young. I was naive. I thought, *"Sure, MySQL has a JSON column type. What could go wrong?"*

The logs were **10MB**.  
The table grew.  
And then, one random Tuesday, the database choked.

CPU spiked to 99%. Queries that usually took 10ms were taking 10 seconds. My phone started vibrating with PagerDuty alerts, and I was sweating through my shirt.

Here’s what I learned the hard way using massive JSON blobs in a relational database is a surefire way to kill your performance.

***

## The Trap: "It's Just Data, Put It In The DB"

It feels logical. You have a User. Users have Logs. Put the Logs in the database.

But when those logs are massive blobs (10MB+), you aren’t just "storing data." You are fundamentally breaking how a database works.

### Why Your DB Hates You Now

Databases like MySQL live and die by their **Buffer Pool** (RAM).
You want your RAM filled with useful stuff:
- Index pages (so lookups are instant).
- Hot rows (data people actually access).

When you stuff a 10MB blob into a row, that row becomes a **Buffer Pool Bully**.

It walks into the club, kicks out all the lean, efficient index pages, and sits there taking up space. Even if you just want to read *one* field, the DB often has to drag that entire fat payload from disk into RAM.

Your cache hit ratio tanks. Your disk IOPS scream. And your users leave.

I built a simulation so you can see exactly what happens inside your RAM:

<style>
  :root \{
    --sa-bg: #0f172a;
    --sa-border: #1e293b;
    --sa-primary: #3b82f6;
    --sa-danger: #ef4444;
    --sa-text: #94a3b8;
    --sa-text-light: #e2e8f0;
    --sa-glow-blue: 0 0 10px rgba(59, 130, 246, 0.5);
    --sa-glow-red: 0 0 20px rgba(239, 68, 68, 0.6);
  \}

  /* Scoped container */
  .sa-sim-container \{
    background: var(--sa-bg);
    border: 1px solid var(--sa-border);
    border-radius: 16px;
    padding: 24px;
    margin: 2rem 0;
    font-family: 'Inter', system-ui, -apple-system, sans-serif;
    color: var(--sa-text);
    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
    position: relative;
    overflow: hidden;
  \}

  .sa-sim-label \{
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: #64748b;
    margin-bottom: 16px;
    font-weight: 600;
  \}

  /* --- RAM SIM --- */
  .sa-ram-stage \{
    position: relative;
    width: 100%;
    height: 240px;
    background: #1e293b;
    border-radius: 12px;
    border: 1px solid #334155;
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
  \}

  .sa-page \{
    position: absolute;
    width: 18px;
    height: 18px;
    background: rgba(59, 130, 246, 0.2);
    border: 1px solid rgba(59, 130, 246, 0.4);
    border-radius: 4px;
    transition: all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
    box-shadow: 0 0 5px rgba(0,0,0,0.2);
  \}
  .sa-page.active \{
    background: rgba(59, 130, 246, 0.8);
    box-shadow: var(--sa-glow-blue);
    border-color: #60a5fa;
  \}

  .sa-blob-monster \{
    position: absolute;
    width: 120px;
    height: 120px;
    background: radial-gradient(circle at 30% 30%, #f87171, #991b1b);
    border-radius: 50%;
    transform: scale(0);
    opacity: 0;
    transition: transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.3s ease;
    box-shadow: var(--sa-glow-red), inset 0 0 20px rgba(0,0,0,0.5);
    z-index: 10;
    display: flex;
    align-items: center;
    justify-content: center;
    color: white;
    font-weight: 800;
    font-size: 0.9rem;
    text-align: center;
    line-height: 1.2;
    border: 2px solid #fecaca;
  \}

  .sa-controls \{
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 16px;
  \}

  .sa-btn \{
    appearance: none;
    background: var(--sa-primary);
    color: white;
    border: none;
    padding: 10px 20px;
    border-radius: 8px;
    font-weight: 600;
    font-size: 0.9rem;
    cursor: pointer;
    transition: all 0.2s;
    box-shadow: 0 4px 6px rgba(59, 130, 246, 0.3);
  \}
  .sa-btn:hover \{
    background: #2563eb;
    transform: translateY(-1px);
    box-shadow: 0 6px 12px rgba(59, 130, 246, 0.4);
  \}
  .sa-btn:active \{
    transform: translateY(0);
  \}

  .sa-legend \{
    display: flex;
    gap: 16px;
    font-size: 0.85rem;
  \}
  .sa-legend-item \{
    display: flex;
    align-items: center;
    gap: 8px;
  \}
  .sa-dot \{
    width: 8px;
    height: 8px;
    border-radius: 50%;
  \}
</style>

<div class="sa-sim-container">
  <div class="sa-sim-label">Simulation: Buffer Pool Eviction</div>
  <div class="sa-ram-stage" id="sa-ram-stage">
    <div class="sa-blob-monster" id="sa-monster">10MB<br />BLOB</div>
  </div>
  <div class="sa-controls">
    <div class="sa-legend">
      <div class="sa-legend-item">
        <div class="sa-dot" style="background: #3b82f6; box-shadow: 0 0 8px #3b82f6;"></div> Index Page
      </div>
      <div class="sa-legend-item">
        <div class="sa-dot" style="background: #ef4444; box-shadow: 0 0 8px #ef4444;"></div> The Bully
      </div>
    </div>
    <button class="sa-btn" id="sa-btn-bully">Inject Blob</button>
  </div>
</div>

<script>
  (function() \{
    const stage = document.getElementById('sa-ram-stage');
    const monster = document.getElementById('sa-monster');
    const btn = document.getElementById('sa-btn-bully');
    
    // Grid config
    const COLS = 12; // increased density
    const ROWS = 6;
    const TOTAL = COLS * ROWS;
    const PAGES = [];
    
    // Init Grid
    function initGrid() \{
      // Clear existing pages (but keep monster)
      const existing = stage.querySelectorAll('.sa-page');
      existing.forEach(e => e.remove());
      PAGES.length = 0;

      const stageRect = stage.getBoundingClientRect();
      const cellW = stageRect.width / COLS;
      const cellH = stageRect.height / ROWS;

      for (let r = 0; r \< ROWS; r++) \{
        for (let c = 0; c \< COLS; c++) \{
          const page = document.createElement('div');
          page.className = 'sa-page active';
          
          // Store original non-jittered position
          const baseX = c * cellW + (cellW - 18)/2;
          const baseY = r * cellH + (cellH - 18)/2;
          
          page.style.left = baseX + 'px';
          page.style.top = baseY + 'px';
          
          // Metadata for physics
          page.dataset.bx = baseX;
          page.dataset.by = baseY;
          
          stage.appendChild(page);
          PAGES.push(page);
        \}
      \}
    \}

    // Run Simulation
    function runSim() \{
      monster.style.opacity = '1';
      monster.style.transform = 'scale(1)';
      btn.disabled = true;
      btn.innerText = 'Eviction in progress...';

      // Physics Loop: pure JS calculation for "Scatter"
      // We push pages away from center
      const cx = stage.offsetWidth / 2;
      const cy = stage.offsetHeight / 2;
      const radius = 80; // Monster radius + buffer

      PAGES.forEach(page => \{
        const bx = parseFloat(page.dataset.bx);
        const by = parseFloat(page.dataset.by);
        
        // Vector from center to page
        let dx = bx + 9 - cx; // +9 is half width
        let dy = by + 9 - cy;
        const dist = Math.sqrt(dx*dx + dy*dy);
        
        if (dist \< radius) \{
          // Push it OUT
          const force = (radius - dist) / radius; // 1.0 at center, 0.0 at edge
          const angle = Math.atan2(dy, dx);
          const pushDist = 100 * force;
          
          const tx = Math.cos(angle) * pushDist;
          const ty = Math.sin(angle) * pushDist;
          
          // Apply transform
          page.style.transform = `translate(${tx}px, ${ty}px) scale(0.5)`;
          page.style.opacity = '0.3'; // Dim it
          page.classList.remove('active');
        \}
      \});

      // Reset sequence
      setTimeout(() => \{
        monster.style.opacity = '0';
        monster.style.transform = 'scale(0)';
        
        PAGES.forEach(page => \{
          page.style.transform = 'translate(0,0) scale(1)';
          page.style.opacity = '1';
          setTimeout(() => page.classList.add('active'), 300);
        \});
        
        btn.disabled = false;
        btn.innerText = 'Inject Blob';
      \}, 3000);
    \}

    // Responsive Handlers
    window.addEventListener('resize', initGrid);
    
    // Boot
    // Delay init slightly to ensure layout is ready
    setTimeout(initGrid, 100);
    
    btn.addEventListener('click', runSim);
  \})();
</script>

See what happened? That red bully just nuked your performance cache.

***

## The "Solutions" That Don't Work

I tried these. Do not try these.

### 1. "Let's Use Athena / Data Lake"
**The logic:** "S3 is cheap, let's dump it there and query it!"
**The reality:** Athena is designed for *analytics*, not for your user clicking "View Details". It takes seconds. Users don't wait seconds. They rage-click.

### 2. "The Hybrid Approach"
**The logic:** "Store small JSON in DB, move big ones to S3!"
**The reality:** Now you have two code paths. Two sources of truth. And an onboarding doc that takes 30 minutes to explain "where the data is."
Don't be cute. Be boring.

***

## The Smart Fix: The S3 Pointer Pattern

Here is the "Secret Sauce" (it's actually really standard, but feels like magic).

**The DB stores:** A pointer (`s3_key`).  
**The S3 bucket stores:** The fat blob.

That's it. Your database row is now tiny. It fits in the index. It stays in RAM.

### The Flow

```text
1. User asks Backend for data.
2. Backend checks DB: "Oh, file is at logs/123/cat.json"
3. Backend tells S3: "Give me a temporary URL for this file."
4. Backend gives URL to User.
5. User downloads from AWS directly.
```

Your backend server? Chill.
Your database? Chill.
Amazon's massive servers? They carry the heavy load.

<div class="sa-sim-container" id="sa-flow-container">
  <div class="sa-sim-label">Simulation: Pointer vs Payload Path</div>
  
  <div class="sa-flow-stage" id="sa-flow-stage">
    
    <svg class="sa-connections" id="sa-connections">
      <defs>
        <marker id="arrow-blue" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto" markerUnits="strokeWidth">
          <path d="M0,0 L0,6 L6,3 z" fill="#3b82f6" />
        </marker>
        <marker id="arrow-orange" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto" markerUnits="strokeWidth">
          <path d="M0,0 L0,6 L6,3 z" fill="#f97316" />
        </marker>
        
        <filter id="glow-blue">
          <feGaussianBlur stdDeviation="2.5" result="coloredBlur"/>
          <feMerge>
            <feMergeNode in="coloredBlur"/>
            <feMergeNode in="SourceGraphic"/>
          </feMerge>
        </filter>
        <filter id="glow-orange">
          <feGaussianBlur stdDeviation="3.5" result="coloredBlur"/>
          <feMerge>
            <feMergeNode in="coloredBlur"/>
            <feMergeNode in="SourceGraphic"/>
          </feMerge>
        </filter>
      </defs>
      
      <path id="path-db-client" d="" stroke="#334155" stroke-width="2" stroke-dasharray="6,4" fill="none" />
      <path id="path-s3-client" d="" stroke="#334155" stroke-width="2" stroke-dasharray="6,4" fill="none" />
      
      <circle id="particle-key" r="5" fill="#3b82f6" filter="url(#glow-blue)" opacity="0">
        <animateMotion dur="0.8s" fill="freeze" calcMode="spline" keySplines="0.42 0 0.58 1" keyTimes="0;1" begin="indefinite" />
      </circle>
      <circle id="particle-blob" r="12" fill="#f97316" filter="url(#glow-orange)" opacity="0">
        <animateMotion dur="1s" fill="freeze" calcMode="spline" keySplines="0.42 0 0.58 1" keyTimes="0;1" begin="indefinite" />
      </circle>
    </svg>
    
    <div class="sa-nodes-grid">
      <div class="sa-node-card sa-node-db" id="node-db">
        <div class="sa-icon">🗄️</div>
        <div class="sa-node-title">MySQL</div>
        <div class="sa-node-sub">Metadata Only</div>
      </div>
      <div class="sa-node-card sa-node-client" id="node-client">
        <div class="sa-icon">💻</div>
        <div class="sa-node-title">Client</div>
        <div class="sa-node-sub">Browser/App</div>
      </div>
      <div class="sa-node-card sa-node-s3" id="node-s3">
        <div class="sa-icon">☁️</div>
        <div class="sa-node-title">Amazon S3</div>
        <div class="sa-node-sub">Heavy Storage</div>
      </div>
    </div>
  </div>

  <div class="sa-controls">
    <div class="sa-flow-status" id="sa-flow-status">Ready to request...</div>
    <button class="sa-btn" id="sa-btn-flow">Visualize Request</button>
  </div>
</div>

<style>
  .sa-flow-stage \{
    position: relative;
    padding: 20px 0;
    min-height: 200px;
  \}
  
  .sa-connections \{
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: 1;
    pointer-events: none;
    overflow: visible;
  \}
  
  .sa-nodes-grid \{
    display: flex;
    justify-content: space-between;
    align-items: center;
    position: relative;
    z-index: 2;
  \}
  
  /* Mobile Stack */
  @media (max-width: 640px) \{
    .sa-nodes-grid \{
      flex-direction: column;
      gap: 50px;
    \}
  \}
  
  .sa-node-card \{
    background: #1e293b;
    border: 1px solid #334155;
    border-radius: 12px;
    padding: 16px;
    width: 110px;
    text-align: center;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    box-shadow: 0 4px 6px rgba(0,0,0,0.2);
    transition: transform 0.3s ease;
  \}
  
  .sa-node-db \{ border-top: 3px solid #3b82f6; \}
  .sa-node-client \{ border-top: 3px solid #10b981; \}
  .sa-node-s3 \{ border-top: 3px solid #f97316; \}
  
  .sa-icon \{ font-size: 1.8rem; \}
  .sa-node-title \{ font-weight: 700; color: #e2e8f0; font-size: 0.9rem; \}
  .sa-node-sub \{ font-size: 0.7rem; color: #94a3b8; \}
  
  .sa-flow-status \{
    font-size: 0.9rem;
    color: #94a3b8;
    font-family: monospace;
  \}
</style>

<script>
  (function() \{
    const container = document.getElementById('sa-flow-container');
    const svg = document.getElementById('sa-connections');
    const db = document.getElementById('node-db');
    const client = document.getElementById('node-client');
    const s3 = document.getElementById('node-s3');
    const btn = document.getElementById('sa-btn-flow');
    const status = document.getElementById('sa-flow-status');
    const pathDbClient = document.getElementById('path-db-client');
    const pathS3Client = document.getElementById('path-s3-client');
    
    // Particles
    const pKey = document.getElementById('particle-key');
    const pBlob = document.getElementById('particle-blob');
    
    // State to block clicks
    let isAnimating = false;

    // --- Geometry Logic ---
    function getCenter(el) \{
      const rect = el.getBoundingClientRect();
      const contRect = container.getBoundingClientRect(); // relative to container
      return \{
        x: rect.left + rect.width/2 - contRect.left,
        y: rect.top + rect.height/2 - contRect.top
      \};
    \}

    function createCurvedPath(p1, p2, bendY = 0) \{
      // Create a quadratic bezier curve
      const mx = (p1.x + p2.x) / 2;
      const my = (p1.y + p2.y) / 2 + bendY;
      return `M ${p1.x},${p1.y} Q ${mx},${my} ${p2.x},${p2.y}`;
    \}

    function updatePaths() \{
      const cDb = getCenter(db);
      const cClient = getCenter(client);
      const cS3 = getCenter(s3);
      
      // Calculate bend based on layout (vertical vs horizontal)
      const isMobile = window.innerWidth \< 640;
      const bend = isMobile ? 0 : -30; // slight arc on desktop

      // Path 1: DB -> Client
      pathDbClient.setAttribute('d', createCurvedPath(cDb, cClient, isMobile ? 0 : -20));
      
      // Path 2: S3 -> Client
      // Note: Animation direction is S3->Client, but for SVG path we draw "from->to"
      // to reuse the same logic
      pathS3Client.setAttribute('d', createCurvedPath(cS3, cClient, isMobile ? 0 : 20));
    \}

    // --- Animation Logic ---
    function runFlow() \{
      if (isAnimating) return;
      isAnimating = true;
      btn.disabled = true;
      
      // Reset particles
      pKey.setAttribute('opacity', '0');
      pBlob.setAttribute('opacity', '0');

      // 1. Fetch Key (DB -> Client)
      status.innerText = "1. Fetching pointer (fast)...";
      status.style.color = "#3b82f6";
      
      // Attach animation element dynamically to trigger it
      const animKey = pKey.querySelector('animateMotion');
      animKey.setAttribute('path', pathDbClient.getAttribute('d'));
      
      pKey.setAttribute('opacity', '1');
      animKey.beginElement();
      
      // 2. Wait for key to arrive
      setTimeout(() => \{
        pKey.setAttribute('opacity', '0'); // vanish on arrival
        
        // 3. Fetch Blob (S3 -> Client)
        status.innerText = "2. Streaming blob (direct)...";
        status.style.color = "#f97316";
        
        const animBlob = pBlob.querySelector('animateMotion');
        animBlob.setAttribute('path', pathS3Client.getAttribute('d'));
        
        pBlob.setAttribute('opacity', '1');
        animBlob.beginElement();
        
        // 4. Finish
        setTimeout(() => \{
           pBlob.setAttribute('opacity', '0');
           status.innerText = "Done. DB untouched.";
           status.style.color = "#10b981";
           isAnimating = false;
           btn.disabled = false;
           btn.innerText = "Replay";
        \}, 1200); // 1s + buffer
        
      \}, 800);
    \}
    
    // Observers
    window.addEventListener('resize', () => \{
      requestAnimationFrame(updatePaths);
    \});
    
    // Init
    setTimeout(updatePaths, 100); // Layout settle
    btn.addEventListener('click', runFlow);
  \})();
</script>

***

## "Wait, isn't direct S3 access dangerous?"

I can hear you screaming: *“You want me to open my S3 bucket to the internet? Are you crazy?!”*

Calm down. I’m not saying "Public Read".

I’m saying **Presigned URLs**.

These are magic tokens your backend generates. They look like this: `https://s3.aws.../cat.json?signature=123xyz&expires=300`.
- They are valid for **5 minutes** (or whatever you set).
- They only unlock **one specific file**.
- They are cryptographically signed by your backend.

### Why you shouldn't "proxy" the file
You might think: *"I'll just download the file to my backend and pass it to the user. That feels safer."*

This is the **"Proxy Trap"**.

| Strategy | Backend Impact | Result |
| :--- | :--- | :--- |
| **Direct S3** | **Zero.** Backend signs a tiny string. CPU sleeps. | 🚀 Fast. Scalable. |
| **Proxy** | **Massive.** Backend holds 10MB in RAM for every request. | 🐌 Slow. OOM errors. |

Unless you are watermarking a PDF on the fly or working around a strict corporate firewall, **don't be a proxy**. Let S3 do its job.

***

## "Show me the Code"

Okay, here is how stupidly simple this is.

### 1. The Schema
Your DB row loses weight immediately.

```sql
CREATE TABLE logs (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  s3_key VARCHAR(255) NOT NULL, -- The only thing that matters
  details JSON, -- Store SMALL metadata here if you need to query it
  created_at TIMESTAMP,
  PRIMARY KEY (id)
);
```

### 2. The Read Path (Python/Boto3)

```python
def get_log_download_url(log_id, user):
    # 1. Check if user owns this log (CRITICAL STEP)
    log = db.fetch_log(log_id)
    if log.owner_id != user.id:
        raise Forbidden("Nice try, hacker.")

    # 2. Ask AWS for a temporary pass
    url = s3.generate_presigned_url(
        'get_object',
        Params={'Bucket': 'my-bucket', 'Key': log.s3_key},
        ExpiresIn=300 # 5 minutes
    )
    
    # 3. Give it to the frontend
    return {"url": url}
```

***

## Check the Receipt (Cost)

If you are still unconvinced, let’s talk money.

Storing 1TB of junk in RDS (SSD storage) costs about **$115/month**.
Storing 1TB of junk in S3 Standard IA costs about **$12.50/month**.

You are paying **10x** more to make your database slower. Make it make sense.

<link href="https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet" />

<div class="receipt-demo-container">
  <div class="receipt-wrapper" id="receipt-widget">
    <div class="receipt-paper" id="receipt-paper">
      <div class="receipt-header">
        <div class="brand">AWS COST ESTIMATOR</div>
        <div class="meta">
          <span>STORE: US-EAST-1</span>
          <span id="receipt-date"></span>
        </div>
        <div class="divider">================================</div>
      </div>
      <div class="receipt-body">
        <div class="line-item">
          <div class="item-desc">
            <span class="qty">1.0 TB</span>
            <span class="name">RDS Storage (SSD gp3)</span>
            <div class="specs">Block Storage, Multi-AZ capable</div>
          </div>
          <div class="item-price" id="price-rds">$115.00</div>
        </div>
        <div class="line-item highlight-item">
          <div class="item-desc">
            <span class="qty">1.0 TB</span>
            <span class="name">S3 Standard IA</span>
            <div class="specs">Object Storage, Infrequent Access</div>
          </div>
          <div class="item-price" id="price-s3">$12.50</div>
        </div>
        <div class="divider">--------------------------------</div>
        <div class="summary-section">
          <div class="summary-row">
            <span>SUBTOTAL (RDS)</span>
            <span id="sub-rds">$115.00</span>
          </div>
          <div class="summary-row">
            <span>SUBTOTAL (S3)</span>
            <span id="sub-s3">$12.50</span>
          </div>
          <div class="divider-double">================================</div>
          <div class="total-row">
            <span>OVERSPEND</span>
            <span class="total-amount" id="overspend-amount">$102.50</span>
          </div>
          <div class="savings-badge">YOU PAY <span id="multiplier">9.2x</span> MORE</div>
        </div>
      </div>
      <div class="receipt-footer">
        <p>THANK YOU FOR BURNING MONEY</p>
        <div class="barcode">||| || ||| | |||| ||| ||</div>
      </div>
    </div>
    <div class="receipt-controls">
      <div class="receipt-status" id="receipt-flow-status">Viewing Monthly Cost</div>
      <button class="receipt-btn" id="receipt-btn-flow">Annualize Receipt</button>
    </div>
  </div>
</div>

<style>
  /* Proper Scoping */
  .receipt-demo-container \{
    --bg-color: #f3f4f6;
    --paper-color: #fff;
    --ink-color: #2d2d2d;
    --accent-red: #ef4444;
    --accent-green: #22c55e;
    --font-mono: 'Space Mono', monospace;
    
    background-color: var(--bg-color);
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 3rem 1rem;
    margin: 2rem 0;
    border-radius: 12px;
    font-family: var(--font-mono);
  \}

  .receipt-wrapper \{
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 24px;
    width: 100%;
    max-width: 380px;
    perspective: 1000px; /* For 3D printing effect */
  \}

  /* Receipt Visuals */
  .receipt-paper \{
    background: var(--paper-color);
    /* Noisy paper texture */
    background-image: 
      linear-gradient(rgba(255,255,255,0.9), rgba(255,255,255,0.9)),
      url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='0.1'/%3E%3C/svg%3E");
    padding: 24px;
    width: 100%;
    box-shadow: 
      0 1px 2px rgba(0,0,0,0.1), /* Close shadow */
      0 10px 20px -5px rgba(0,0,0,0.15); /* Lifted shadow */
    position: relative;
    border-top: 5px solid var(--ink-color);
    color: var(--ink-color);
    transform-origin: top center;
    transition: transform 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  \}
  
  /* Printing Animation */
  .receipt-printing \{
    animation: print-receipt 0.8s ease-out;
  \}
  
  @keyframes print-receipt \{
    0% \{ transform: scaleY(0.4) translateY(-100px); opacity: 0; \}
    30% \{ opacity: 1; \}
    100% \{ transform: scaleY(1) translateY(0); \}
  \}

  /* Jagged bottom edge effect */
  .receipt-paper::after \{
    content: "";
    position: absolute;
    bottom: -10px;
    left: 0;
    width: 100%;
    height: 10px;
    background: linear-gradient(45deg, transparent 33.333%, var(--paper-color) 33.333%, var(--paper-color) 66.667%, transparent 66.667%), 
                linear-gradient(-45deg, transparent 33.333%, var(--paper-color) 33.333%, var(--paper-color) 66.667%, transparent 66.667%);
    background-size: 20px 20px;
    background-position: 0 -10px;
  \}

  .receipt-header \{
    text-align: center;
    margin-bottom: 20px;
  \}

  .brand \{
    font-size: 1.25rem;
    font-weight: 700;
    margin-bottom: 8px;
    letter-spacing: -1px;
    color: #000;
  \}

  .meta \{
    font-size: 0.75rem;
    color: #666;
    display: flex;
    justify-content: space-between;
    margin-bottom: 8px;
  \}

  .divider \{
    color: #ddd;
    overflow: hidden;
    white-space: nowrap;
    margin: 12px 0;
    font-weight: 300;
  \}

  .divider-double \{
    border-top: 1px dashed #000;
    border-bottom: 1px dashed #000;
    height: 4px;
    margin: 12px 0;
    color: transparent;
  \}

  /* Line Items */
  .line-item \{
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: 16px;
  \}

  .highlight-item \{
    position: relative;
  \}
  
  .highlight-item::before \{
    content: "RECOMMENDED";
    position: absolute;
    top: -10px;
    left: 0;
    font-size: 0.6rem;
    background: var(--accent-green);
    color: white;
    padding: 2px 4px;
    border-radius: 2px;
  \}

  .item-desc \{
    display: flex;
    flex-direction: column;
  \}

  .qty \{
    font-size: 0.75rem;
    color: #666;
  \}

  .name \{
    font-weight: 700;
  \}

  .specs \{
    font-size: 0.65rem;
    color: #888;
    margin-top: 2px;
    max-width: 150px;
  \}

  .item-price \{
    font-weight: 700;
  \}

  /* Summary */
  .summary-row \{
    display: flex;
    justify-content: space-between;
    font-size: 0.85rem;
    margin-bottom: 4px;
  \}

  .total-row \{
    display: flex;
    justify-content: space-between;
    align-items: center;
    font-size: 1.1rem;
    font-weight: 700;
    margin-top: 10px;
  \}

  .total-amount \{
    color: var(--accent-red);
  \}

  .savings-badge \{
    background: var(--ink-color);
    color: white;
    text-align: center;
    padding: 8px;
    margin-top: 16px;
    font-size: 0.9rem;
    font-weight: 700;
    border-radius: 2px;
  \}

  .receipt-footer \{
    text-align: center;
    margin-top: 30px;
    font-size: 0.7rem;
    color: #888;
  \}

  .barcode \{
    font-family: 'Libre Barcode 39', cursive; /* Fallback if not loaded */
    font-size: 1.5rem;
    margin-top: 8px;
    letter-spacing: 2px;
    transform: scaleY(0.7);
  \}

  /* Controls */
  .receipt-controls \{
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
  \}

  .receipt-status \{
    font-size: 0.85rem;
    color: #666;
    font-weight: 500;
  \}

  .receipt-btn \{
    background-color: #3b82f6;
    color: white;
    border: none;
    padding: 12px 24px;
    border-radius: 8px;
    font-family: var(--font-mono);
    font-weight: 700;
    font-size: 1rem;
    cursor: pointer;
    box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.5);
    transition: all 0.2s ease;
    width: 100%;
  \}

  .receipt-btn:hover \{
    background-color: #2563eb;
    transform: translateY(-2px);
    box-shadow: 0 6px 8px -1px rgba(59, 130, 246, 0.6);
  \}
  
  .receipt-btn:active \{
    transform: translateY(0);
  \}
</style>

<script>
  (function() \{
    // Configuration
    const pricing = \{
      rds: 115.00, // $115/TB/Month
      s3: 12.50    // $12.50/TB/Month
    \};

    const els = \{
      btn: document.getElementById('receipt-btn-flow'),
      status: document.getElementById('receipt-flow-status'),
      date: document.getElementById('receipt-date'),
      priceRds: document.getElementById('price-rds'),
      priceS3: document.getElementById('price-s3'),
      subRds: document.getElementById('sub-rds'),
      subS3: document.getElementById('sub-s3'),
      overspend: document.getElementById('overspend-amount'),
      multiplier: document.getElementById('multiplier'),
      paper: document.getElementById('receipt-paper')
    \};
    
    // Safety check
    if (!els.btn) return;

    // Set Date
    const now = new Date();
    els.date.textContent = now.toLocaleDateString('en-US').toUpperCase();

    let isAnnual = false;

    function formatCurrency(amount) \{
      return '$' + amount.toLocaleString('en-US', \{ minimumFractionDigits: 2, maximumFractionDigits: 2 \});
    \}

    function animateValue(obj, start, end, duration) \{
      let startTimestamp = null;
      const step = (timestamp) => \{
        if (!startTimestamp) startTimestamp = timestamp;
        const progress = Math.min((timestamp - startTimestamp) / duration, 1);
        const val = progress * (end - start) + start;
        obj.innerHTML = formatCurrency(val);
        if (progress \< 1) \{
          window.requestAnimationFrame(step);
        \}
      \};
      window.requestAnimationFrame(step);
    \}

    els.btn.addEventListener('click', () => \{
      isAnnual = !isAnnual;
      
      const multiplier = isAnnual ? 12 : 1;
      const duration = 800; // ms

      // Trigger Print Animation
      els.paper.classList.remove('receipt-printing');
      void els.paper.offsetWidth; // Reflow
      els.paper.classList.add('receipt-printing');

      // Update Button & Status
      els.btn.textContent = isAnnual ? "View Monthly Cost" : "Annualize Receipt";
      els.status.textContent = isAnnual ? "Projection: 1 Year Cost" : "Viewing Monthly Cost";

      // Calculate new values
      const newRds = pricing.rds * multiplier;
      const newS3 = pricing.s3 * multiplier;
      const newOverspend = newRds - newS3;

      // Animate Numbers
      // Current values for smooth transition
      const currentRds = parseFloat(els.priceRds.textContent.replace(/[$,]/g, ''));
      const currentS3 = parseFloat(els.priceS3.textContent.replace(/[$,]/g, ''));
      const currentOverspend = parseFloat(els.overspend.textContent.replace(/[$,]/g, ''));

      animateValue(els.priceRds, currentRds, newRds, duration);
      animateValue(els.priceS3, currentS3, newS3, duration);
      animateValue(els.subRds, currentRds, newRds, duration);
      animateValue(els.subS3, currentS3, newS3, duration);
      animateValue(els.overspend, currentOverspend, newOverspend, duration);
    \});

  \})();
</script>

## TL;DR

1. **Don't** put 10MB blobs in MySQL. It bullies your Buffer Pool.
2. **Do** store files in S3 and keep a pointer (`s3_key`) in the DB.
3. **Do** use Presigned URLs so your backend doesn't have to carry the payload.
4. **Don't** overcomplicate it.

Simple scales. Clever fails.
