Deep dive
Service map
| Need | AWS | GCP | Azure | Self-host / others |
| In-memory cache | ElastiCache (Valkey, Redis OSS, Memcached), MemoryDB (durable) | Memorystore | Azure Cache for Redis / Managed Redis | Valkey, Redis, Dragonfly, Memcached, Upstash |
| Work queue | SQS (standard, FIFO) | Cloud Tasks, Pub/Sub | Service Bus queues, Storage Queues | RabbitMQ, NATS JetStream, Postgres SKIP LOCKED |
| Pub/sub fan-out | SNS, EventBridge | Pub/Sub | Event Grid, Service Bus topics | NATS, Redis Streams |
| Replayable log | Kinesis, MSK | Pub/Sub (seek), Managed Kafka | Event Hubs (Kafka API) | Kafka, Redpanda, WarpStream |
Delivery semantics
| System | Delivery | Ordering | Replay | Max msg |
| SQS standard | At least once | Best effort | No | 256 KB (1 MB since 2025) |
| SQS FIFO | Exactly-once processing within 5 min dedupe window | Per message group | No | same |
| Pub/Sub | At least once (exactly-once option) | Per ordering key | Seek to timestamp | 10 MB |
| Kafka / Kinesis | At least once; exactly-once with transactions | Per partition / shard | Yes, by offset | 1 MB default |
| Redis Streams | At least once with consumer groups | Per stream | Yes, while retained | Memory-bound |
Decision table
| Situation | Choice | Why |
| Background jobs, retries, DLQ, low ops | SQS / Cloud Tasks / Service Bus | Pay per request, zero ops |
| Jobs in a small app that already has Postgres | Postgres queue table with FOR UPDATE SKIP LOCKED | One less system, transactional enqueue |
| Event streaming, many consumers, replay | Kafka-compatible log | Offsets, retention, partitions |
| Low-latency pub/sub between services | NATS | Order of 100 µs, simple |
| Session/cache data, loss tolerable | Valkey/Redis, self-host or managed | Cheap, fast |
| Cache must survive node loss without warmup storm | Managed Redis with replicas, or MemoryDB | Replica promotion keeps it hot |
| > 100 MB/s sustained log | Self-hosted Kafka/Redpanda, or WarpStream-style S3-backed | Managed per-GB and cross-AZ charges scale linearly |
Code
Cache-aside with stampede protection (Rust, redis crate):
use redis::{cmd, Connection, RedisResult};
use std::time::{SystemTime, UNIX_EPOCH};
/// Cache-aside with a per-key refill lock and jittered TTL (redis crate, raw commands).
fn get_user(con: &mut Connection, uid: u64, load: impl Fn(u64) -> String, ttl: u64) -> RedisResult<String> {
let key = format!("user:{uid}");
if let Some(hit) = cmd("GET").arg(&key).query::<Option<String>>(con)? {
return Ok(hit);
}
// SET lock:key 1 NX EX 10 -> only one caller per key refills the cache
let lock = format!("lock:{key}");
let won = cmd("SET").arg(&lock).arg(1).arg("NX").arg("EX").arg(10).query::<Option<String>>(con)?;
let val = load(uid); // losers: better to sleep 10-50 ms and re-read
if won.is_some() {
let nanos = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().subsec_nanos() as u64;
let jitter = nanos % (ttl / 10 + 1); // spread expiries of keys written together
cmd("SET").arg(&key).arg(&val).arg("EX").arg(ttl + jitter).query::<()>(con)?;
cmd("DEL").arg(&lock).query::<()>(con)?;
}
Ok(val)
}
Postgres as a job queue:
CREATE TABLE jobs (id bigserial PRIMARY KEY, kind text, payload jsonb,
run_at timestamptz DEFAULT now(), attempts int DEFAULT 0);
CREATE INDEX ON jobs (run_at);
-- worker loop: claim one job without blocking other workers
WITH j AS (SELECT id FROM jobs WHERE run_at <= now()
ORDER BY run_at LIMIT 1 FOR UPDATE SKIP LOCKED)
UPDATE jobs SET attempts = attempts + 1, run_at = now() + interval '5 minutes'
FROM j WHERE jobs.id = j.id RETURNING jobs.*;
-- on success: DELETE FROM jobs WHERE id = $1;
SQS consumer settings that prevent duplicate storms:
aws sqs set-queue-attributes --queue-url "$Q" --attributes '{
"VisibilityTimeout": "120", "ReceiveMessageWaitTimeSeconds": "20",
"RedrivePolicy": "{\"deadLetterTargetArn\":\"'"$DLQ_ARN"'\",\"maxReceiveCount\":\"5\"}" }'
Efficiency
- Cost: a cache read is one network round trip plus microseconds of server work, so the round trip bounds a naive client: at 250 ms RTT a server that handles 100k requests/s serves at most four requests/s per connection (Redis pipelining docs). SQS bills per request, and batch actions handle up to 10 messages per call (SQS Developer Guide, as of 2026-10). Bound: latency and request count.
- Levers:
- pipelining — many commands per round trip, and more operations per second on the server (Redis pipelining docs) — bulk reads and writes from one client
- SQS batch actions — up to 10 messages per call, fewer billed requests (SQS batch actions doc) — producers and consumers with a backlog
- SQS long polling, wait up to 20 s — fewer empty receives (SQS polling doc) — low-traffic queues
- jittered TTL plus a per-key refill lock — no synchronized expiry stampede (Code above) — hot keys
- admission policy such as W-TinyLFU — higher hit rate at equal memory (order of; see that entry) — skewed key popularity
- Naive → standard → state of the art:
- one command or message per call, short polling · one RTT and one billed request per item
- pipelined commands, batches of 10, long polling · RTTs and requests divided by the batch size
- admission-aware cache plus request coalescing · one origin load per missed key
Implementation notes
- Make every consumer idempotent (dedupe key in the DB); at-least-once is the norm everywhere.
- Visibility timeout must exceed worst-case processing time, or messages are delivered twice concurrently.
- Long polling (20 s) on SQS cuts empty-receive cost by orders of magnitude.
- Retry with Exponential backoff with jitter; send poison messages to a DLQ after N attempts.
- Size caches with a hit-rate target; eviction policy matters (W-TinyLFU style admission beats plain LRU on skewed loads).
Pitfalls
- Thundering herd: a popular key expires and 10k requests hit the DB at once. Use locks, early refresh or request coalescing.
- Redis
KEYS * or large HGETALL block the single-threaded server for seconds.
- Kafka cross-AZ replication: replication factor 3 over 3 zones pays inter-zone transfer twice per byte produced.
- FIFO queues cap throughput per message group; one hot group serializes everything.
- Caching errors or empty results without a short TTL hides recoveries.
Implementations
| name | lang | repo | install | verdict | licence | health | as_of | notes |
| redis-rs | Rust | redis-rs/redis-rs | cargo add redis | — | — | — | — | Redis/Valkey client, pipelines, async |
| Valkey | C | valkey-io/valkey | brew install valkey | — | — | — | — | Redis-compatible server, open licence fork |
| Memcached | C | memcached/memcached | brew install memcached | — | — | — | — | plain key-value cache, multithreaded |
| NATS Server | Go | nats-io/nats-server | brew install nats-server | — | — | — | — | pub/sub, JetStream persistence |
| Apache Kafka | Java | apache/kafka | brew install kafka | — | — | — | — | replayable partitioned log |
Sources
- Rajesh Nishtala et al., Scaling Memcache at Facebook, NSDI 2013. paper
- Jay Kreps, Neha Narkhede, Jun Rao, Kafka: a Distributed Messaging System for Log Processing, NetDB 2011.
- AWS, Amazon SQS Developer Guide (documentation). docs · Amazon ElastiCache pricing. pricing
- Redis documentation, Pipelining. docs