open source · go 1.26

shared state across pods.
without the cluster.

podshare is a small Go library. Every pod holds the same map[string]T; writes broadcast over a pluggable transport. Reads are local — about 17 nanoseconds. Pick the wire: Redis pub/sub, P2P TCP, or roll your own.

live · 3 pods · MemoryTransport
pod-1
online
empty
pod-2
online
empty
pod-3
online
empty
the pattern

replicate the route, not the resource

You can't ship a *websocket.Conn between pods. You can ship two bytes of "who owns it." That's the whole idea.

pod-3 pub/sub pod-1 │ │ │ │ call{id, "ws:alice", │ │ │ "Send", "hello"} ─┼─► │ │ │ handlers["ws:alice"] │ │ │ ["Send"](args) │ │ │ ↓ │ │ │ conn.Write(...) │ │ │ │ │◄── reply{id, nil/err} ──────┼── │

Every pod holds an identical map[string]Route. When a user connects to pod-1, pod-1 publishes "ws:alice" → "pod-1"; every other pod sees the claim in under 100 milliseconds.

When any pod wants to push to alice, it looks up the route locally (~17ns), publishes a small call envelope to the owning pod's inbox, and awaits the reply on its own inbox.

The connection itself never moves. The route — a tiny string keyed by user ID — is the only thing that crosses the network.

three lines

type-safe over the wire

Generic over T. JSON by default; swap with WithCodec for msgpack, protobuf, anything.

⌘ basic
⌘ redis
⌘ watch
⌘ callreply (rpc)
type Config struct {
    RateLimit    int             `json:"rate_limit"`
    FeatureFlags map[string]bool `json:"feature_flags"`
}

tr := transport.NewMemoryTransport()
defer tr.Close()

store, _ := podshare.New[Config](ctx, "app-config", tr)
defer store.Close()

store.Set(ctx, "global", Config{RateLimit: 1000})
cfg, _ := store.Get("global")  // ~17ns, local
// Two pods on the same Redis converge automatically.
tr, _ := transport.NewRedisTransport(transport.RedisOptions{
    Addr: "localhost:6379",
})
defer tr.Close()

store, _ := podshare.New[Flag](ctx, "feature-flags", tr,
    podshare.WithNodeID[Flag]("pod-1"),
    podshare.WithErrorHandler[Flag](func(e error) {
        slog.Error("podshare", "err", e)
    }),
)
defer store.Close()

// After a Redis failover, recover missed events:
store.Refresh(ctx)
// Live propagation — non-blocking dispatch.
// WatchPrefix filters server-side; misses don't consume buffer.
for ev := range store.Watch(ctx, podshare.WatchPrefix("ws:")) {
    switch ev.Kind {
    case podshare.EventSet:
        cache.Store(ev.Key, &ev.Value)
    case podshare.EventDelete:
        cache.Delete(ev.Key)
    }
}
// Channel closes on ctx cancel OR slow-consumer drop.
// Closure = "re-Watch + Refresh".
// Call a method on whichever pod owns "ws:alice" —
// even if her WebSocket lives somewhere else.
ep, _ := callreply.New(tr, callreply.Options{SelfID: "pod-B"})
defer ep.Close()

// On the pod that owns alice's WS:
ep.Register("ws:alice", "Send", func(ctx context.Context, args []byte) ([]byte, error) {
    return nil, conn.WriteMessage(websocket.TextMessage, args)
})

// From any other pod:
ep.Call(ctx, "ws:alice", "Send", []byte("hello"))
what's in the box

small, sharp, complete

No leaky abstractions, no hidden allocations on hot paths, every option in the godoc.

<T>

Generic and type-safe

Store[T] uses Go generics — no any casts, no codegen, no reflection on the hot path.

[tx]

Pluggable transports

Memory for tests, Redis pub/sub for prod, P2P TCP for no-deps. Implement Transport for anything else.

Dynamic peer discovery

AddPeer / RemovePeer at runtime, plus a bundled DNSDiscoverer for K8s headless Services. Survives autoscaling without code changes.

LWW + Seq tiebreaker

Conflicts ordered by (time, origin, seq) — same-instant same-node writes are deterministically resolved.

Filtered watches

WatchKey / WatchPrefix apply server-side. Misses skip dispatch entirely.

Snapshot catch-up

New pods hydrate from a peer snapshot. Reconnects recover with Refresh(ctx).

CRDT seam

WithMerger for a true commutative join — G-Sets, counters, OR-Sets converge across pods.

Tombstones with TTL

Deletes retained long enough that late writes can't resurrect them. Periodic compaction tick.

callreply RPC

Forward calls to whichever pod owns a target. Self-call short-circuits to in-process.

Observable

Stats() counters, WithLogger for slog, OnError for everything else.

honest comparison

pick the smallest tool that solves the problem

podshare is intentionally narrow. Here's where it fits — and where it doesn't.

library model consistency embedded sweet spot
podshare full replication, LWW eventual yes (lib) small shared map, in-cluster
olric sharded DMap + replicas tunable yes large embedded KV
NATS JetStream KV sharded, history per-key strong no (cluster) durable shared state
etcd v3 Raft KV linearizable no (cluster) locks, leader election
memberlist gossip primitive eventual yes (lib) cluster membership
groupcache peer cache, consistent hash n/a (no replication) yes (lib) content distribution
where it shines

built for small-N, hot, mutable state

Six real patterns the library handles in roughly the same shape.

live rollout

feature flags

Operator pod sets a flag; every worker sees it via Watch within ms. Lock-free hot path via atomic.Pointer.

examples/feature-flags
hot cache

chat history

Recent conversations replicated across all chat-server pods. Any pod answers a follow-up turn — no DB hit.

examples/chat-cache
via callreply

websocket routing

Push a message to a user from any pod; the routing table forwards to whichever pod holds the WS connection.

examples/ws-routing
small map

presence / online users

Replicate a tiny "who's online" registry across the fleet. Local Get is ~17 nanoseconds.

examples/p2p
ops

fleet kill-switches

One pod trips a circuit-breaker flag; every other pod backs off within milliseconds. Same store as your config.

your pattern
routing

sharding hints

"user X lives on pod-3" — broadcast a small map of routing decisions for sticky-session-style affinity.

your pattern

not a fit for everything

  • durable source of truth — no durability guarantee, use Postgres.
  • large datasets — every pod holds the full map, use sharded storage.
  • strong consistency — LWW only, use etcd or a real Raft lib.
  • cross-region replication — designed for in-cluster sub-millisecond fan-out, not WAN.
  • order-preserving append — LWW collapses concurrent writes; model as a G-Set + sort on read.

start in 30 seconds

$ go get github.com/thanhphuchuynh/podshare@latest
$ go run github.com/thanhphuchuynh/podshare/examples/basic@latest