NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
Go modules · #1306 by repository stars
Last release 3 days ago
05 Oct 2026
Ships on a steady schedule
a new release about every 9 days
Some releases are documented
notes for 35 of the last 60 stable releases
Nothing withdrawn
no release was ever pulled
9 years old
1153 releases · first in 2018
Nothing published for this version
Nothing published for this version
Nothing published for this version
One column per quarter.
Nothing published for this version
Nothing published for this version
Fix flaky TestPresenceTickDefaultIsAllocationFree by @FZambia in #632
Full Changelog: v0.39.2...v0.39.3
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Reject expired client-side subscription refresh by @FZambia in #627
Full Changelog: v0.39.1...v0.39.2
Nothing published for this version
Nothing published for this version
metrics: add transport_frame_size histogram by @FZambia in #620
transport_frame_size histogram by @FZambia in #620client closed or unsubscribed after adding subscription log entry level to be info instead of error by @FZambia in #621Full Changelog: v0.39.0...v0.39.1
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
…decoder ( #612 , protocol #40 / #41 ). This is a breaking change to the exported HandleReadFrame helper.
This is one of the largest Centrifuge releases to date — 57 merged PRs since v0.38.0. The headline feature is two new subscription types: Map subscriptions and Shared Poll subscriptions. Alongside them, a deep investigation into Redis Sentinel failover reliability produced fixes both in Centrifuge and upstream in the rueidis driver, and a major round of connection runtime stability and performance work landed as well.
Centrifuge gets two new subscription types addressing patterns that don't fit well into the existing stream-based model, #565. Both are experimental: the API may still change based on real-world feedback. Today, only centrifuge-js implements client-side support for them — other official SDKs (Go, Swift, Java/Kotlin, Dart) don't have Map/Shared Poll support yet:
Ephemeral (PUB/SUB only, no recovery stream, entries expire via TTL), Recoverable (stream-backed with offset-based recovery, entries still expire via TTL), and Persistent (stream-backed with offset-based recovery, entries live forever until explicitly removed). Both the new MemoryMapBroker and RedisMapBroker support all three modes — the difference between them is durability: Memory keeps everything in-process, so even Persistent-mode data is gone after a restart, while Redis makes state and recovery survive restarts and work across multiple nodes. Delta compression works per key.track/untrack individual keys. Backend load scales as O(active items) instead of O(active clients) — 10k users watching 100 objects means one backend poll per cycle instead of 10k. Versionless and Versioned delivery modes, plus fast-track publish for immediate updates outside the poll cycle.Both integrate with the existing channel/Hub/transport model, and centrifuge-js (v5.6.0+) ships type-safe MapSubscription and SharedPollSubscription APIs on the client side.
This repo itself has two new runnable examples that use the library directly, with no Centrifugo layer in between: _examples/map_demo (collaborative cursors, a game lobby, inventory management, a stock ticker, a live scoreboard, and a protocol visualizer, over the in-memory or Redis map broker) and _examples/shared_poll (a feature-flags demo with HMAC-signed per-key track authorization). _examples/map_presence_massive pushes map presence to 100k and 1M synchronized members in a single channel to show where the protocol's limits actually are.
Centrifugo, the server built on top of Centrifuge, showcases both features end to end at the product level, including a PostgreSQL-backed map broker (an application of Centrifuge's MapBroker interface, living in Centrifugo) with transactional publishing from your own database writes. Useful as extra documentation and as a demonstration of what the new primitives make possible:
centrifuge-js):
map_cursors — real-time multi-user cursors over the Redis map brokermap_demo — a collection of map subscription scenarios, including a sprint board backed by PostgreSQL transactionsshared_poll_demo/votes — live vote results with dynamic per-key trackingshared_poll_demo/drones — a 500-drone geospatial tracker using cell-based spatial partitioningThis work started from centrifugal/centrifugo#1189: a report that Redis Sentinel pods could go permanently silent after a primary failover under load. WebSocket connections stayed open, publishes kept returning success, and /health stayed green — but nothing was actually being delivered to subscribers. Investigating it turned up several real problems, fixed both in Centrifuge itself and upstream in the rueidis driver it depends on.
Upstream, in rueidis (Centrifuge now pulls v1.0.77, up from v1.0.68 at v0.38.0), five fixes worth knowing about if you run Sentinel:
ErrClosing, without even trying to dial a working node.nil error), so their retry loops stopped instead of trying again. Combined with #1015 above, this is exactly how an ordinary few-second failover could turn into a permanent outage: the retry loop that should have replaced the closed connection was told everything was fine and gave up.Close() could hang forever on a connection that had silently stalled. In the Sentinel client, Close() was called while holding a lock, so this hang blocked all further topology handling on that client.TopologyRefreshInterval, giving Sentinel a periodic backup path to notice a failover instead of relying solely on the +switch-master PUB/SUB event. Centrifuge now turns this on internally for every Sentinel-backed RedisShard, with a fixed 5-second interval — no config change needed (#611).In Centrifuge itself:
/health still reports everything is fine — exactly the symptom in #1189. Centrifuge now periodically sends a probe message on each PUB/SUB connection and checks it comes back; if it doesn't, the connection is torn down and rebuilt. This works independently of the rueidis-level fixes above.SSUBSCRIBE panic on Redis Cluster when a PUB/SUB connection reconnects with live subscriptions already in place — fixed as part of the subscription-handling refactor in #565 (reported in #613, thanks to the detailed report).NumShardedPubSubPartitions > 0) with many partitions, look at the new opt-in RedisBrokerConfig.UsePrecomputedPartitionTags (#570, reported in #554): the hash tag used to pick a partition's connection was a bare index ({0}, {1}, ...), and CRC16 collisions between those short tags can cause severe load skew on larger clusters — at 16 shards, roughly half the nodes saw zero PUB/SUB traffic. The option switches to a precomputed tag table that spreads evenly; the default is unchanged, so existing deployments aren't affected unless you turn it on.If you're running Redis Cluster or Sentinel, this release is worth prioritizing.
#590 is the largest single PR in this release — a full audit of the subscribe/unsubscribe/close lifecycle that started as a presence-tick performance pass. Highlights:
Hub.NumClients and Hub.NumSubscriptions used to be O(n) scans taken under the hub's read lock, so a Prometheus scrape on a large node both burned CPU and contended with the broadcast path. They're now O(shard-count) counters — measured ~137x faster for NumClients and ~495x faster for NumSubscriptions at 5k connections / 20k subscriptions, and the gap widens with node size.c.channels could disagree — previously, a stalled or racing subscribe could tear down a subscription that had just succeeded, leak a hub entry, leak presence until TTL expiry, or drift the connections_inflight/subscriptions_inflight metrics. Every reservation is now identity-matched by generation, which closes this entire category of bugs.-race, including a deadlock between a recovering subscribe's close path and broadcast, and a double-write to Publication.Offset in MemoryBroker.Publish._examples/stress_runtime, which drives real centrifuge-go clients and raw protocol clients against a multi-node memory + Redis cluster through 16 adversarial scenarios and asserts full state convergence afterwards.Two related fixes from separate PRs: reordering disconnect to close the transport before the synchronous per-channel presence/leave cleanup, so a slow Redis round trip no longer keeps a dead socket open — in one reproduction, peak concurrent sockets during a reconnect storm dropped from ~13 to ~2 (#595); and a closer audit of the recovery and channel-medium code that turned up three more bugs, each with its own regression test: an index-out-of-range in recovery's publication merge when a buffered window contained only filtered publications, a goroutine leak in channelMedium teardown present on any server using queued/delayed medium options, and a join/leave wire-ordering race where a client that disconnected very quickly after subscribing could have its leave event reach observers before its join (#574).
HandleReadFrame helper.SubscribeOptions.AutoCacheRecover lets the server initiate cache recovery for a subscription even when the client has no way to ask for it itself — needed for server-side subscriptions of unidirectional clients (SSE, HTTP-streaming, unidirectional WebSocket), which only send a token and have no protocol-level way to attach recover: true (#581, #582).map[string]string to a connection at connect time for Prometheus metric segmentation and targeted Node.{Subscribe,Unsubscribe,Disconnect,Refresh} operations via LabelFilter, scoped across all users of a label value (#539, #577).WebsocketConfig.ProcessCommandsOffReadLoop — a WebSocket read loop processes each command inline (handler, broker, broadcast fan-out), so its goroutine stack grows to fit the deepest call it ever makes — around 8KB — and never shrinks back, even once the connection goes idle for the rest of its life. With this option on, each frame is handed to a short-lived goroutine instead, so the deep stack returns to Go's runtime pool and gets reused by other connections. Trades a bit of latency for meaningfully less memory per idle connection at scale (#607). See Tuning Centrifugo PRO for large number of idle WebSocket connections for measured numbers on real workloads: this option alone took idle-connection goroutine stacks from ~12.3KB to ~8.2KB per connection, and combined with four other Centrifugo PRO tuning options, it let the same 16-core node hold 1,000,000 idle WebSocket connections using ~2.3 CPU cores and ~11GB of memory — versus needing well over the machine's RAM at the default configuration._examples/native_histograms_otel showing the metrics → OTel bridge pipeline), plus a dedicated map_broker metrics subsystem and clearer broker metric naming (#583).MemoryBroker already handled it (#549), and an HTTP/2 WebSocket stream failure caused by a stale write deadline left behind after a frame ping (cf_ws_frame_ping_pong=true only) (#588).Node.Shutdown no longer leaves the metrics-aggregator goroutine running after shutdown — a one-goroutine-per-Shutdown leak that only mattered for apps that create and tear down many Node instances in one process, such as tests (ed306c6).RedisShardConfig.AuthCredentialsFn lets you supply Redis auth credentials dynamically per connection attempt, instead of a fixed password — needed for managed Redis/Valkey setups that use short-lived, rotating tokens, such as GCP or AWS IAM authentication (#560, thanks to @kf-ajaib).Unlock() on an early-return path in the writer that could deadlock it (#552, thanks to @palkan).This is a Centrifuge (Go library) changelog, so none of this shipped as part of v0.39.0 itself — but Centrifugal Labs has also been closing capability gaps across the official client SDKs, so a feature isn't limited to whichever platform it launched on first:
getState on stream subscriptions is now supported by every official SDK — JavaScript, Go, Swift, Java/Kotlin, Dart, Python, and C#. It's an option where the SDK asks your application for its current state and the stream position that state corresponds to, right before subscribing — and again, later, if recovery ever turns out to be impossible, so a subscription is never left running on top of stale state. This replaces a fair amount of manual re-sync code applications used to write by hand for exactly this problem. See App-owned state with stream subscriptions for the full pattern, with a Kafka aggregator and a multi-tenant kitchen-orders example worked through end to end.github.com/centrifugal/protocol updated to v0.22.1centrifuge takes a decoder/encoder from this package for every WebSocket frame, so its perf work lands directly on Centrifuge's hot path. Two rounds of decode/encode optimization, benchmarked on Apple M4 against unmodified code:
| Path | Before | After | Change |
|---|---|---|---|
| Stream JSON decode, 1 cmd/frame (#41) | 525 ns/op, 4642 B, 9 allocs | 199 ns/op, 344 B, 5 allocs | −62% time, −93% memory |
| Stream JSON decode, 8 cmds/frame (#41) | 1.75 µs/op, 7.10 KiB | 1.31 µs/op, 2.36 KiB | −25% time, −67% memory |
| Whole-frame Protobuf encode, 64 cmds (#36) | 5927–6084 ns/op, 40960 B | 2783–2804 ns/op, 18432 B | ~2.1x faster, −55% memory |
| Stream Protobuf decode (#41) | 131.0 ns/op | 121.6 ns/op | −7% |
#41 also closed a gap where a command's size limit was only checked on some code paths, letting an oversized command slip through at up to roughly 2x the configured limit in a multi-command frame — now enforced consistently and paired with the mandatory-limit change described under Security above. Other changes pulled in along the way (v0.17.0 → v0.22.0): new fields to support Map/Shared Poll subscriptions and server-side tag filtering (#33, #35), and a DeflateFrameCodec.Decompress fix for silent truncation at maxSize=math.MaxInt (#48).
For the vast majority of setups — using the built-in RedisBroker/MemoryBroker as-is, standard transports — this should be a smooth, drop-in upgrade: bump the dependency, go build, done. Two things require actual code changes, and both are narrow:
Broker implementation. Broker.Publish now returns (PublishResult, error) instead of (StreamPosition, bool, error) — PublishResult carries the suppression info (Suppressed, SuppressReason) that Map/Shared Poll publish semantics need. PublishResult.FromCache was removed. Broker.Subscribe/Unsubscribe now take a variadic ...string instead of a single channel, to support batching. If you only use RedisBroker/MemoryBroker through Node.SetBroker, you're unaffected — Centrifuge calls these methods internally.HandleReadFrame helper directly (only relevant if you've implemented a custom transport on top of centrifuge.Client). It gained a mandatory messageSizeLimit int64 parameter — see Security above for why.A few behavioral changes ship without any API change, worth a quick read even though no code changes are needed:
WebsocketConfig.DecompressedMessageSizeLimit explicitly, decompressed messages are now capped at 10x MessageSizeLimit by default (previously unbounded) — set the field explicitly if you legitimately expect higher compression ratios (#584).Client.Subscribe/Node.Subscribe can now return ErrorAlreadySubscribed for a channel that's already subscribed (and, new, while a map subscribe is still loading that same channel). If you call it idempotently, treat this error as success (#590).OnUnsubscribe now fires after the transport is closed rather than before (matching OnDisconnect, which already did) — only matters if a handler assumed it could still write to the disconnecting client from OnUnsubscribe (#590, #595).Everything else — Map/Shared Poll subscriptions, client labels, native histograms, ProcessCommandsOffReadLoop, the Sentinel/Redis reliability work — is either opt-in or purely internal, so it applies automatically without touching your config.
Centrifuge was originally built around a single subscription type — stream subscriptions — and SubscribeOptions and the client protocol grew around that one assumption. Now that Map and Shared Poll subscriptions exist too, SubscribeOptions mixes fields and events that only make sense for one subscription type with fields that make sense for another. We're looking into ways to draw a clearer boundary between subscription types and give each one a more explicit, purpose-built set of options, which may also mean some changes to the client protocol down the line. To be clear: this is early exploration, not work in progress — no implementation has started, and we'll share specifics if and when it does.
See the list of merged PRs for the complete set of changes, including CI and dependency updates not listed above.
gorelease -base v0.38.0 -version v0.39.0
# github.com/centrifugal/centrifuge
## incompatible changes
(*MemoryBroker).Publish: changed from func(string, []byte, PublishOptions) (StreamPosition, bool, error) to func(string, []byte, PublishOptions) (PublishResult, error)
(*MemoryBroker).Subscribe: changed from func(string) error to func(...string) error
(*MemoryBroker).Unsubscribe: changed from func(string) error to func(...string) error
(*RedisBroker).Publish: changed from func(string, []byte, PublishOptions) (StreamPosition, bool, error) to func(string, []byte, PublishOptions) (PublishResult, error)
(*RedisBroker).Subscribe: changed from func(string) error to func(...string) error
(*RedisBroker).Unsubscribe: changed from func(string) error to func(...string) error
Broker.Publish: changed from func(string, []byte, PublishOptions) (StreamPosition, bool, error) to func(string, []byte, PublishOptions) (PublishResult, error)
Broker.Subscribe: changed from func(string) error to func(...string) error
Broker.Unsubscribe: changed from func(string) error to func(...string) error
HandleReadFrame: changed from func(*Client, io.Reader) bool to func(*Client, io.Reader, int64) bool
PublishResult.FromCache: removed
## compatible changes
(*Client).Labels: added
(*Client).OnMapPublish: added
(*Client).OnMapRemove: added
(*Client).OnTrack: added
(*Client).OnUntrack: added
(*Client).Profile: added
(*Hub).BroadcastPublicationDelta: added
(*Node).AddMapBrokerCleanupRemoved: added
(*Node).IncMapBrokerCleanupErrors: added
(*Node).IncTransportOutgoingClose: added
(*Node).MapClear: added
(*Node).MapPublish: added
(*Node).MapRemove: added
(*Node).MapStateRead: added
(*Node).MapStats: added
(*Node).MapStreamRead: added
(*Node).OnSharedPoll: added
(*Node).SetMapBroker: added
(*Node).SetMapBrokerCleanupLag: added
(*Node).SharedPollNotify: added
(*Node).SharedPollPublish: added
Config.Map: added
Config.SharedPoll: added
ConnectEvent.Profile: added
ConnectReply.Labels: added
ConnectReply.Profile: added
ConnectReply.QueueShrinkDelay: added
ConnectReply.WriteWithTimer: added
DisconnectStateInvalidated: added
ErrorConcurrentPagination: added
FilterNode: added
KeyMode: added
KeyModeIfExists: added
KeyModeIfNew: added
KeyModeReplace: added
MakeOrderedCursor: added
MapBroker: added
MapChannelOptions: added
MapClearOptions: added
MapConfig: added
MapCurrentEntry: added
MapMode: added
MapModeEphemeral: added
MapModePersistent: added
MapModeRecoverable: added
MapPhaseLive: added
MapPhaseState: added
MapPhaseStream: added
MapPublishCallback: added
MapPublishEvent: added
MapPublishHandler: added
MapPublishOptions: added
MapPublishReply: added
MapReadStateOptions: added
MapReadStreamOptions: added
MapRemoveCallback: added
MapRemoveEvent: added
MapRemoveHandler: added
MapRemoveOptions: added
MapRemoveReply: added
MapStateResult: added
MapStats: added
MapStatsResult: added
MapStreamResult: added
MapUpdateResult: added
MemoryMapBroker: added
MemoryMapBrokerConfig: added
MetricsConfig.ClientLabels: added
MetricsConfig.EnableNativeHistograms: added
NewMemoryMapBroker: added
NewRedisMapBroker: added
Publication.Epoch: added
Publication.Key: added
Publication.Removed: added
Publication.Score: added
Publication.Version: added
PublishOptions.Epoch: added
PublishOptions.Key: added
PublishOptions.Offset: added
PublishOptions.PrevData: added
PublishOptions.Removed: added
PublishResult.SuppressReason: added
PublishResult.Suppressed: added
RedisAuthCredentials: added
RedisAuthCredentialsContext: added
RedisBrokerConfig.UsePrecomputedPartitionTags: added
RedisMapBroker: added
RedisMapBrokerConfig: added
RedisShardConfig.AuthCredentialsFn: added
ResolveAndValidateMapChannelOptions: added
SharedPollChannelOptions: added
SharedPollConfig: added
SharedPollEvent: added
SharedPollHandler: added
SharedPollItem: added
SharedPollManager: added
SharedPollModeVersioned: added
SharedPollModeVersionless: added
SharedPollNotification: added
SharedPollNotificationItem: added
SharedPollRefreshItem: added
SharedPollResult: added
StreamFilter: added
SubRefreshReply.ServerTagsFilter: added
SubscribeEvent.Type: added
SubscribeOptions.AutoCacheRecover: added
SubscribeOptions.ClientPublishDebounceInterval: added
SubscribeOptions.MapClientPresenceChannel: added
SubscribeOptions.MapRemoveClientOnUnsubscribe: added
SubscribeOptions.MapUserPresenceChannel: added
SubscribeOptions.ServerTagsFilter: added
SubscribeOptions.Type: added
SubscribeReply.Publications: added
SubscriptionType: added
SubscriptionTypeMap: added
SubscriptionTypeMapClients: added
SubscriptionTypeMapUsers: added
SubscriptionTypeSharedPoll: added
SubscriptionTypeStream: added
SuppressReason: added
SuppressReasonIdempotency: added
SuppressReasonKeyExists: added
SuppressReasonKeyNotFound: added
SuppressReasonNone: added
SuppressReasonPositionMismatch: added
SuppressReasonVersion: added
TrackBatch: added
TrackBatchReply: added
TrackCallback: added
TrackEvent: added
TrackHandler: added
TrackItem: added
TrackReply: added
TransportWriteEvent.Key: added
UnsubscribeCodeStateInvalidated: added
UntrackEvent: added
UntrackHandler: added
WebsocketConfig.DecompressedMessageSizeLimit: added
WebsocketConfig.ProcessCommandsOffReadLoop: added
WithAutoCacheRecover: added
WithDisconnectAllUsers: added
WithDisconnectLabelFilter: added
WithKey: added
WithRefreshAllUsers: added
WithRefreshLabelFilter: added
WithSubscribeAllUsers: added
WithSubscribeLabelFilter: added
WithUnsubscribeAllUsers: added
WithUnsubscribeLabelFilter: added
# summary
v0.39.0 is a valid semantic version for this release.
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →