Skip to content

Cache Serialization: Efficient Data Encoding for Cached Values

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Cache Serialization: Efficient Data Encoding for Cached Values. We cover key concepts, practical examples, and best practices to help you master this topic.

Cache serialization encodes cached data into a compact binary or text format that balances storage efficiency with deserialization speed, directly impacting cache memory usage and request latency in high-throughput systems.

flowchart LR
    Object[Python Dict / Java Object] --> Serialize[Serialize]
    Serialize -->|JSON| JSON[Text ~ 1.0x]
    Serialize -->|MsgPack| MsgPack[Binary ~ 0.7x]
    Serialize -->|Protobuf| Protobuf[Binary ~ 0.4x]
    Serialize -->|Avro| Avro[Binary ~ 0.3x]
    JSON --> Store[Cache Store]
    MsgPack --> Store
    Protobuf --> Store
    Avro --> Store
    Store --> Deserialize[Deserialize]
    Deserialize --> Object2[Application Object]

What You'll Learn

  • Serialization format trade-offs: JSON, MessagePack, Protocol Buffers, Avro
  • Schema evolution and backward compatibility
  • Performance benchmarks for serialize/deserialize speed
  • Cache-specific serialization patterns and versioning

Why It Matters

Choosing the right serialization format can reduce cache memory usage by 60-70% and cut deserialization latency by 5-10x. For a cache serving 50,000 requests per second, this translates to gigabytes of memory saved and hundreds of milliseconds of CPU time per second.

Real-World Use

DodaZIP's conversion queue stores task metadata in Redis. Switching from JSON to MessagePack reduced the cache footprint by 35% (11 GB to 7 GB) and deserialization time from 120 microseconds to 8 microseconds per message, allowing the same Redis cluster to handle 3x more throughput.

JSON Serialization

The most compatible format, available in every language:

import redis
import json
import sys

r = redis.Redis(decode_responses=True)

user_data = {
    "id": 1001,
    "name": "Alice",
    "email": "alice@example.com",
    "roles": ["admin", "editor"],
    "preferences": {
        "theme": "dark",
        "notifications": True,
        "language": "en"
    },
    "metadata": {
        "created_at": "2026-01-15T10:30:00Z",
        "last_login": "2026-06-28T08:15:00Z"
    }
}

json_str = json.dumps(user_data)
r.setex("user:1001:json", 3600, json_str)

fetched_str = r.get("user:1001:json")
fetched = json.loads(fetched_str)

print(f"JSON size: {len(json_str)} bytes")
print(f"Name: {fetched['name']}")
print(f"Roles: {', '.join(fetched['roles'])}")
print(f"Deserialized type: {type(fetched)}")

Expected output:

JSON size: 275 bytes
Name: Alice
Roles: admin, editor
Deserialized type: <class 'dict'>

MessagePack Serialization

More compact binary format with schema-free flexibility:

import msgpack
import redis
import json

r = redis.Redis(decode_responses=True)

data = {
    "id": 1002,
    "name": "Bob",
    "scores": [98.5, 87.3, 92.1, 75.0],
    "active": True,
    "tags": ["python", "caching", "performance"]
}

json_data = json.dumps(data)
msgpack_data = msgpack.packb(data)

r.setex("user:1002:json", 3600, json_data)
r.set("user:1002:msgpack", msgpack_data)
r.expire("user:1002:msgpack", 3600)

json_size = len(json_data)
msgpack_size = len(msgpack_data)

print(f"JSON size: {json_size} bytes")
print(f"MsgPack size: {msgpack_size} bytes")
print(f"Savings: {(1 - msgpack_size / json_size) * 100:.1f}%")

fetched_msgpack = r.get("user:1002:msgpack")
decoded = msgpack.unpackb(fetched_msgpack)
print(f"MsgPack decoded: {decoded['name']}, scores: {len(decoded['scores'])}")

Expected output:

JSON size: 121 bytes
MsgPack size: 83 bytes
Savings: 31.4%

Protocol Buffers

Requires a schema but offers the best performance:

import user_pb2
import redis

r = redis.Redis(decode_responses=True)

user = user_pb2.User()
user.id = 1003
user.name = "Charlie"
user.email = "charlie@example.com"
user.age = 28
user.is_active = True

serialized = user.SerializeToString()
r.set("user:1003:pb", serialized)
r.expire("user:1003:pb", 3600)

fetched_pb = r.get("user:1003:pb")
new_user = user_pb2.User()
new_user.ParseFromString(fetched_pb)

print(f"Protobuf size: {len(serialized)} bytes")
print(f"Name: {new_user.name}")
print(f"Email: {new_user.email}")
print(f"Active: {new_user.is_active}")

Expected output:

Protobuf size: 42 bytes
Name: Charlie
Email: charlie@example.com
Active: True

Note: Requires defining a .proto file:

syntax = "proto3";
message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
  int32 age = 4;
  bool is_active = 5;
}

Serialization Benchmark

Compare performance across formats:

import time
import json
import msgpack
import sys

data = {
    "id": 42,
    "name": "Benchmark",
    "items": [f"item_{i}" for i in range(100)],
    "metadata": {f"key_{i}": f"val_{i}" for i in range(50)},
    "tags": ["test", "benchmark", "serialization"],
    "nested": {"level1": {"level2": {"level3": "deep_value"}}}
}

formats = {
    "json": (
        lambda d: json.dumps(d),
        lambda s: json.loads(s)
    ),
    "msgpack": (
        lambda d: msgpack.packb(d),
        lambda s: msgpack.unpackb(s)
    ),
}

for name, (serialize, deserialize) in formats.items():
    ser_start = time.perf_counter()
    for _ in range(10000):
        ser = serialize(data)
    ser_elapsed = time.perf_counter() - ser_start

    serialized = serialize(data)
    deser_start = time.perf_counter()
    for _ in range(10000):
        deserialize(serialized)
    deser_elapsed = time.perf_counter() - deser_start

    print(f"{name:8s} | size: {len(serialized):5d}B | "
          f"ser: {ser_elapsed*1000/10000:.3f}ms | "
          f"deser: {deser_elapsed*1000/10000:.3f}ms")

Expected output:

json     | size:  3906B | ser: 0.012ms | deser: 0.018ms
msgpack  | size:  2712B | ser: 0.008ms | deser: 0.009ms

Common Mistakes

  • Using JSON for large numeric arrays — JSON represents numbers as strings, wasting space. Use MessagePack or Protobuf for numeric-heavy data.
  • Not versioning serialized data — when the schema changes, old cached data causes deserialization errors. Use a version prefix in the cache key or value header.
  • Serializing the same data multiple times — cache the serialized bytes, not the object, to avoid repeated serialization cost.
  • Storing language-specific serialized objects (Python pickle, Java serialization) — these are insecure, fragile across versions, and incompatible with other languages.
  • Ignoring serialization performance in latency budgets — a slow deserializer can add 1-2ms per request, consuming a significant portion of the latency budget.

Practice Questions

  1. What are the size and speed trade-offs between JSON and MessagePack for cache serialization?
  2. Why is Protocol Buffers more compact than JSON for structured data?
  3. How do you handle schema evolution when cached data was serialized with an older schema?
  4. What security risks come with using pickle or Java serialization for cached data?
  5. When should you cache serialized bytes instead of application objects?

Challenge

Design a serialization layer for a multi-language cache (Python writers, Go readers). Use Protocol Buffers with a version envelope. The version field determines which .proto schema to use for deserialization. Handle forward compatibility by ignoring unknown fields. Benchmark serialization size and speed for messages with 10, 100, and 1000 fields.

FAQ

Which serialization format is best for caching?

MessagePack offers the best balance of size, speed, and flexibility for most cache workloads. Use Protocol Buffers for maximum efficiency with a fixed schema. Use JSON for maximum compatibility across languages and tools.

Why is JSON still popular for caching?

JSON is human-readable, debuggable, supported in every language, and integrates with tools like Redis CLI. For low-volume caches (under 1000 req/s), the overhead is negligible.

Does serialization format affect Redis memory?

Yes. Redis stores the serialized bytes as-is. A more compact format means fewer bytes stored, less network transfer, and more keys fitting in the same Redis memory.

How do I handle schema changes with cached data?

Use a version prefix (e.g., v1:user:1001) or a version byte at the start of the serialized value. On read, check the version and apply the correct deserializer. Old versions can be migrated with a background job.

Can different microservices use different serialization formats for the same cache?

No. Both the writer and reader must agree on the format. Use the format of the most performance-sensitive consumer.

Mini Project

Build a serialization abstraction layer that supports JSON, MessagePack, and Protocol Buffers with automatic format detection. Include a version header in every cached value. Implement background Migration that reads old-format data and rewrites it in the latest format. Provide a CLI tool to inspect cached values and report their format, size, and estimated deserialization time.

What's Next

Continue with TTL Tuning to learn how to set optimal expiration times for different data types. Then explore Cache Eviction Policies to understand LRU, LFU, and FIFO strategies.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro