← Back to Blog

Reducing Cloud Costs by 93%: A Cloudflare KV Optimization Story

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:

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:

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?

  1. IndexerPage component - An admin dashboard that polled every 30 seconds
  2. 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:

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:

// 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:

After removing IndexerPage:

After badge optimization:

Math verification:

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:

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:

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:

  1. Cached metadata approach: Have the cron job write a single summary key (snapshot_metadata) with timestamps. Frontend fetches this via get() (Read operation, 10M free tier).
  1. Event-driven updates: Use Cloudflare Durable Objects or WebSockets to push snapshot updates to connected clients.
  1. 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

---

EF-Map is an open-source interactive map for EVE Frontier. Check out the source code on GitHub or join our community on Discord.

cloudflareoptimizationserverlesscost reductionkv storage