The Complete Redis Guide — Features, Best Practices & Patterns

A comprehensive reference for Redis data structures, architecture, and production best practices.

🌱 Seedling·created: ·category:Databases

A comprehensive reference for Redis data structures, architecture, operational patterns, and production best practices.


Table of Contents

  1. Introduction & Core Concepts
  2. Data Types Deep Dive
  3. Expiration & Eviction
  4. Persistence: RDB & AOF
  5. Replication
  6. Redis Sentinel (High Availability)
  7. Redis Cluster (Sharding)
  8. Transactions & Scripting
  9. Pub/Sub & Streams
  10. Common Design Patterns
  11. Caching Strategies
  12. Performance & Memory Optimization
  13. Security Best Practices
  14. Monitoring & Observability
  15. Anti-Patterns to Avoid
  16. Client Libraries & Connection Pooling
  17. Backup, Disaster Recovery & Upgrades

1. Introduction & Core Concepts

Redis (REmote DIctionary Server) is an in-memory data structure store used as a database, cache, message broker, and streaming engine. Key architectural facts every engineer should internalize:

  • Single-threaded execution model for command processing (I/O threading exists since Redis 6 for network reads/writes, but command execution itself is still single-threaded). This means Redis commands are effectively atomic relative to each other — no two commands run in parallel on the same instance.
  • In-memory first: all active data lives in RAM; persistence to disk is optional and asynchronous by default.
  • Rich data types, not just key-value strings — this is what separates Redis from Memcached.
  • Extremely low latency: sub-millisecond for most operations because there’s no disk I/O in the hot path.

Why single-threadedness matters

Because only one command executes at a time:

  • Long-running commands (e.g., KEYS *, unbounded SORT, huge SMEMBERS) block everything else, including health checks — this is a leading cause of production incidents.
  • You never need application-side locking for a single Redis operation, but you DO need to be careful when doing multi-step “read-modify-write” logic (see Transactions section).

2. Data Types Deep Dive

String

The most basic type; can hold text, serialized JSON, or binary data up to 512MB.

SET user:1000:name "Ayşe"
GET user:1000:name
INCR page:views          # atomic counter
INCRBY wallet:1000 500
SETEX session:abc123 3600 "token-data"   # TTL in one call
SETNX lock:job:42 "worker-1"             # only set if not exists (naive locking)

Best practice: Use SET key value EX seconds NX instead of separate SETNX + EXPIRE — the combined form is atomic; doing it as two calls creates a race condition window.

SET lock:job:42 "worker-1" EX 30 NX

Hash

Ideal for representing objects (like a row/document) without needing multiple keys.

HSET user:1000 name "Ayşe" email "ayse@example.com" age 29
HGET user:1000 name
HGETALL user:1000
HINCRBY user:1000 login_count 1
HDEL user:1000 age

Best practice: Prefer one hash per entity over many flat string keys (user:1000:name, user:1000:email, …). Hashes are far more memory-efficient for small objects due to internal listpack encoding, and let you fetch/update the whole entity atomically.

List

Ordered collection, implemented as a linked list of quicklist nodes. Great for queues, timelines, recent-activity feeds.

LPUSH queue:jobs "job1"
RPUSH queue:jobs "job2"
LPOP queue:jobs
BRPOP queue:jobs 5              # blocking pop with 5s timeout — basis of simple job queues
LRANGE timeline:user:1000 0 20  # latest 20 items
LTRIM timeline:user:1000 0 999  # cap list size (keep only latest 1000)

Best practice: Always LTRIM capped lists (e.g., activity feeds) to prevent unbounded memory growth.

Set

Unordered unique collection. Excellent for tags, unique visitor tracking, relationship graphs.

SADD tags:post:55 "redis" "database" "cache"
SISMEMBER tags:post:55 "redis"
SINTER online:users vip:users        # intersection: online VIP users
SUNIONSTORE all:tags tags:post:55 tags:post:56
SCARD tags:post:55                    # count

Sorted Set (ZSet)

Unique members ordered by a floating-point score. One of Redis’s most powerful structures — backs leaderboards, rate limiters, priority queues, delayed job scheduling.

ZADD leaderboard 1500 "player1" 2200 "player2"
ZINCRBY leaderboard 50 "player1"
ZREVRANGE leaderboard 0 9 WITHSCORES      # top 10
ZRANK leaderboard "player1"
ZRANGEBYSCORE leaderboard 1000 2000
ZREMRANGEBYRANK leaderboard 0 -1001       # keep top 1000, trim rest

Pattern — time-ordered events: use a timestamp as the score to get a naturally sorted event log, then ZRANGEBYSCORE for range queries by time window.

Bitmaps

Strings treated as bit arrays — extremely memory-efficient for boolean flags at scale (e.g., daily active user tracking).

SETBIT active:2026-08-17 1000 1     # user 1000 was active today
BITCOUNT active:2026-08-17          # daily active users
BITOP AND result active:mon active:tue   # active both days

HyperLogLog

Probabilistic structure for approximate cardinality (unique counts) using only ~12KB regardless of set size, with ~0.81% standard error.

PFADD unique:visitors:2026-08-17 "user1" "user2" "user3"
PFCOUNT unique:visitors:2026-08-17
PFMERGE unique:visitors:month unique:visitors:2026-08-17 unique:visitors:2026-08-18

Use when: you need approximate unique counts (page views, distinct search terms) at massive scale and can’t afford a full Set’s memory cost.

Geospatial

Built on sorted sets internally; stores lat/lng and supports radius/box queries.

GEOADD shops -0.1276 51.5072 "shop:london"
GEOSEARCH shops FROMLONLAT -0.12 51.50 BYRADIUS 5 km ASC
GEODIST shops shop:london shop:paris km

Streams

An append-only log data type (since Redis 5) modeled after Kafka-style logs — the right choice for event sourcing, activity logs, and multi-consumer pipelines (covered more in Section 9).

XADD events:orders '*' order_id 1001 status "created"
XRANGE events:orders - +
XREAD COUNT 10 STREAMS events:orders 0

3. Expiration & Eviction

TTL Mechanics

EXPIRE session:abc 3600
PEXPIRE session:abc 3600000     # milliseconds
TTL session:abc                 # seconds remaining, -1 = no TTL, -2 = doesn't exist
PERSIST session:abc             # remove TTL

Redis uses two eviction mechanisms for expired keys:

  1. Passive: checked lazily when the key is accessed.
  2. Active: a background cycle randomly samples keys with TTLs and removes expired ones, running periodically to bound memory held by stale data even if never accessed again.

Eviction Policies (when maxmemory is reached)

PolicyBehavior
noevictionReturns errors on writes when memory full (default)
allkeys-lruEvicts least-recently-used key across all keys
volatile-lruEvicts LRU among keys with a TTL set
allkeys-lfuEvicts least-frequently-used key (better for skewed access patterns)
volatile-lfuLFU among keys with TTL
allkeys-randomRandom eviction
volatile-randomRandom eviction among keys with TTL
volatile-ttlEvicts keys with shortest remaining TTL first

Best practice: If Redis is used purely as a cache, use allkeys-lru or allkeys-lfu. If Redis mixes cache data and durable data (e.g., session + persistent counters) in the same instance, use volatile-lru/volatile-lfu and make sure durable keys never get a TTL — but strongly consider separating cache and primary-store workloads into different instances instead.


4. Persistence: RDB & AOF

RDB (Redis Database Snapshot)

Point-in-time binary snapshot, triggered by SAVE (blocking) or BGSAVE (forks a child process, non-blocking).

save 900 1        # snapshot if >=1 write in 900s
save 300 10        
save 60 10000
  • Pros: compact single file, fast restarts, good for backups.
  • Cons: data loss window between snapshots; fork() on very large datasets can cause latency spikes (copy-on-write memory pressure).

AOF (Append Only File)

Logs every write operation; replayed on restart.

appendonly yes
appendfsync everysec     # fsync every second (best balance)
# appendfsync always     # fsync every write — safest, slowest
# appendfsync no         # let OS decide — fastest, least safe

Since Redis 7, AOF uses a multi-part format (base file + incremental files + manifest), making rewrites safer and avoiding the old single-giant-file rewrite risk.

  • Pros: much smaller data-loss window (≤1s with everysec), human-auditable log.
  • Cons: larger file size than RDB, potentially slower restarts (replaying the whole log), though AOF rewrite compacts it periodically.

Use both RDB + AOF together: RDB for fast, compact backups/restores; AOF for minimal data loss. Redis will use AOF on restart if enabled (more complete), falling back to RDB otherwise.

appendonly yes
appendfsync everysec
save 3600 1
save 300 100
save 60 10000

Best practice: Always test your restore procedure — a backup you’ve never restored is not a backup.


5. Replication

Redis replication is asynchronous, leader-follower (primary-replica). A replica connects, performs an initial full sync (RDB transfer), then receives a continuous command stream.

# On replica:
replicaof 10.0.0.1 6379
# or dynamically:
REPLICAOF 10.0.0.1 6379
REPLICAOF NO ONE     # promote to primary

Key facts:

  • Replication is asynchronous by default — a write can be acknowledged to the client before all replicas have it. Use WAIT numreplicas timeout if you need stronger durability guarantees before considering a write “safe.”
  • Replicas are read-only by default (replica-read-only yes) — good practice, don’t disable this casually.
  • Chained replication (replica-of-replica) is supported to reduce load on the primary during fan-out.

Best practice: Never treat asynchronous replication as a substitute for durability (AOF/RDB) — it protects against node failure, not against data loss on a crash before replication completes.


6. Redis Sentinel (High Availability)

Sentinel provides automatic failover, monitoring, and service discovery for a primary-replica setup without sharding.

sentinel monitor mymaster 10.0.0.1 6379 2   # quorum = 2 sentinels must agree
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1
  • Run at least 3 Sentinel processes on separate hosts for quorum-based decisions (avoids split-brain from a single Sentinel’s false positive).
  • Applications should connect via a Sentinel-aware client that discovers the current primary dynamically, not a hardcoded IP.
  • Sentinel handles failover orchestration; it does not shard data — for that, use Redis Cluster.

7. Redis Cluster (Sharding)

Redis Cluster partitions data across multiple primary nodes using 16384 hash slots. Each key is mapped to a slot via CRC16(key) mod 16384.

redis-cli --cluster create \
  10.0.0.1:6379 10.0.0.2:6379 10.0.0.3:6379 \
  10.0.0.4:6379 10.0.0.5:6379 10.0.0.6:6379 \
  --cluster-replicas 1

Hash Tags for Multi-Key Operations

Multi-key commands (MGET, transactions, Lua scripts) require all keys to live on the same slot. Force co-location using {} hash tags:

user:{1000}:profile
user:{1000}:settings
user:{1000}:sessions

All three hash to the same slot because only the substring inside {} is hashed.

  • Cluster tolerates node failure via automatic failover (each primary should have ≥1 replica).
  • Client must support cluster redirection (MOVED/ASK responses) — use a cluster-aware client library, never a plain single-node client.
  • Cross-slot multi-key operations will error (CROSSSLOT) unless hash-tagged.

Best practice: Design your key schema with hash tags from day one if you anticipate needing Cluster — retrofitting hash tags into an existing large dataset is painful.


8. Transactions & Scripting

MULTI/EXEC

Queues commands and executes them atomically as a batch (not truly “rollback-on-error” like SQL transactions — a runtime error in one command doesn’t abort the others).

MULTI
INCR counter
LPUSH log "event"
EXEC

WATCH (Optimistic Locking)

Used for compare-and-swap style logic (read, compute, conditional write).

WATCH balance:user:1000
val = GET balance:user:1000
# ... compute new value in application ...
MULTI
SET balance:user:1000 newVal
EXEC     # returns nil if balance:user:1000 changed since WATCH — retry logic needed

Lua Scripting (EVAL) and Functions

Scripts execute atomically (the whole script runs as one indivisible unit, blocking other commands).

EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey myvalue

Since Redis 7, Redis Functions (FUNCTION LOAD) are the recommended replacement for ad-hoc EVAL scripts — they’re versioned, named, and persisted properly across replication/AOF, unlike scripts cached only in the script cache.

Best practice: Keep Lua scripts short. A slow script blocks the entire server just like a slow command — there is no timeout enforcement that safely interrupts a running script without an admin SCRIPT KILL intervention (which itself can’t interrupt write scripts safely).


9. Pub/Sub & Streams

Pub/Sub

Fire-and-forget messaging. No persistence, no delivery guarantee — if no subscriber is listening, the message is lost.

SUBSCRIBE news:tech
PUBLISH news:tech "Redis 8 released"
PSUBSCRIBE news:*          # pattern subscribe

Use for: real-time notifications, cache-invalidation broadcast, live dashboards — never for anything requiring durability.

Streams (the durable alternative)

Streams solve Pub/Sub’s durability gap with an append-only log, consumer groups, and acknowledgment.

XADD orders:stream '*' order_id 123 status "paid"

# Consumer group setup
XGROUP CREATE orders:stream processors '$' MKSTREAM
XREADGROUP GROUP processors worker-1 COUNT 10 STREAMS orders:stream '>'
XACK orders:stream processors 1234567-0
XPENDING orders:stream processors        # inspect unacknowledged messages
XCLAIM orders:stream processors worker-2 60000 1234567-0   # reassign stuck message

Best practice: Cap stream size to prevent unbounded growth:

XADD orders:stream MAXLEN ~ 100000 '*' order_id 123

The ~ makes trimming approximate (more efficient, avoids trimming exactly on every insert).


10. Common Design Patterns

Distributed Lock (Redlock-style, simplified single-instance)

SET lock:resource:42 "unique-random-token" NX EX 10
# ... critical section ...
# Release only if you still own it (avoid deleting someone else's lock):
EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end" 1 lock:resource:42 "unique-random-token"

For multi-instance safety, the Redlock algorithm acquires the lock across a majority of independent Redis nodes. Note: Redlock’s correctness under network partitions has been debated (see Martin Kleppmann’s critique) — for hard correctness guarantees, prefer a consensus-based lock service (e.g., Zookeeper, etcd) over Redis for safety-critical locking.

Rate Limiting (Sliding Window with Sorted Sets)

ZADD ratelimit:user:1000 <timestamp> <unique-request-id>
ZREMRANGEBYSCORE ratelimit:user:1000 0 <timestamp - window>
ZCARD ratelimit:user:1000     # if > limit, reject
EXPIRE ratelimit:user:1000 60

Rate Limiting (Simple Fixed Window)

INCR ratelimit:user:1000:minute:202608171530
EXPIRE ratelimit:user:1000:minute:202608171530 60

Leaderboard

ZADD leaderboard:game1 <score> <player_id>
ZREVRANGE leaderboard:game1 0 9 WITHSCORES
ZREVRANK leaderboard:game1 <player_id>

Job Queue (simple)

LPUSH queue:emails '{"to":"x@y.com","template":"welcome"}'
BRPOP queue:emails 0            # worker blocks until job arrives

For reliability (avoid losing a job if the worker crashes mid-processing), use BRPOPLPUSH/LMOVE into a “processing” list, then remove on success:

BLMOVE queue:emails queue:emails:processing 0 LEFT RIGHT
# on success:
LREM queue:emails:processing 1 <job>

For production-grade queues with retries, delayed jobs, and dead-letter handling, prefer Streams with consumer groups over raw lists.

Session Store

HSET session:abc123 user_id 1000 role "admin" ip "1.2.3.4"
EXPIRE session:abc123 1800

Caching (Cache-Aside)

1. App reads cache:key
2. If miss → read from DB → SET cache:key value EX ttl
3. If hit → return cached value

Idempotency Keys

SET idempotency:req:abc123 "processed" NX EX 86400
# if SET returns nil, request was already processed — skip reprocessing

Counters with Expiring Windows (Analytics)

INCR pageviews:2026-08-17
EXPIRE pageviews:2026-08-17 2592000   # keep 30 days

11. Caching Strategies

StrategyDescriptionTrade-off
Cache-Aside (Lazy Loading)App checks cache, falls back to DB on miss, populates cacheSimple, but first request after eviction is slow; risk of thundering herd
Write-ThroughEvery write goes to cache and DB synchronouslyCache always fresh, but write latency increases
Write-Behind (Write-Back)Write to cache immediately, async flush to DBFast writes, risk of data loss if cache crashes before flush
Read-ThroughCache layer itself fetches from DB on miss (via a caching library)Cleaner app code, needs caching middleware

Preventing Thundering Herd / Cache Stampede

When a hot key expires, many concurrent requests may simultaneously miss and hammer the DB. Mitigations:

  • Probabilistic early expiration: recompute slightly before actual TTL, weighted by random probability.
  • Locking: first request to miss acquires a short lock and recomputes; others wait or serve stale.
  • Never-expire + background refresh: keep serving stale data while a background job refreshes it.

Preventing Cache Penetration (queries for non-existent keys)

Cache negative results too (short TTL), or use a Bloom filter to reject queries for keys that definitely don’t exist before hitting the DB.

Cache Key Design

  • Use consistent, hierarchical naming: <namespace>:<entity>:<id>:<field> e.g. app:user:1000:profile.
  • Include a version prefix (v2:user:1000) to allow instant cache invalidation on schema changes by bumping the version.

12. Performance & Memory Optimization

Avoid Slow Commands in Production

  • KEYS * — scans the entire keyspace, blocking. Use SCAN (cursor-based, non-blocking) instead.
SCAN 0 MATCH user:* COUNT 100
  • FLUSHALL/FLUSHDB — obviously destructive, but also blocking on large datasets unless ASYNC is used:
FLUSHALL ASYNC
  • Unbounded SORT, SMEMBERS, HGETALL, LRANGE 0 -1 on huge collections — use SSCAN/HSCAN/LRANGE with bounded ranges/pagination instead.

Pipelining

Batch multiple commands in one round trip to eliminate network latency overhead:

redis-cli --pipe <<EOF
SET a 1
SET b 2
SET c 3
EOF

In client code, pipelining can turn thousands of round trips into one, often a 10-100x throughput improvement for bulk operations.

Memory Optimization

  • Use MEMORY USAGE <key> to inspect per-key memory cost.
  • Prefer hashes over many string keys for small objects (internal listpack encoding is compact below configurable thresholds):
hash-max-listpack-entries 128
hash-max-listpack-value 64
  • Same principle applies to small lists, sets, and sorted sets — tune list-max-listpack-size, set-max-listpack-entries, zset-max-listpack-entries.
  • Use short key names and short field names at scale — every byte is duplicated across millions of keys.
  • Consider OBJECT ENCODING <key> to verify Redis is using the compact encoding you expect.

Connection & Command Efficiency

  • Use MGET/MSET instead of looping single GET/SET.
  • Use EXPIRE sparingly on hot loops; prefer setting TTL once at creation.
  • Avoid DEBUG commands and MONITOR in production — MONITOR in particular has severe throughput impact since it streams every command to the client.

13. Security Best Practices

  • Never expose Redis directly to the internet. Bind to private interfaces only:
bind 127.0.0.1 10.0.0.5
protected-mode yes
  • Require authentication:
requirepass "a-very-long-random-secret"
  • Use ACLs (Redis 6+) instead of a single shared password — scope users to specific commands and key patterns:
ACL SETUSER app-readonly on >password ~app:* +get +mget -@all
ACL SETUSER app-writer on >password ~app:* +@read +@write -flushall -flushdb -config
  • Rename or disable dangerous commands in production:
rename-command FLUSHALL ""
rename-command CONFIG "CONFIG_9f8a7b6c"
  • Enable TLS for data in transit (Redis 6+ supports native TLS):
tls-port 6379
tls-cert-file /path/redis.crt
tls-key-file /path/redis.key
tls-ca-cert-file /path/ca.crt
  • Network isolation: place Redis in a private subnet/VPC, restrict access via security groups/firewalls, never rely on requirepass alone as your only defense.
  • Avoid storing plaintext secrets as values without application-level encryption if compliance requires it.

14. Monitoring & Observability

Key Metrics to Track

INFO all

Watch for:

  • used_memory / used_memory_rss / mem_fragmentation_ratio (ideal ~1.0–1.5; much higher indicates fragmentation)
  • connected_clients
  • instantaneous_ops_per_sec
  • keyspace_hits / keyspace_misses (hit ratio = hits / (hits+misses))
  • evicted_keys — non-zero and rising means you’re under memory pressure
  • expired_keys
  • rejected_connections
  • blocked_clients
  • master_repl_offset / replication lag (INFO replication on replicas)
  • latest_fork_usec — high values indicate BGSAVE/BGREWRITEAOF fork latency impact

Slow Query Log

CONFIG SET slowlog-log-slower-than 10000   # log commands slower than 10ms (microseconds unit)
SLOWLOG GET 25
SLOWLOG RESET

Latency Monitoring

CONFIG SET latency-monitor-threshold 100    # ms
LATENCY HISTORY command
LATENCY LATEST
  • redis-cli --latency / --latency-history for live latency sampling.
  • redis-cli --bigkeys to scan for oversized keys that risk blocking operations or memory imbalance.
  • Prometheus + redis_exporter for time-series metrics and alerting.
  • redis-cli --stat for a live-updating summary view.

15. Anti-Patterns to Avoid

  1. Using KEYS * in application code — always SCAN.
  2. Storing huge values in a single key (multi-MB blobs) — degrades performance for that shard/slot and complicates replication; consider chunking or external object storage with Redis only holding a pointer.
  3. No TTL on cache keys — leads to unbounded memory growth and stale data.
  4. One giant Redis instance for everything (cache + queue + session + pub/sub) — a slow command or eviction storm in one workload disrupts unrelated workloads; isolate by use case where practical.
  5. Ignoring maxmemory-policy — leaving it as noeviction on a cache-only instance causes write errors instead of graceful eviction.
  6. Treating replication as backup — it isn’t; a bad DEL/FLUSHALL replicates instantly to all replicas.
  7. Not hash-tagging keys that need multi-key atomic operations before scaling to Cluster.
  8. Blindly trusting client-side connection pools without health checks — stale connections after failover cause silent errors.
  9. Running MONITOR in production for debugging — significant throughput hit.
  10. Not setting appendfsync everysec or equivalent durability for data you can’t afford to lose, while assuming “Redis persists to disk” is automatically safe.
  11. Ignoring mem_fragmentation_ratio — over time fragmentation can silently consume far more RSS than used_memory suggests; consider activedefrag yes for long-lived instances.
  12. Synchronous cross-region calls to Redis — always deploy Redis close (same AZ/region) to its application tier; cross-region round trips defeat the purpose of an in-memory store.

16. Client Libraries & Connection Pooling

  • Always use a connection pool, never open a new TCP connection per request.
  • Popular clients: redis-py (Python), ioredis/node-redis (Node.js), Jedis/Lettuce (Java), go-redis (Go), StackExchange.Redis (.NET).
  • For Cluster deployments, use the cluster-aware variant of your client (e.g., redis-py-cluster support built into redis-py>=4, ioredis Cluster mode, Lettuce cluster client) — these correctly handle MOVED/ASK redirections and slot caching.
  • Configure sane timeouts (socket_timeout, connect_timeout) — a hung Redis call without a timeout can cascade into application-wide thread pool exhaustion.
  • Prefer clients that support RESP3 protocol (Redis 6+) for features like client-side caching and better push-message support.

17. Backup, Disaster Recovery & Upgrades

  • Schedule regular BGSAVE snapshots and ship RDB files off-host (S3, GCS, etc.) on a rotation.
  • Test restore procedures on a non-production instance regularly — an untested backup is a liability.
  • For major version upgrades, always test against a replica first, review the release’s breaking-changes notes, and roll out with a plan to fail back to the previous version.
  • For Cluster upgrades, use rolling replica-first upgrades: upgrade replicas, fail over, then upgrade the former primaries.
  • Keep redis.conf under version control and treat configuration as code — untracked manual CONFIG SET changes are a common source of “it worked before the restart” incidents (since CONFIG SET is runtime-only unless followed by CONFIG REWRITE).

Quick Reference: Command Complexity Cheat Sheet

CommandComplexityNote
GET/SETO(1)
HGET/HSETO(1)
LPUSH/RPUSHO(1)
LRANGEO(S+N)S=start offset, N=elements returned — avoid large N
SADD/SISMEMBERO(1) avg
ZADDO(log N)
ZRANGEO(log N + M)M=elements returned
KEYSO(N)Avoid in production
SCANO(1) per callCursor-based, safe for production
SORTO(N log N)Can be expensive on large collections
FLUSHALLO(N)Use ASYNC

This guide reflects Redis behavior as of the Redis 7.x/8.x generation. Always cross-check against the official documentation at redis.io for the exact version you’re running, since defaults and available commands evolve between releases.

This note is part of the Digital Garden — a collection of connected, evolving thoughts.