Это пятая статья серии «Погружение в NATS». Предыдущие четыре описывали механизмы: Core NATS — subjects и at-most-once транспорт, JetStream — streams, retention и consumers, классический кластер — RAFT и репликация, гео-топологии — supercluster и mirror. В каждой из них надёжность и производительность упоминались по касательной, вместе с описанием механизма. Здесь — фокус только на них, потому что это не две отдельные темы, а одна: любое решение, повышающее надёжность, стоит производительности, и наоборот. Три реплики надёжнее одной — и медленнее на запись. sync publish даёт точную latency на сообщение — и на порядок ниже throughput, чем async. Slow consumer в Core может быть отброшен — и это цена, которую платит система в целом за то, чтобы не встать из-за одного медленного потребителя.
Разобраться в этом компромиссе — значит перестать спрашивать «а NATS надёжный?» и начать спрашивать «какая гарантия мне нужна и сколько она стоит здесь конкретно».
В статье
- Матрица гарантий
- Storage и репликация
- Перегрузка и slow consumer
- Производительность и tuning
- Измеряем: nats bench
- Вывод
Матрица гарантий
«Надёжность» — не одна шкала, а набор конкретных вопросов: что происходит, если подписчика нет в момент публикации; что происходит, если сервер падает после ack; может ли сообщение дойти дважды. Ответы у Core NATS и у JetStream разные, и путать их — источник сюрпризов в проде.
Core NATS — at-most-once. Сообщение доставляется максимум один раз, и это может означать ноль раз. Публикация в subject, на который в этот момент никто не подписан, — это сообщение, которое никогда не будет доставлено, без ошибки, без предупреждения, без возможности прочитать его позже. Это не баг и не недоработка — это вся модель Core целиком: сервер ничего не хранит, только маршрутизирует то, что происходит прямо сейчас (ст. 1). То же самое верно для медленного подписчика — если он не успевает вычитывать входящий поток, сервер отбросит сообщения этому подписчику, а не остановит публикацию для всех остальных (подробнее — дальше в этой статье).
JetStream — at-least-once, с exactly-once-семантикой через дедупликацию. Сообщение, записанное в stream и подтверждённое сервером, не потеряется в штатных сценариях — оно переживёт рестарт consumer’а (durable-позиция) и, при --replicas 3, падение одной ноды (ст. 3). Но at-least-once означает буквально «минимум один раз»: если consumer обработал сообщение, но не успел отправить ack до истечения AckWait, JetStream доставит его снова — это не баг дедупликации, это и есть контракт at-least-once (ст. 2). Полное «exactly-once» — не встроенное свойство сервера, а результат двух приёмов вместе: дедупликация по Nats-Msg-Id на публикации (защита от повторной записи при retry) плюс идемпотентная обработка на стороне consumer (защита от повторной обработки при redelivery). Ст. 2 разбирала это подробно — здесь важно, что для темы этой статьи это не техническая деталь, а прямое следствие модели гарантий: если пропустить любую из двух частей, exactly-once не работает, а at-least-once остаётся at-least-once.
graph TD
subgraph Core["Core NATS — at-most-once"]
C1["Нет подписчика при публикации"] --> C2["Сообщение потеряно, без ошибки"]
C3["Подписчик не успевает читать"] --> C4["Сервер дропает избыток — by design"]
end
subgraph JS["JetStream — at-least-once"]
J1["Сообщение записано и подтверждено"] --> J2["Переживает рестарт consumer'а"]
J3["Ack не пришёл до AckWait"] --> J4["Redelivery — то же сообщение придёт снова"]
J5["Nats-Msg-Id + Duplicate Window"] --> J6["Повторная запись отброшена"]
J4 -.->|"exactly-once = dedup + идемпотентный consumer"| J6
end
style Core fill:#f9f3e3,stroke:#8b7355
style JS fill:#c9e4c5,stroke:#5b8a5e
Мост: «надёжность» в NATS, Kafka и RabbitMQ — три разные модели
Это прямое продолжение темы из обзорной статьи о выборе брокера: там формулировка была «надёжность — не общий флажок в сравнительной таблице, а разные модели компромисса», и именно здесь эта фраза раскрывается на конкретных механизмах.
- NATS JetStream — at-least-once по умолчанию, exactly-once-семантика достигается парой «
Nats-Msg-Id+ окно дедупа на входе» и «идемпотентная обработка на выходе». Ничего не гарантируется автоматически без этой пары. - RabbitMQ — confirms (publisher confirms) на стороне отправителя гарантируют, что брокер принял сообщение; ack потребителя гарантирует, что оно не потеряется при падении consumer’а до обработки; транзакции (или, чаще на практике, publisher confirms как более дешёвая альтернатива) закрывают промежуток между «отправил» и «брокер принял». Модель ближе к явному двустороннему протоколу подтверждений на каждом шаге, чем к «настроил окно дедупа и живи».
- Kafka — идемпотентный producer (
enable.idempotence=true) убирает дубли на уровне партиции при retry отправки,acks=allтребует подтверждения от всех реплик из ISR перед тем, как публикация считается успешной, а полное exactly-once end-to-end строится через transactional producer API поверх этого. Опять другой набор ручек для той же итоговой цели.
Ни одна из трёх систем не даёт «exactly-once из коробки без усилий» — это распространённый миф про все три. Разница не в том, «какая надёжнее», а в том, из каких конкретно примитивов надёжность собирается и сколько это стоит операционно.
Storage и репликация
Две независимые оси, которые вместе определяют, что именно и в каком сценарии теряется.
File vs memory storage
Stream в JetStream создаётся с --storage file или --storage memory (ст. 2). Разница не только в скорости:
memory— сообщения живут в RAM процессаnats-server. Здесь важно разделить два разных сценария отказа. Падение одного узла в кластере с репликацией (--replicas 3) memory-stream переживает: оставшийся кворум держит данные и доступность, а вернувшийся узел до-реплицирует состояние из RAM живых реплик — как и у file-storage, RAFT-группа закрывает потерю одной ноды. А вот полный рестарт кластера — когда процессы теряются на всех репликах разом (docker compose down, OOM-kill всех нод, коррелированное отключение питания) — стирает stream безвозвратно: реплицируется тоже RAM-состояние, и если оно исчезло везде одновременно, восстанавливать неоткуда. Это осознанный выбор для данных, которым не нужна durability дольше времени жизни процессов кластера — кэш-подобные сценарии, временные буферы, где потеря при полном рестарте приемлема.file— сообщения пишутся на диск вstore_dir. Переживают рестарт процесса. Это выбор по умолчанию для всего, что должно хоть немного походить на надёжное хранилище.
sync_interval: то, что легко упустить
Здесь есть нюанс, который стоит знать перед тем, как полагаться на file-storage как на «данные гарантированно на диске после ack». Официальная документация JetStream прямо описывает: сервер не делает fsync на каждую запись — по умолчанию sync_interval равен 2 минутам, и данные физически сбрасываются на диск с этим интервалом, а не сразу после подтверждения клиенту. В штатной работе кластера это не проблема: JetStream «полагается на репликацию в кластере, чтобы данные остались доступны после падения ОС» — если одна нода потеряла несинхронизированные на диск данные при крэше, RAFT-группа восстановит её из реплик, у которых кворум эти данные подтвердил.
Проблема — в редком, но реальном классе отказов: коррелированный сбой сразу нескольких реплик (например, одновременная потеря питания на нескольких нодах в одном ЦОД). Независимый анализ отказоустойчивости Jepsen: NATS 2.12.1 (декабрь 2025, кластеры с фактором репликации 3 и 5) зафиксировал сценарии, где такой коррелированный сбой приводил к потере уже подтверждённых клиенту записей — потому что часть данных на нескольких нодах одновременно не успела дойти до диска в окне между fsync. Ответ Synadia (компания-мейнтейнер NATS) подтверждает механизм и формулирует рамку «долговечность против производительности»: строгая durability стоит throughput, и для нагрузок, которым нужны жёсткие гарантии, sync_interval стоит делать настолько малым, насколько практично, — вплоть до sync_interval: always, который форсирует fsync после каждой записи и полностью убирает это окно. Сам отчёт Jepsen приводит цену такого режима — throughput падает до «нескольких сотен сообщений в секунду» — и отдельно советует: для топологий без репликации или с фактором репликации 2 (там, где нет кворума-подстраховки) стоит укоротить sync_interval, а не полагаться на дефолт в 2 минуты.
jetstream {
store_dir: /data/jetstream
sync_interval: always # fsync на каждую запись — максимальная durability, минимальный throughput
}
Практический вывод для R3 в проде: дефолтный sync_interval: 2m — разумный компромисс при условии, что реплики физически независимы друг от друга по отказу (разные машины, в идеале разные зоны доступности) — тогда коррелированный сбой маловероятен, и повторное обращение к репликам гасит риск главного happy-path сценария. Если это условие не выполняется (все три реплики — контейнеры на одном хосте, как в демо-стенде этой статьи, или три ВМ в одной стойке с общим UPS), риск, который описывает Jepsen-отчёт, реальнее, чем кажется, — и sync_interval стоит сокращать явно, а не полагаться на репликацию как единственную линию защиты.
Влияние R1/R3 на latency записи
Ст. 3 разбирала механику: запись в stream с --replicas 3 считается подтверждённой (committed), только когда её видит большинство реплик — кворум ⌊N/2⌋ + 1, для R3 это 2 из 3. Практическое следствие для каждой отдельной публикации: клиент, ожидающий ack (sync publish), ждёт не только запись на диск локальной ноды, но и round-trip репликации до кворума через сеть кластера.
sequenceDiagram
participant Client
participant Leader as Leader (R1)
Client->>Leader: publish (sync)
Leader->>Leader: запись на диск / в RAM
Leader-->>Client: ack
Note over Client,Leader: Только локальный commit
participant L3 as Leader (R3)
participant F1 as Follower 1
participant F2 as Follower 2
Client->>L3: publish (sync)
L3->>F1: replicate
L3->>F2: replicate
F1-->>L3: ack
Note over L3,F1: Кворум 2 из 3 — committed
L3-->>Client: ack
Note over Client,L3: Round-trip до кворума, не только локальный диск
На демо-стенде этой статьи (nats/04-bench) разница измерена напрямую: синхронная публикация в R1-stream и в R3-stream на одном и том же кластерном железе (переменная — только число реплик) показала latency R3 примерно в 2.5–2.7 раза выше, чем R1. Асинхронная публикация (PublishAsync, конвейеризация запросов) снижает видимую клиенту среднюю задержку на сообщение за счёт перекрытия ожиданий, но не отменяет саму работу кворума на сервере — это разница между «клиент не блокируется построчно» и «репликация стала бесплатной». Подробнее — раздел «Измеряем: nats bench» и scenarios.md демо с точными числами конкретного прогона.
Перегрузка и slow consumer
Что происходит, когда потребитель не успевает за потоком, — принципиально разное поведение в Core и в JetStream, и разница здесь не случайная, а спроектированная.
Core: дроп by design
У каждого клиентского соединения есть внутренние pending limits — сколько сообщений и байт может накопиться в буфере на стороне клиента, ожидая обработки приложением (по умолчанию 65 536 сообщений и 65 536×1024 байт; настраивается через SetPendingLimits() в клиентских библиотеках). Если приложение не читает из подписки быстрее, чем приходят сообщения, буфер заполняется, и происходит одно из двух:
- клиент детектирует переполнение локально — избыточные сообщения дропаются, приложение получает асинхронный error callback («slow consumer»), но продолжает работать;
- сервер детектирует, что не успевает писать клиенту данные (сетевая перегрузка на его стороне) — соединение с медленным клиентом разрывается.
Официальная документация формулирует принцип прямо: NATS «предпочитает защищать систему в целом, а не подстраиваться под конкретного медленного потребителя, ради доставки сообщения». Это не баг и не повод для issue — это прямое следствие модели at-most-once: раз Core в принципе не гарантирует доставку, у него нет причины ставить publisher’ов или всю систему в очередь ради одного отстающего subscriber’а. Дроп сообщений медленному consumer’у — ровно то же поведение, что и дроп сообщения при отсутствии подписчика вообще, просто вызванное другой причиной.
JetStream: flow control и backpressure вместо дропа
JetStream не может позволить себе молча дропать данные — это противоречило бы at-least-once. Вместо этого перегрузка выражается через flow control, работающий по-разному для push и pull consumers, но с общей идеей: сервер не присылает данные быстрее, чем consumer успевает их подтверждать.
MaxAckPending — ключевой параметр: сколько сообщений может быть доставлено consumer’у без ack одновременно. Значение по умолчанию — 1000 (-1 отключает лимит полностью). Как только это число неподтверждённых доставок достигнуто, сервер приостанавливает доставку новых сообщений этому consumer’у, пока не придут подтверждения на часть уже отправленных. Это и есть backpressure: медленный consumer не теряет данные, он просто получает их медленнее — давление передаётся обратно на источник (сервер придерживает доставку), а не на потребителя (который бы захлебнулся).
Для push consumer’ов есть дополнительный механизм явного flow control — сервер отправляет служебное сообщение о достижении лимита pending и ждёт подтверждения от клиента, прежде чем возобновить поток; современные клиентские библиотеки обрабатывают это прозрачно. Для pull consumer’ов backpressure устроена нагляднее: клиент сам явно запрашивает батч (fetch), то есть управляет темпом по построению — не может «захлебнуться» потоком, которого сам не запрашивал. Это одна из причин, по которой ст. 2 называла pull режимом по умолчанию для новых проектов: явный контроль над темпом — не побочный эффект, а прямое architectural преимущество для управления нагрузкой.
error callback slow consumer"] CC --> CD["Publisher продолжает публиковать —
system protected, not consumer"] end subgraph JSFlow["JetStream"] JA["Неподтверждённых доставок = MaxAckPending"] --> JB["Сервер приостанавливает доставку"] JB --> JC["Consumer подтверждает часть — ack"] JC --> JD["Доставка возобновляется"] JD -.-> JA end style CoreFlow fill:#f9f3e3,stroke:#8b7355 style JSFlow fill:#c9e4c5,stroke:#5b8a5e
graph TD
subgraph CoreFlow["Core NATS"]
CA["Consumer не успевает читать"] --> CB["Буфер клиента переполнен"]
CB --> CC["Сообщения дропаются,
error callback slow consumer"]
CC --> CD["Publisher продолжает публиковать —
system protected, not consumer"]
end
subgraph JSFlow["JetStream"]
JA["Неподтверждённых доставок = MaxAckPending"] --> JB["Сервер приостанавливает доставку"]
JB --> JC["Consumer подтверждает часть — ack"]
JC --> JD["Доставка возобновляется"]
JD -.-> JA
end
style CoreFlow fill:#f9f3e3,stroke:#8b7355
style JSFlow fill:#c9e4c5,stroke:#5b8a5e
Практический вывод: если в Core-приложении логи показывают slow consumer — это сигнал, что consumer нужно ускорять или масштабировать (queue group, ст. 1), а не баг сервера. В JetStream аналогичный симптом выглядит иначе: MaxAckPending придерживает доставку конкретному отстающему consumer’у, пока тот не подтвердит часть сообщений, — сам по себе на publish-throughput этот лимит не влияет и publisher напрямую не тормозит. Поэтому MaxAckPending стоит проверять там, где он и проявляется: доставка конкретному consumer’у встала, а его lag растёт. На публикацию он влияет лишь косвенно — если из-за отстающего consumer’а вместе с retention-лимитами stream упирается в предел хранения (discard: new или лимит по размеру/возрасту) и уже сам stream начинает отклонять запись. Так что проседание именно publish-throughput ищут не в MaxAckPending, а в лимитах и retention-политике stream’а.
Производительность и tuning
Порядки величин: Core vs JetStream
Ключевая интуиция, которую стоит вынести из этой статьи: разница между Core и JetStream по throughput и latency — не «немного медленнее», а на порядки. Core публикует в сотни тысяч сообщений в секунду с одного процесса при задержке в единицы микросекунд — потому что не делает почти ничего, кроме маршрутизации в памяти. JetStream добавляет per-сообщение состояние на сервере, запись на диск (или в RAM, но с репликацией состояния) и, при --replicas > 1, сетевой round-trip до кворума на каждую подтверждённую запись — платит за durability пропускной способностью. Порядок разницы на демо-стенде этой статьи — от сотен тысяч msgs/sec у Core до единиц тысяч msgs/sec у JetStream sync publish; точные числа и методика — в разделе «Измеряем: nats bench».
Это не повод считать JetStream «медленным» — это ожидаемая, спроектированная цена конкретной гарантии. Вопрос не «что быстрее», а «какая гарантия нужна этому конкретному потоку данных», и дальше выбирается соответствующий инструмент (вплоть до смешанной архитектуры: часть subjects — Core, часть — JetStream, в рамках одного кластера, как обсуждалось в ст. 1–2).
Кардинальность subjects
Число уникальных subjects, на которые фактически идёт трафик, — не бесплатная переменная. У Core рост числа subjects увеличивает объём interest-таблиц, которые сервер поддерживает для маршрутизации (кто на что подписан) — на очень большом количестве уникальных subjects (например, subject с id пользователя или заказа в каждом сообщении вместо wildcard-паттерна) это сказывается на памяти и на скорости построения маршрута. У JetStream добавляется ещё один эффект: MaxMsgsPerSubject и любая логика, завязанная на подсчёт состояния по каждому subject внутри stream, масштабируется от числа уникальных subjects, а не только от общего числа сообщений. Практическое правило то же, что в большинстве систем с subject/topic-моделью: используйте иерархию с wildcard-подписками (orders.>) там, где нужна фильтрация, вместо генерации уникального subject на каждую бизнес-сущность — это разница между «сервер держит один паттерн подписки» и «сервер держит миллион подписок по одной на сущность».
Батчинг publish и PublishAsync
Разница между sync и async publish в клиентских библиотеках (js.Publish() vs js.PublishAsync() в nats.go, аналоги в других языках) — это ровно та же дилемма latency-vs-throughput, что и на уровне replicas, только на уровне клиентского кода:
- sync publish — вызов блокируется до получения ack от сервера. Даёт точную latency на конкретное сообщение, но throughput ограничен round-trip time: пока не пришёл ack на сообщение N, сообщение N+1 не отправлено.
- async publish (
PublishAsync) — вызов возвращает управление сразу, подтверждения приходят батчами через callback/future. Throughput ограничен уже не round-trip на сообщение, а пропускной способностью канала и скоростью обработки подтверждений — на порядок выше, потому что запросы конвейеризируются, а не сериализуются один за другим.
Практическое правило: sync — для операций, где нужна гарантия перед следующим шагом бизнес-логики (например, «подтверди списание, прежде чем показать пользователю успех»); async — для потоковой загрузки данных, где важен агрегатный throughput, а не задержка одного конкретного сообщения (миграция данных, bulk-ingest, телеметрия). Смешивать — нормально в рамках одного сервиса: критичные по консистентности операции синхронно, фоновая заливка асинхронно.
Sizing: CPU / RAM / диск / сеть
Грубые ориентиры, требующие проверки на своей нагрузке (nats bench, раздел ниже) — не готовые цифры для копирования в спеку:
- CPU — Core почти не требует CPU на сообщение (маршрутизация в памяти); JetStream добавляет сериализацию метаданных, работу RAFT (для R>1) и, если включено, сжатие — на потоках с высоким msgs/sec CPU на JetStream-ноде стоит мониторить отдельно от сети.
- RAM —
max_mem/max_memory_storeрезервирует верхнюю границу подmemory-streams и служебное состояние JetStream (метаданные, буферы соединений); RAM не резиновая, лимит превышения — жёсткий отказ записи, не деградация. - Диск —
max_file/max_file_store, плюс сама скорость диска:sync_interval: alwaysрезко чувствителен к IOPS диска (см. раздел про storage выше) — на медленном диске он может опустить throughput до нескольких сотен сообщений в секунду, на NVMe — заметно выше; это первое, что стоит проверить перед тем, как менятьsync_intervalв проде. - Сеть — для R3/R5 каждая публикация — это не один пакет, а
N-1реплицирующих сообщений внутри кластера дополнительно к клиентскому трафику; при планировании пропускной способности между нодами кластера стоит закладывать это умножение, а не только клиентский входящий поток.
Измеряем: nats bench
Все числа выше в этой статье — не абстракция, а результат nats bench на demo-стенде nats/04-bench в digital-cookbook, который переиспользует уже знакомые по серии стенды: 01-jetstream для Core и JetStream R1, 02-cluster для R3.
Важная оговорка, повторю прямо здесь: все числа в этой статье и в демо — иллюстрация порядка величин и соотношения R1/R3 на конкретном demo-железе автора (один ноутбук, все ноды кластера — контейнеры одной машины), а не бенчмарк и не обещание производительности. На отдельных физических нодах с честной сетью между ними абсолютные цифры будут другими — вероятно, выше и стабильнее. Единственный способ узнать реальные числа для вашей нагрузки — прогнать nats bench на своей инфраструктуре с вашим размером сообщений и вашей топологией.
Синтаксис (актуален для natscli с деревом подкоманд bench)
Начиная с некоторой версии natscli команда nats bench перестала быть одной командой с флагами --pub/--sub/--js и стала деревом подкоманд — если вы видели старый синтаксис в чужих примерах, он не будет работать на актуальной версии CLI. Проверено на nats CLI v0.3.0 (совместимо с nats-server 2.12.x):
# Core: publish без подписчика
nats --server localhost:4222 bench pub foo --clients 1 --msgs 100000 --size 128 --no-progress
# Core: publish + subscribe (полный путь доставки)
nats --server localhost:4222 bench sub foo --clients 1 --msgs 100000 --no-progress &
nats --server localhost:4222 bench pub foo --clients 1 --msgs 100000 --size 128 --no-progress
# JetStream: синхронный publish, создать stream с нужным storage/replicas
nats --server localhost:4222 bench js pub sync foo --create --replicas 3 --storage file --msgs 5000 --size 128 --purge --no-progress
# JetStream: асинхронный publish (throughput-режим)
nats --server localhost:4222 bench js pub async foo --replicas 3 --storage file --msgs 50000 --size 128 --purge --no-progress
# Pull-consumer: durable consumer нужно создать заранее (nats consumer add), затем
nats --server localhost:4222 bench js fetch --stream STREAM --consumer NAME --clients 1 --msgs 20000 --acks explicit --no-progress
--create внутри bench js pub sync|async создаёт (или обновляет) stream с заданными --storage/--replicas/--maxbytes — не нужно создавать его отдельной командой nats stream add перед первым запуском. --purge перед прогоном чистит stream от предыдущих данных, чтобы не искажать счётчики. Флаг --persistasync (bench js pub sync|async) переключает persistence stream’а в асинхронный режим — но только для R1: это отдельная ручка от sync_interval уровня сервера, специфичная именно для бенчмарк-стрима, и её тоже стоит знать, если сравниваете «максимально быстрый R1» с «максимально надёжный R1».
Три сценария демо
- Core pub/sub throughput — верхняя граница механической скорости без persistence.
- JetStream publish R1 vs R3 — тот же subject-размер и объём сообщений, единственная переменная — число реплик; sync и async отдельно.
- Pull-consumer (fetch) — скорость вычитывания уже подтверждённого backlog, отдельная от publish переменная.
Полные команды, точные флаги и таблицы с фактическими числами конкретного прогона — в scenarios.md демо; там же — раздел «как мерить под свою нагрузку» с чек-листом (варьировать --size, --clients, гонять sync/async отдельно, мерить на целевой топологии реплик, не экстраполировать R1→R3 руками).
Вывод
Надёжность и производительность в NATS — не два независимых свойства, которые можно максимизировать одновременно, а одна ось компромисса с понятной механикой на каждом шаге. Core NATS быстр именно потому, что ничего не хранит и ничего не гарантирует после отправки — slow consumer в нём дропается не по недосмотру, а by design, потому что защита системы в целом важнее одного отстающего потребителя. JetStream меняет это уравнение — at-least-once, durable state, redelivery — и платит за это throughput’ом: запись на диск, RAFT-кворум при репликации, flow control вместо дропа при перегрузке consumer’а. Три реплики стоят latency записи примерно в 2.5–3 раза выше одной — это не накладные расходы плохой реализации, а физика консенсуса: подтверждение ждёт кворум, а не одну ноду.
Практический вывод простой и он же — три тезиса, с которых стоило бы начинать: R3 стоит latency, и это осознанная цена, а не баг; slow consumer в Core — дроп by design, и решать его нужно на стороне consumer’а (масштабирование, queue groups), а не ждать, что сервер сам подстроится; не берите числа производительности из статьи или из чужого блога — меряйте nats bench на своей топологии и под свою нагрузку, потому что абсолютные цифры зависят от железа, сети и конфигурации сильнее, чем от версии сервера.
Дальше в серии — практика: клиентские библиотеки на конкретных языках, где вся эта теория превращается в код, который принимает решения sync/async, R1/R3 и retry на каждый вызов.
Документация
- NATS docs
- Slow Consumers
- JetStream Model Deep Dive
- Data Replication (JetStream admin)
- Streams
- nats bench (NATS CLI)
- Jepsen: NATS 2.12.1
- Synadia: Jepsen NATS 2.12.1 — ответ мейнтейнера
- Обзорная статья: выбор брокера
- Первая статья серии: Core NATS
- Вторая статья серии: JetStream
- Третья статья серии: классический кластер
- Четвёртая статья серии: гео-топологии
Комментарии