My Discord webhook project (the one I deployed on Fly.io last week) started getting duplicate events — GitHub retries webhooks when your endpoint is slow, and my deduplication logic was just an in-memory Python set. Restart the container, lose the set, process everything twice.
I needed a KV store with TTL. Same workload, three approaches: Upstash (serverless Redis), Redis Cloud (managed free tier), and Valkey (the open-source Redis fork) self-hosted on the same Fly.io VM.
The Workload
# Deduplication: store event ID with 24h TTL, check before processing
import time, os
def is_duplicate(event_id: str) -> bool:
key = f"gh:event:{event_id}"
if kv.exists(key):
return True
kv.setex(key, 86400, "1") # 24h TTL
return False
Traffic: ~300 webhook events/day, 99.8% reads on duplicates (GitHub retries aggressively), sub-10ms lookup target.
The Contenders
| Upstash | Redis Cloud | Valkey (self-hosted) | |
|---|---|---|---|
| Free tier | 10K commands/day | 30MB storage | Unlimited (your RAM) |
| Latency (same region) | 8-15ms | 12-25ms | 0.3ms (localhost) |
| Cold start penalty | Pay-per-request | Always-on | Always-on |
| Setup time | 2 min | 5 min | 20 min |
| Data persistence | ✅ | ✅ | You configure it |
The Results (7 Days, 2,147 Events)
| Metric | Upstash | Redis Cloud | Valkey |
|---|---|---|---|
| Daily command usage | 8,412 / 10K | N/A (storage-based) | N/A |
| Storage used | 1.2MB | 1.2MB / 30MB | 1.2MB / 256MB |
| p50 lookup latency | 11ms | 18ms | 0.4ms |
| p99 lookup latency | 47ms | 89ms | 1.2ms |
| Free tier headroom | 16% left | 96% left | Infinite |
| Monthly cost at this usage | $0 | $0 | $0 |
The Plot Twist
Upstash's 10K commands/day free tier sounds generous until you realize GitHub's retry logic burns 25 commands per event (each retry is a new command). My 300 events/day became 8,400 commands. Two busy days and I'd be rate-limited into the paid tier ($0.2 per 100K commands — still cheap, but not free).
Redis Cloud's 30MB is storage-based, not command-based. My 1.2MB of event IDs fits forever. But 18ms p50 latency from Fly.io (Singapore) to Redis Cloud (AWS us-east-1) was the slowest option.
Valkey on the same VM: 0.4ms p50. It's just Redis with a different license and governance. Setup was one docker run command:
docker run -d --name valkey -p 6379:6379 \
--restart unless-stopped \
valkey/valkey:7.2-alpine \
valkey-server --maxmemory 64mb --maxmemory-policy allkeys-lru
The Verdict
| Use Case | Winner | Why |
|---|---|---|
| Bursty, low-volume | Upstash | Pay-per-request scales to zero |
| Small persistent cache | Redis Cloud | 30MB storage-based free tier, no command counting |
| Co-located with your app | Valkey | Sub-millisecond, no egress, no surprises |
For my webhook dedup (same VM, tiny data, latency-sensitive), Valkey wins by a mile. If I were building a serverless function that sleeps between invocations, Upstash's pay-per-request model would be the only sane choice.
The Tooling
I sketched the benchmark harness and the Valkey Docker setup with MonkeyCode — the free AI coding assistant that runs entirely on your machine. It generated the retry-aware command counting logic that caught Upstash's free tier trap: https://ly.cyberserval.tech/iIETXiF
What's your free-tier KV strategy? I keep going back and forth on whether "self-hosted on the same box" is cheating or just good architecture. Convince me in the comments.
Top comments (0)