Caching and queues
Problem
Redis, SQS, Kafka, and in-process caches all store bytes. Developers paste the fastest local serializer into the cache “temporarily.” Months later another language must read the key. Or an attacker influences a value. The cache becomes a serialization and trust boundary that no one designed.
In plain language: a cache or queue is not “just memory for us.” It is a store that other processes may open. Other languages may open it. Future versions of your service may open it. Design it that way from the start.
Short answer
For shared caches and queues, use portable formats. Use an explicit schema or a documented JSON contract. See trust boundaries and multi-language systems (polyglot estates). Reserve language-native codecs for single-binary, trusted, non-shared state. That is only if the written security assumptions allow it.
Separate event log design from ephemeral cache values. See schema registries for event logs. Do not lower the portability bar just because time-to-live (TTL) is short. TTL is how long a key or message is allowed to live before automatic expiry. Size limits and poison-message handling matter as much as codec speed.
A poison message is a payload that repeatedly fails processing. It may be corrupt, too large, or schema-invalid. Without quarantine, it can crash a consumer loop forever.
Constraints that matter
| Store | Prefer | Avoid |
|---|---|---|
| Cross-service Redis | JSON, MessagePack, or Protobuf with a schema | pickle or Java serialization |
| Single-service memory cache | Native encoding or plain structs are OK if not shared | Accidentally exposing native values via an admin API |
| Durable bus | Schema culture plus a registry | Undocumented dual formats |
| Task queues | Portable job payloads with a version field | Opaque blobs with no documented reader |
This matters because “only our service writes Redis today” often becomes “three services read it next quarter.”
Decision frame
Can another process, language, or version read this key?
yes → portable + versioned contract
no → native is optional under documented trust
| Concern | Practice |
|---|---|
| Poison messages | Send them to a dead-letter queue; do not infinite-retry bad payloads |
| Schema change | Use a version field or subject; dual-read during transition |
| Large values | Store a pointer to object storage plus a small metadata message |
| PII in queues | Apply retention and redaction (payload surfaces) |
In other words, short TTL reduces how long a bad encoding lives. It does not make an unsafe format safe while it lives.
Failure modes
| Mistake | Outcome |
|---|---|
| pickle in Redis “only we write” | A second service appears later, or remote code execution becomes possible |
| No maximum size | Memory blowups |
| Dual formats without a version | Random consumers fail |
| Cache used as system of record | The evolution story is lost |
| Compressing without a framing version | Deploy skew between writers and readers |
Real-world sketch
A session cache stores MessagePack with a v version field and a documented schema. The auth service (Go) and the API (Python) share fixtures in continuous integration. A proposal to switch to Python pickle for speed dies in review. A future Node edge worker could not participate. Security also rejects native deserialize from Redis.
In this suite
| Resource | Role |
|---|---|
| Dashboard | Cost of candidate portable codecs in each language |
| Native entries | Cost of portability—not a green light for shared stores |
| Using this suite | Local comparisons only |
Experiments
Question: Are shared cache and queue payloads portable, versioned, and safe for every consumer that can read them?
Setup
- List cache keys or topics and all reader services and languages.
- Note the current encoding. Often it is native or ad hoc JSON.
- Record TTL, poison-message handling, and dead-letter queue behavior.
Procedure
- Apply the trust-boundary test. Multi-service or multi-language readers require a portable format.
- Encode a golden fixture. Consume it from each reader. Check logical equality.
- Deploy a compatible schema change. Confirm old readers still work.
- Inject a poison payload. Confirm quarantine rather than crash loops.
- Use the suite for size and speed among allowed portable codecs. Measure against the payload budget.
Decision rule
- Any cross-service reader plus native encoding means migrate to portable.
- No poison handling means fix operations before chasing serialize benchmarks.
Metrics
| Metric / signal | Role |
|---|---|
| Reader language and service count | Primary portability driver |
| Interop matrix pass rate | Correctness |
| Poison and DLQ rate | Operational safety |
| Payload size at p95 versus broker or cache limits | Capacity |
| Schema evolution success | Longevity |
| Suite size and deserialize time | Cost among portable options |
Conclusion style: “The Redis blob is portable Protobuf; native cache encoding has been removed.”
What this suite cannot tell you
- Redis eviction policy and hot-key design.
- Exactly-once queue semantics.
- The correct TTL for your sessions.
Common mistakes
- “TTL is 60 seconds, so schema doesn’t matter.”
- Storing entire user graphs per key.
- Logging cache values that contain tokens.
Key takeaways
- Shared stores are interchange boundaries.
- Portable and versioned formats beat native speed on multi-service caches.
- Queues need poison handling and contracts. Throughput alone is not enough.
- The suite picks libraries after the store’s trust model is fixed.