Redis keys are binary-safe names in a flat keyspace, not filesystem paths or schema declarations. Colons are a useful convention for separating ownership, environment, entity, and identifier. A name such as cart:prod:string:42 can explain intent, but Redis does not interpret those labels as a type check or access restriction.
Make the convention unambiguous
Agree on field order and identifier encoding across writers, readers, maintenance jobs, and migrations. Distinct prefixes separate names, but identical encoded names still collide. A convention helps people cooperate; it does not create an independently managed namespace.
A type label is documentation. SET can create a string under a key whose name contains hash, and HSET will then report WRONGTYPE. Use TYPE to inspect data, and keep serialization and type expectations in the application contract. Names cannot prove value types.
Prefer short, readable names, not cryptic abbreviations or entire documents embedded in keys. Names consume memory and travel in command arguments, but object overhead and payloads also matter. A naming change is not evidence of faster queries without measurement. Encode identifiers so separator characters, glob metacharacters, and cluster braces cannot acquire unintended meaning.
Match patterns without inventing deletion semantics
SCAN MATCH accepts a glob pattern. Start with cursor zero, reuse each returned cursor, and continue until zero returns again. COUNT is a work hint, not an exact result count or memory ceiling. A call may return no matches while the cursor remains nonzero. Each call still performs server work; incremental iteration does not mean zero blocking cost.
A full scan returns matching keys present throughout it, but it may return duplicates. Keys added or removed during the scan have weaker guarantees. Deduplicate when necessary or make processing safe to repeat. This is not a snapshot, and an empty page is not the end of the collection.
DEL has different semantics: it receives exact key names, not patterns. Sending an argument ending in an asterisk does not delete matching siblings. It only deletes a key literally containing that asterisk if one exists. Shell expansion is another layer, so quote arguments when using command-line tools; do not mistake shell behavior for Redis pattern matching.
When it breaks
A maintenance script that assumes DEL expands a prefix may remove nothing and falsely report cleanup. The opposite mistake is collecting a broad SCAN result and deleting every match without checking ownership. Review exact targets and the mutation policy. A key can be recreated between discovery and deletion, so a previously reviewed name alone may not identify the same logical object later.
Prefixes are not security boundaries. Restrict access with appropriate ACL key patterns and command permissions, or with separate deployment boundaries when required. Key-pattern permissions do not constrain commands that operate on an entire database without key arguments. A visible service prefix also does not reserve memory or isolate eviction.
Redis Cluster hash tags are an intentional exception to otherwise conventional naming. A valid brace-delimited hash tag selects the bytes used for slot calculation. Different service prefixes can therefore share a slot when their tags match. This can support related multi-key work, but putting every key under one constant tag can concentrate load instead of distributing it.
Check it quickly
Use a POSIX environment with a C compiler, make, curl, tar, and Python 3.10 or newer. This ran on macOS with Python 3.14.4, Redis 8.10.1, and redis-py 8.1.0. The setup compiles Redis core with libc and installs the client in a temporary virtual environment, not a system service.
redis_lab=$(mktemp -d)
curl --fail --location --output "$redis_lab/redis.tar.gz" https://download.redis.io/releases/redis-8.10.1.tar.gz
tar -xzf "$redis_lab/redis.tar.gz" -C "$redis_lab"
make -C "$redis_lab/redis-8.10.1" build redis -j2 MALLOC=libc
export REDIS_SERVER="$redis_lab/redis-8.10.1/src/redis-server"
python3 -m venv "$redis_lab/venv"
"$redis_lab/venv/bin/python" -m pip install redis==8.1.0
Run the Python block with "$redis_lab/venv/bin/python" in the same shell. It starts a private Unix-socket Redis under /tmp with TCP and persistence disabled, then stops only its own process and cleans its runtime directory. Downloads require network access. Existing Redis instances are not contacted.
from contextlib import contextmanager
import os
from pathlib import Path
import subprocess
from tempfile import TemporaryDirectory
import time
import redis
from redis.backoff import NoBackoff
from redis.retry import Retry
@contextmanager
def instance():
with TemporaryDirectory(prefix="redis-lab-", dir="/tmp") as directory:
endpoint = str(Path(directory) / "redis.sock")
process = subprocess.Popen([
os.environ["REDIS_SERVER"], "--port", "0",
"--unixsocket", endpoint, "--unixsocketperm", "700",
"--save", "", "--appendonly", "no", "--dir", directory,
"--maxmemory-policy", "noeviction",
], stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
client = redis.Redis(unix_socket_path=endpoint, decode_responses=False,
socket_timeout=2, retry=Retry(NoBackoff(), 0))
try:
deadline = time.monotonic() + 5
while True:
assert process.poll() is None, "Redis exited during startup"
try:
if client.ping():
break
except redis.ConnectionError:
if time.monotonic() >= deadline:
raise
time.sleep(0.02)
yield client
finally:
client.close()
if process.poll() is None:
process.terminate()
try:
_, errors = process.communicate(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
process.communicate()
raise
assert process.returncode == 0, errors.decode()
with instance() as client:
client.set("cart:prod:string:42", "cart-value")
client.set("billing:prod:string:42", "billing-value")
assert client.get("cart:prod:string:42") == b"cart-value"
assert client.get("billing:prod:string:42") == b"billing-value"
client.set("cart:prod:hash:42", "not-a-hash")
assert client.type("cart:prod:hash:42") == b"string"
try:
client.hset("cart:prod:hash:42", "item", "value")
except redis.ResponseError as error:
assert "WRONGTYPE" in str(error)
else:
raise AssertionError("a type label unexpectedly enforced a schema")
print("namespaces separate exact names; a hash label does not create a hash value")
prefix = "cart:prod:string:"
expected = {f"{prefix}{number}".encode() for number in range(24)}
for key in expected:
client.set(key, "value")
expected.add(b"cart:prod:string:42")
cursor, found, pages = 0, set(), 0
while True:
cursor, keys = client.scan(cursor=cursor, match=prefix + "*", count=3)
assert all(key.startswith(prefix.encode()) for key in keys)
found.update(keys)
pages += 1
assert pages < 1000, "unexpected nontermination in this bounded test"
if cursor == 0:
break
assert found == expected
print("SCAN reaches cursor zero and finds the stable fixture, with duplicates deduplicated")
assert client.delete(prefix + "*") == 0
assert all(client.exists(key) for key in expected)
client.set(prefix + "*", "literal-star")
assert client.delete(prefix + "*") == 1
assert all(client.exists(key) for key in expected)
print("DEL treats the asterisk literally; it does not expand a key pattern")
from redis.crc import key_slot
assert key_slot(b"cart:{customer-42}:items") == key_slot(b"billing:{customer-42}:total")
assert key_slot(b"cart:{customer-42}:items") == key_slot(b"customer-42")
print("matching hash tags select the same slot across different service prefixes")
The first case stores different values under cart and billing prefixes, then writes a string under a name labeled hash. TYPE reports string and HSET fails, proving that readable labels do not enforce a schema. These are separate exact names, not nested directories.
A stable fixture contains 25 matching string keys. The scan loop accumulates them until cursor zero, tolerating page size variation and deduplicating results. Its bounded test guard detects unexpected nontermination for this small fixture; it is not a universal production scan limit or evidence about concurrently changing keys.
Deleting the literal prefix-plus-asterisk first returns zero while all 25 keys remain. Creating exactly that literal name makes the next deletion return one, still leaving the siblings intact. No wildcard cleanup or shared data deletion is performed.
Finally, the pinned client’s key_slot helper confirms that two differently prefixed names with customer-42 in braces compute the same slot. This is a client-side calculation, not a live Redis Cluster test. Four summaries follow assertions; the exercise does not validate ACL enforcement, concurrent deletion safety, cluster routing, or memory savings.
Sources
- Redis keys and values, binary-safe keys and naming conventions, checked 2026-09-08.
- Redis SCAN, cursor guarantees, duplicates, and COUNT hints, checked 2026-09-08.
- Redis DEL, deleting exact key arguments, checked 2026-09-08.
- Redis TYPE, inspecting the actual value type, checked 2026-09-08.
- Redis ACL documentation, key patterns and separate command permissions, checked 2026-09-08.
- Redis Cluster specification, hash tags and slot placement, checked 2026-09-08.
- redis-py 8.1.0 key-slot calculation, client-side hashing used in the exercise, checked 2026-09-08.
- Redis 8.10.1 build instructions, core-only compilation and allocator selection, checked 2026-09-08.