Scaling
Scale your Go Bananas! deployment for growth.
Cloudflare's Auto-Scaling
Go Bananas! runs on Cloudflare's edge network, which provides automatic scaling:

Request routing across 300+ edge locations worldwide
What Scales Automatically
| Component | Scaling |
|---|---|
| Workers | Automatic per request |
| Durable Objects | Automatic with request routing |
| R2 Storage | Unlimited |
| KV | Global replication |
What Needs Planning
| Component | Limitation | Mitigation |
|---|---|---|
| D1 Database | 5GB per database | Partition data, archive old records |
| KV Operations | 100K writes/day (free) | Upgrade plan |
| Worker CPU | 50ms/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:
-- 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:
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:
{
"d1_databases": [
{
"binding": "DB_SHARD_1",
"database_name": "go-bananas-shard-1"
},
{
"binding": "DB_SHARD_2",
"database_name": "go-bananas-shard-2"
}
]
}Routing logic:
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:
// 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:
{
"r2_buckets": [
{
"binding": "R2_IMAGES",
"bucket_name": "go-bananas-images",
"jurisdiction": "eu" // or "fedramp"
}
]
}Caching Strategies
KV Caching
Increase cache TTL for stable data:
// 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:
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:
- Create custom domain for R2 bucket
- Enable caching in dashboard
- Set cache rules for images
Load Testing
Test Script
// 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:
k6 run -e API_KEY=sk_test_xxx scripts/load-test.tsBenchmark Results
Expected performance:
| Metric | Target | Notes |
|---|---|---|
| p50 latency | < 3s | Image generation |
| p99 latency | < 10s | Including Gemini API |
| Error rate | < 0.1% | Excluding rate limits |
| Throughput | 100+ req/s | Per edge location |
Multi-Tenant Isolation
Resource Limits
Enforce per-tenant limits:
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:
// 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:
// 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:
- Database backups: Daily exports to R2
- Cross-region R2: Enable if available
- Configuration backup: Store wrangler.jsonc in git
- Secret rotation plan: Document recovery procedures
Monitoring at Scale
Key Metrics
// Track in usage_logs
{
operation: 'generate_image',
duration_ms: endTime - startTime,
size_bytes: imageBuffer.byteLength,
api_calls_made: 1,
tenant_id: tenantId
}Aggregation Queries
-- 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
| Usage | Workers | D1 | R2 | Total |
|---|---|---|---|---|
| 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.