Subspace is a shared-memory pub/sub IPC system. The key insight is that the server is only involved in setup — actual message transfer happens directly through shared memory with no server round-trips, which is what gives it sub-microsecond latency.
- The client connects to the server via a Unix Domain Socket (default
/tmp/subspace). - It sends an
Initprotobuf request. - The server responds with a session ID and a file descriptor for the System Control Block (SCB) — a shared memory region the client maps into its address space.
Each channel uses three shared memory structures:
- System Control Block (SCB) — one per system, tracks channel-level counters for reload detection.
- Channel Control Block (CCB) — one per channel, contains slot metadata, bitsets for available/retired/free slots, ordinals, and stats. Publishers map it read-write; subscribers map it read-only.
- Message Buffers — the actual data. Stored in
/dev/shm/(Linux) or POSIX shared memory. Publishers map read-write, subscribers read-only. Buffers can grow dynamically. Split-buffer channels keep prefixes in these buffers but place payload slots in separate payload allocations.
- Create publisher — client sends an RPC to the server, which allocates shared memory and returns file descriptors for the CCB, buffers, and trigger FDs.
GetMessageBuffer(size)— returns a pointer into a shared memory slot. If the message is larger than the current slot size, the channel auto-resizes.- User writes data directly into the buffer (zero-copy).
PublishMessage(length)— writes aMessagePrefix(size, ordinal, timestamp, optional checksum and user metadata in the prefix area), marks the slot as available, and triggers subscriber file descriptors to wake them up.
- Create subscriber — similar RPC to the server, gets back FDs for shared memory and a trigger FD.
ReadMessage()— scans theavailable_slotsbitset in the CCB, finds the next unseen ordinal, atomically increments the slot's reference count, and returns a pointer directly into shared memory (zero-copy read).- Message lifetime — the returned
Messageobject holds a reference count on the slot. The slot can't be reused by a publisher until all subscribers release it.
- Unreliable — publisher always has a slot, may overwrite old unread messages (subscribers detect dropped messages via ordinal gaps).
- Reliable — publisher waits for a free slot, guaranteeing no message loss.
Client (public API, copyable)
└── ClientImpl (core implementation)
├── Publisher → PublisherImpl → ClientChannel → Channel
└── Subscriber → SubscriberImpl → ClientChannel → Channel
Split-buffer channels separate the MessagePrefix from the payload bytes. The
prefix remains in regular Subspace shared memory so ordinals, flags, checksums,
metadata, and slot state use the same layout. Payload slots live in separate
allocations that are either built-in shared-memory objects or application-owned
allocator handles mapped through callbacks.
Publishers opt in when creating the channel. Subscribers learn the channel mode from the server and provide mapping callbacks only when custom allocator handles are used. The server validates compatible options and replicates split-buffer metadata to the shadow process for crash recovery. See Split Buffers.
A C ABI layer over the C++ API using opaque void* pointers, plain structs, and
thread-local error strings. Functions like subspace_create_publisher(),
subspace_publish_message(), and subspace_read_message() mirror the C++ API
with C-style naming. Messages must be explicitly freed with
subspace_free_message().
The C wrapper also exposes channel introspection, stats/counters, timeout and fd-based waits, bulk reads, user metadata, checksum callbacks, split-buffer callbacks and handle lookup, transform callbacks, and slot/prefix diagnostics. See C Client API.
- Virtual channels — multiplex logical channels on one physical channel.
- Coroutine support — the client can yield when waiting for slots/messages (uses the
colibrary). - Checksums — optional message integrity verification using CRC32 by default, with support for arbitrary-sized checksums via callbacks. The
checksum_sizeandmetadata_sizepublisher options control the prefix layout; user metadata can be attached to each message throughGetMetadata(). See Checksums and User Metadata for details. - Split buffers — optional separate payload allocations for custom memory pools or handle-based mapping APIs. See Split Buffers.
- Bridging — forwards channels between servers over TCP for cross-machine communication.
- Tunnel support — the
for_tunnelpublisher/subscriber option marks messages with thekMessageCrossMachineflag in theMessagePrefix, allowing external tunnel processes to distinguish locally and remotely generated messages.
After the initial setup handshake with the server, all message passing is just reads and writes to shared memory plus lightweight FD triggers for notification — no context switches through a broker.