У нас был большой продакшен-сервис с ~10M MAU, где Redis использовался как основное хранилище состояния пользователей. Все данные лежали в нём в виде JSON-сериализованных Pydantic-моделей. Это выглядело удобно, пока не стало больно.

На определённом этапе мы выросли до Redis Cluster из пяти нод — и он всё равно задыхался по памяти. JSON-объекты раздувались в разы относительно полезных данных, и мы платили за тонны пустоты — буквально деньгами и деградацией.
Я посчитал сколько весят реально полезные данные и получил цифру, от которой понял, что так жить больше нельзя:
14000 байт на пользователя в JSON → 2000 байт в бинарном формате
Семикратная разница. Только из-за формата.
Вот тогда я и написал решение, которое теперь превратилось в небольшую библиотеку: PyByntic — бинарный encoder/decoder для Pydantic-моделей. И ниже — история того, как я к этому пришел.
Почему JSON стал проблемой
JSON хорош как универсальная валюта обмена. Но в низкоуровневом кэше он — прожорливый монстр:
хранит ключи целиком
хранит типы косвенно (как строки)
дублирует структуру
не оптимизирован под бинарные данные
inflate на уровне RAM: данные 3–10× больше, чем должны быть
Когда у тебя десятки миллионов объектов в Redis — это не абстрактная неэффективность, это счёт на реальные деньги и лишние сервера в кластере.
Какие альтернативы я пробовал (и почему они не зашли)
Я честно проверил все очевидные варианты:
Формат | Проблема в нашем кейсе |
|---|---|
Protobuf | слишком замороченный, отдельные схемы, генерация, кодген, лишняя боль |
MessagePack | компактнее JSON, но недостаточно, и интеграция с Pydantic всё равно пляски |
BSON | размер лучше JSON, но интеграция с Pydantic всё равно не удобная |
Все эти форматы хорошие сами по себе. Но в точечном сценарии «Pydantic + Redis как state storage» они похожи на стрельбу из пушки по воробьям – сложно, шумно, а облегчения почти нет.
Мне нужно было решение, которое:
встраивается в текущий код за пару строк
даёт радикальный выигрыш по памяти
не требует отдельного DSL, схем или генерации кода
работает строго с Pydantic-моделями, не ломая экосистему
Что я сделал
Я написал минималистичный бинарный формат + encoder/decoder поверх аннотированных Pydantic-моделей. Так появилась библиотека PyByntic.
Её API специально сделан так, чтобы можно было просто заменить вызовы:
model.serialize() # вместо .json()
Model.deserialize(bytes) # вместо .parse_raw()Пример:
from pybyntic import AnnotatedBaseModel
from pybyntic.types import UInt32, String, Bool
from typing import Annotated
class User(AnnotatedBaseModel):
user_id: Annotated[int, UInt32]
username: Annotated[str, String]
is_active: Annotated[bool, Bool]
data = User(
user_id=123,
username="alice",
is_active=True,
)
raw = data.serialize()
obj = User.deserialize(raw)
Опционально можно задать функцию для дополнительной компрессии:
import zlib
serialized = user.serialize(encoder=zlib.compress)
deserialized_user = User.deserialize(serialized, decoder=zlib.decompress)Сравнение
Для сравнения я создал 2 миллиона записей пользователей на основе наших продакшн моделей. У пользователей есть различные поля: UInt16, UInt32, Int32, Int64, Bool, Float32, String, DateTime32. Так же в каждом пользователе есть вложенные объекты – роли и пермишены, в некоторых случаях пермишенов могут быть сотни.

На графике видно, сколько памяти в Redis занимают 2 000 000 пользовательских объектов при разных форматах сериализации. JSON взят за базу (≈35.1 GB). PyByntic оказался самым компактным – всего ~4.6 GB (13.3% от JSON), что в 7.5 раз меньше. Protobuf и MessagePack тоже заметно выигрывают у JSON, но по абсолютным значениям всё равно сильно уступают PyByntic.
Откуда такая экономия?
Основная экономия места достигается за счёт двух вещей: отсутствия текстового формата и устранения повторяющейся структуры. В JSON обычно хранятся как строки "1970-01-01T00:00:01.000000" — это 26 символов, а каждый ASCII-символ = 1 байт = 8 бит, то есть одна дата занимает 208 бит. В бинарном же виде DateTime32 — это всего 32 бита, что в 6.5 раза компактнее, без всякого форматирования и суффиксов. То же самое происходит и с числами: например, 18446744073709551615 (2^64−1) в JSON занимает 20 символов = 160 бит, а в бинарном представлении — ровно 64 бита. И, наконец, JSON повторяет имена полей для каждого объекта, снова и снова, тысячи и тысячи раз — в бинарной схеме структура известна заранее, поэтому хранить её в каждом экземпляре просто не нужно. Именно на этих трёх механиках и происходит основной выигрыш в размере.
Вывод
Если вы живёте в Pydantic и храните состояние в Redis, то JSON — это роскошь, за которую вы платите RAM-налог.
Бинарный формат, совместимый с существующими моделями — гораздо логичнее.
PyByntic для нас стал именно такой «логичной оптимизацией», которая ничего не ломает, но снимает целый пласт проблем.
GitHub репозиторий проекта
