Published November 3, 2025 • 8 min read
When building serverless applications, operational costs can creep up quietly. What started as "just a few cents" can balloon into significant monthly expenses as your user base grows. Here's how we optimized EF-Map's Cloudflare Workers KV usage and cut our List operations from 1.6 million to 108,000 per month—a 93% reduction.
The Problem: 1.6M List Operations Per Month
EF-Map uses Cloudflare Workers KV to store real-time snapshot data for EVE Frontier's Smart Gates system. Our cron jobs update this data every 2 minutes, and we needed a way to show users when this backend data was fresh.
Our initial implementation used a status badge that polled an endpoint every 2 minutes to check snapshot freshness. Sounds reasonable, right?
The math told a different story:
- Badge polling: 0.5 calls/minute
 - Users leaving the map open 24/7: ~75 active users
 - Monthly operations: 0.5 × 60 × 24 × 30 × 75 = 1.62 million List operations
 
Cloudflare's free tier includes 1 million List operations. We were 600,000 over the limit, costing about $0.30/month. Not devastating, but inefficient—and a signal we needed to optimize.
Understanding KV Operation Types
First, let's clarify what counts as each operation type in Cloudflare Workers KV:
- Read (
get()): Fetch a single key's value - Write (
put()): Store or update a key - List (
list()): Paginate through key names (returns up to 1,000 keys per call) - Delete (
delete()): Remove a key 
Critical insight: Our cron jobs that write snapshots every 2 minutes use put() operations (Writes), NOT List operations. List operations only occur when explicitly calling list().
The Investigation: Finding the Source
We traced the List operations to a single endpoint: /api/debug-snapshots. This endpoint was designed to check the freshness of our Smart Gate snapshots by listing all keys in the KV namespace.
// Original implementation
const list = await env.EF_SNAPSHOTS.list({ cursor });
list.keys.forEach(k => out.keys.push(k.name));
With only 6 total keys in the namespace, each call made exactly 1 List operation (no pagination needed).
Who was calling this endpoint?
- IndexerPage component - An admin dashboard that polled every 30 seconds
 - IndexerStatusBadge component - The "Gates" status pill shown on the main map, polling every 2 minutes
 
The IndexerPage was legacy code from when we had a cloud indexer. We removed it immediately.
But the badge served a valuable purpose: it gave users (and us) reassurance that the backend cron jobs were running properly. We wanted to keep it, just make it smarter.
Optimization Strategy: Match Polling to Purpose
The key insight was decoupling polling frequency from staleness threshold.
Original settings:
- Polling: Every 2 minutes
 - Green (fresh): ≤10 minutes old
 - Yellow (idle): 10-25 minutes old
 - Orange (stale): >25 minutes old
 
The problem: We were checking 5 times within the "acceptable freshness window." That's overkill.
New approach: Design the badge for outage monitoring, not real-time status.
If our cron job (which runs every 2 minutes) fails, we don't need to know within 2 minutes. We need to know if it's been broken for an hour—that's a genuine issue requiring attention.
Redesigned settings:
- Polling: Every 30 minutes (6x reduction)
 - Green (ok): ≤60 minutes old
 - Orange (stale): >60 minutes old
 - Removed "idle" state entirely (binary green/orange)
 
// Optimized implementation
useEffect(() => {
  const INTERVAL_MS = 30 * 60 * 1000; // 30 minutes
  fetchSnapshots();
  timerRef.current = setInterval(() => { fetchSnapshots(); }, INTERVAL_MS);
  return () => clearInterval(timerRef.current);
}, []);
const gatesState = (() => {
  if (!snapshots) return 'loading';
  const age = Date.now() - snapshotTimestamp;
  if (age <= 60 * 60 * 1000) return 'ok';  // ≤1 hour
  return 'stalled';  // >1 hour = genuine issue
})();
The Results
Before optimization:
- 1.62M List operations/month
 - 600K over free tier
 - Cost: ~$0.30/month
 
After removing IndexerPage:
- 648K List ops/month (60% reduction)
 - Still over free tier
 
After badge optimization:
- 108K List ops/month (93% total reduction!)
 - Well under 1M free tier ✅
 - Cost: $0/month
 
Math verification:
- 0.033 calls/min × 43,200 min/month × 75 users = 107,136 operations
 
Key Takeaways
1. Match Monitoring Frequency to Actual Requirements
We were checking backend health 5 times more often than necessary. Ask yourself: "How quickly do I actually need to detect this issue?"
For our Smart Gates cron job:
- Detection window: 30-60 minutes is fine
 - Polling interval: 30 minutes matches perfectly
 - Staleness threshold: 60 minutes catches genuine failures
 
2. Remove Dead Code Aggressively
The IndexerPage admin dashboard was accounting for a significant portion of our List operations, and we hadn't visited it in months. Legacy features hiding in production can silently drain resources.
3. Binary States > Granular States for Monitoring
Our original three-state system (green/yellow/orange) encouraged more frequent polling to catch transitions. The binary system (green/orange) is clearer: it either works or it doesn't.
4. Understand Your Platform's Operation Costs
Not all database operations cost the same. In Cloudflare Workers KV:
- Reads are cheap and plentiful (10M free/month)
 - Writes are moderate (1M free/month)
 - Lists are expensive (1M free/month)
 - Deletes are cheap (1M free/month)
 
We could have used get() operations to fetch specific keys instead of list() to enumerate them. That would have moved us from the List quota to the Read quota entirely.
Alternative Approaches
If we needed real-time monitoring without the List operations, we could have:
- Cached metadata approach: Have the cron job write a single summary key (
snapshot_metadata) with timestamps. Frontend fetches this viaget()(Read operation, 10M free tier). 
- Event-driven updates: Use Cloudflare Durable Objects or WebSockets to push snapshot updates to connected clients.
 
- Client-side caching: Only fetch snapshot metadata on page load, not continuously while the page is open.
 
For our use case, the 30-minute polling approach struck the right balance of simplicity and efficiency.
Try EF-Map's Optimized Infrastructure
The Smart Gates monitoring system now runs efficiently within Cloudflare's free tier while still providing reliable status updates. Experience the seamless routing and real-time data synchronization at ef-map.com.
Related Posts
- Database Architecture: From Blockchain Events to Queryable Intelligence - Learn how our PostgreSQL indexing pipeline complements Cloudflare KV for different data access patterns
 - Route Sharing: Building a URL Shortener for Spatial Navigation - Another Cloudflare KV optimization story focused on compression and short URL generation
 - Smart Gate Authorization: Blockchain-Powered Access Control - How we use KV snapshots to cache blockchain access control data
 
---
EF-Map is an open-source interactive map for EVE Frontier. Check out the source code on GitHub or join our community on Discord.