Skip to content

Scaling ​

Scale your Go Bananas! deployment for growth.

Cloudflare's Auto-Scaling ​

Go Bananas! runs on Cloudflare's edge network, which provides automatic scaling:

Global Edge Scaling

Request routing across 300+ edge locations worldwide

What Scales Automatically ​

ComponentScaling
WorkersAutomatic per request
Durable ObjectsAutomatic with request routing
R2 StorageUnlimited
KVGlobal replication

What Needs Planning ​

ComponentLimitationMitigation
D1 Database5GB per databasePartition data, archive old records
KV Operations100K writes/day (free)Upgrade plan
Worker CPU50ms/request (free)Optimize code, upgrade plan

Performance Tiers ​

Free Tier ​

Suitable for:

  • Development
  • Small teams (< 10 users)
  • Light usage (< 1000 images/month)

Limits:

  • 100K requests/day
  • 50ms CPU/request
  • 5GB D1 storage

Workers Paid ($5/month) ​

Suitable for:

  • Production deployments
  • Medium teams (10-100 users)
  • Regular usage (< 50,000 images/month)

Limits:

  • 10M requests/month included
  • 30s CPU/request
  • Higher concurrency

Enterprise ​

Contact Cloudflare for:

  • Unlimited scaling
  • SLA guarantees
  • Dedicated support

D1 Scaling Strategies ​

Data Partitioning ​

For large datasets, partition by tenant or date:

sql
-- Create monthly partitions
CREATE TABLE images_2024_01 AS
SELECT * FROM images
WHERE created_at >= '2024-01-01'
  AND created_at < '2024-02-01';

-- Create indexes on partition
CREATE INDEX idx_images_2024_01_tenant ON images_2024_01(tenant_id);

Archive Old Data ​

Move old data to R2:

typescript
async function archiveOldImages(env: Env, tenantId: string, olderThan: Date) {
  // Export old records
  const oldImages = await env.DB.prepare(`
    SELECT * FROM images
    WHERE tenant_id = ? AND created_at < ?
  `).bind(tenantId, olderThan.toISOString()).all();

  // Store in R2 as JSON
  await env.R2_ARCHIVES.put(
    `${tenantId}/images-archive-${Date.now()}.json`,
    JSON.stringify(oldImages.results)
  );

  // Delete from D1
  await env.DB.prepare(`
    DELETE FROM images
    WHERE tenant_id = ? AND created_at < ?
  `).bind(tenantId, olderThan.toISOString()).run();
}

Multiple Databases ​

For very large deployments, shard by tenant:

jsonc
{
  "d1_databases": [
    {
      "binding": "DB_SHARD_1",
      "database_name": "go-bananas-shard-1"
    },
    {
      "binding": "DB_SHARD_2",
      "database_name": "go-bananas-shard-2"
    }
  ]
}

Routing logic:

typescript
function getDbShard(tenantId: string, env: Env) {
  const shardIndex = hashString(tenantId) % 2;
  return shardIndex === 0 ? env.DB_SHARD_1 : env.DB_SHARD_2;
}

R2 Scaling ​

Cost Optimization ​

R2 pricing:

  • Storage: $0.015/GB/month
  • Class A operations (writes): $4.50/million
  • Class B operations (reads): $0.36/million
  • Egress: Free

Optimization strategies:

typescript
// 1. Compress images
const compressedBuffer = await compressImage(imageBuffer, 85); // 85% quality

// 2. Generate smaller thumbnails
const thumbnail = await resizeImage(imageBuffer, 200, 200);

// 3. Clean up unused images
await cleanupOrphanedImages(env);

Multi-Region (Enterprise) ​

For global performance:

jsonc
{
  "r2_buckets": [
    {
      "binding": "R2_IMAGES",
      "bucket_name": "go-bananas-images",
      "jurisdiction": "eu"  // or "fedramp"
    }
  ]
}

Caching Strategies ​

KV Caching ​

Increase cache TTL for stable data:

typescript
// Tenant config rarely changes
await env.TENANT_CONFIG.put(key, value, {
  expirationTtl: 3600 // 1 hour
});

// API key mappings change infrequently
await env.API_KEYS.put(key, value, {
  expirationTtl: 1800 // 30 minutes
});

Cache Headers ​

For R2 public URLs:

typescript
await env.R2_IMAGES.put(key, imageBuffer, {
  httpMetadata: {
    cacheControl: 'public, max-age=31536000', // 1 year
    contentType: 'image/png'
  }
});

CDN Configuration ​

Enable Cloudflare CDN for R2:

  1. Create custom domain for R2 bucket
  2. Enable caching in dashboard
  3. Set cache rules for images

Load Testing ​

Test Script ​

typescript
// scripts/load-test.ts
import { check } from 'k6';
import http from 'k6/http';

export const options = {
  stages: [
    { duration: '1m', target: 10 },   // Ramp up
    { duration: '3m', target: 10 },   // Sustain
    { duration: '1m', target: 0 },    // Ramp down
  ],
};

export default function () {
  const res = http.post(
    'https://gobananasai.com/api/images',
    JSON.stringify({ prompt: 'test image' }),
    {
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': __ENV.API_KEY,
      },
    }
  );

  check(res, {
    'status is 200': (r) => r.status === 200,
    'response time < 5s': (r) => r.timings.duration < 5000,
  });
}

Run test:

bash
k6 run -e API_KEY=sk_test_xxx scripts/load-test.ts

Benchmark Results ​

Expected performance:

MetricTargetNotes
p50 latency< 3sImage generation
p99 latency< 10sIncluding Gemini API
Error rate< 0.1%Excluding rate limits
Throughput100+ req/sPer edge location

Multi-Tenant Isolation ​

Resource Limits ​

Enforce per-tenant limits:

typescript
async function checkTenantLimits(tenant: Tenant, env: Env): Promise<boolean> {
  // Check rate limit
  const recentRequests = await countRecentRequests(tenant.tenantId, env);
  if (recentRequests >= tenant.rateLimitPerMinute) {
    throw new RateLimitError('Rate limit exceeded');
  }

  // Check storage quota
  const storageUsed = await calculateStorageUsed(tenant.tenantId, env);
  if (storageUsed >= tenant.monthlyQuotaMb * 1024 * 1024) {
    throw new QuotaExceededError('Storage quota exceeded');
  }

  return true;
}

Fair Scheduling ​

Prevent single tenant from monopolizing:

typescript
// Implement request queuing per tenant
const queue = new Map<string, Promise<void>>();

async function withTenantQueue<T>(
  tenantId: string,
  operation: () => Promise<T>
): Promise<T> {
  // Wait for previous request from same tenant
  const previous = queue.get(tenantId);
  if (previous) {
    await previous;
  }

  // Execute and track
  const promise = operation();
  queue.set(tenantId, promise.then(() => {}));

  return promise;
}

High Availability ​

Regional Failover ​

Cloudflare handles regional failover automatically. For data:

typescript
// Primary write to nearest region
await env.R2_IMAGES.put(key, buffer);

// Metadata stored in D1 (globally distributed)
await env.DB.prepare('INSERT INTO images...').run();

Disaster Recovery ​

For critical deployments:

  1. Database backups: Daily exports to R2
  2. Cross-region R2: Enable if available
  3. Configuration backup: Store wrangler.jsonc in git
  4. Secret rotation plan: Document recovery procedures

Monitoring at Scale ​

Key Metrics ​

typescript
// Track in usage_logs
{
  operation: 'generate_image',
  duration_ms: endTime - startTime,
  size_bytes: imageBuffer.byteLength,
  api_calls_made: 1,
  tenant_id: tenantId
}

Aggregation Queries ​

sql
-- Performance by hour
SELECT
    strftime('%Y-%m-%d %H', timestamp) as hour,
    COUNT(*) as requests,
    AVG(duration_ms) as avg_duration,
    MAX(duration_ms) as max_duration
FROM usage_logs
WHERE timestamp > datetime('now', '-24 hours')
GROUP BY hour
ORDER BY hour DESC;

-- Top tenants by request count
SELECT
    tenant_id,
    COUNT(*) as requests,
    SUM(images_generated) as images
FROM usage_logs
WHERE timestamp > datetime('now', '-1 hour')
GROUP BY tenant_id
ORDER BY requests DESC
LIMIT 10;

Cost Estimation ​

Monthly Cost Calculator ​

UsageWorkersD1R2Total
10K images$5$0$1.50~$7
50K images$5$0$7.50~$13
100K images$5$0$15~$20
500K images$10$0$75~$85

Note: Gemini API costs are separate and depend on Google's pricing.

Next Steps ​

Released under the MIT License.