SENTO-000 ZeroMQ Transport Analysis
DONE Objective
DONE Research Baseline
Reviewed on 2026-07-19:
- libzmq6 official repository and API documentation
- ZeroMQ Message Transport Protocol7 specifications
- ZeroMQ Authentication Protocol8
- CurveZMQ9
- ROUTER and DEALER socket behavior
- PUB, SUB, XPUB, and XSUB behavior
- PUSH and PULL behavior
- socket high-water marks10
- socket monitor API
- steerable proxy API
- Common Lisp PZMQ binding repository
- current Sento actor, dispatcher, router, event-stream, and remoting implementation
DONE Main Decision
ZeroMQ is an optional transport adapter and process bridge.
ZeroMQ does not replace local Sento mailboxes11, domain dispatchers, actor supervision, managed local routers, the component registry, or Star Router12.
The component-handle layer selects ZeroMQ only when a message must cross a process boundary, reach a non-Sento service, use a ZeroMQ message pattern, or join a ZeroMQ-compatible topology.
DONE Thread-Ownership Finding
A ZeroMQ context13 is thread-safe and may be shared by application threads.
Most classic ZeroMQ sockets are not thread-safe. A socket must be created, used, and closed by one owning thread.
A Sento actor on a shared dispatcher may run mailbox handlers on different dispatcher worker threads over time. Such an actor must not directly hold or call a classic ZeroMQ socket.
A pinned Sento actor does have a dedicated mailbox thread, but blocking that receive handler forever in zmq_poll would prevent the actor mailbox from processing lifecycle and control messages.
Resulting design
Use a supervised ZeroMQ reactor component:
- supervisor actor owns lifecycle
- bounded multiple-producer, single-consumer command queue carries outbound work
- one dedicated reactor thread owns sockets
- reactor thread polls sockets, drains commands, expires deadlines, and emits metrics
- inbound frames are sent into Sento through normal actor messages
- no ordinary actor receives a raw socket pointer
DONE Context Model
Use one process-wide libzmq context by default.
The context owns libzmq internal input and output threads and every ZeroMQ socket created by the process.
Each domain may own one reactor, and may split into subdomain reactors when measured load, endpoint ownership, security policy, or independent restart justifies it.
Multiple contexts are possible but duplicate internal resources and complicate shutdown. They require an explicit design reason.
DONE Reactor Topology
Sento actor system
└── /internal/transports/zeromq
├── context-supervisor
├── zap-handler
├── monitor-collector
└── reactors
├── humint
├── socmint
├── sigint
├── cyber
├── bbp
└── threat-intel
Each reactor contains:
- supervisor child specification
- domain or subdomain identifier
- dedicated reactor thread
- bounded outbound command queue
- socket specifications
- endpoint registry
- pending request table
- serializer
- connection monitor
- transport generation
- metrics
DONE Actor-to-ZeroMQ Flow
- A Sento actor sends a transport command to a bridge actor.
- The bridge resolves the component handle.
- The bridge checks deadline, payload size, schema, and policy.
- The bridge enqueues a bounded command for the reactor thread.
- The reactor thread owns the socket call.
- The reactor reports success, overload, route failure, or transport failure through Sento messaging.
A successful local enqueue is not proof that the remote component received or processed the message.
DONE ZeroMQ-to-Actor Flow
- The reactor poll loop receives multipart frames.
- It checks frame count and hard byte limits.
- It decodes only the fixed transport header.
- It sends a typed transport envelope to the domain ingress actor.
- The ingress actor checks authorization, deadline, schema, component generation, and destination export.
- The ingress actor sends the application message to the destination component.
- Replies use the stored routing identity and correlation identifier.
DONE Socket Pattern Selection
ROUTER and DEALER
Use ROUTER14 and DEALER15 for asynchronous requests, replies, commands, and component messaging.
ROUTER receives a routing identity frame for each peer and can select the destination identity when sending.
DEALER can send multiple requests without the strict send-then-receive lockstep imposed by REQ16 sockets.
Enable ZMQ_ROUTER_MANDATORY so an unroutable send returns an error rather than disappearing silently.
XPUB and XSUB
Use XPUB17 and XSUB18 behind a steerable proxy19 for selected cross-process event distribution.
Suitable messages:
- telemetry
- cache invalidation hints
- discovery announcements
- non-authoritative status notifications
- high-volume observations whose loss is acceptable
PUB/SUB20 is not a durable message log. Late or disconnected subscribers can miss messages.
PUSH and PULL
Use PUSH and PULL21 for stateless, idempotent, one-way work distribution where no direct reply, cancellation, or key affinity is required.
Use ROUTER and DEALER for work requiring replies, deadlines, cancellation, target selection, or stable routing keys.
PAIR
Use PAIR22 only for one-to-one internal control, socket monitoring, and proxy control.
REQ and REP
Do not use REQ and REP23 for the main component transport because their strict alternating state machine complicates concurrent requests and out-of-order replies.
REQ and REP remain valid when an external protocol requires them. ZAP itself defines a request and reply dialog.
CLIENT and SERVER
Do not require ZeroMQ CLIENT and SERVER sockets in the first implementation. They belong to the newer thread-safe socket family and have single-part message restrictions. Classic ROUTER and DEALER provide the required stable multipart model with explicit thread ownership.
DONE Transport Selection
inproc://: ZAP, proxy control, socket monitor channels, and internal ZeroMQ-only control inside one processipc://: same-host process communication when supportedtcp://: communication across hosts and containers
Normal actors inside one Sento process continue to use Sento messages. inproc:// is not a replacement local actor bus.
DONE Message Envelope
The ZeroMQ routing identity remains outside the application envelope.
Application frames contain:
- protocol magic and version
- message kind
- message identifier
- correlation identifier
- trace identifier
- source node and component
- target component export
- operation or event topic
- deadline
- content type and schema identifier
- flags
- payload
Supported kinds:
- tell
- ask
- reply
- error
- event
- work
- cancel
- heartbeat
The reactor rejects unsupported versions, wrong frame counts, oversized frames, and malformed fixed fields before payload decoding.
DONE Binding Review
The reviewed Common Lisp PZMQ repository is orivej/pzmq.
Its README describes ZeroMQ 4.0 bindings, warns that functions outside examples and tests may be untested, and states that raw performance was not fully optimized.
The latest reviewed commit is:
6f7b2ca02c23ea53510a9b0e0f181d5364ce9d32 Grovel zmq_pollitem_t (#35) 2021-05-16
Decision
Create sento-zmq as a narrow Common Lisp Foreign Function Interface24 system against a pinned stable libzmq package.
PZMQ may be forked or used as a reference, but every used function, structure, constant, socket option, binary path, error condition, interrupt behavior, and shutdown behavior must have direct tests.
Required binding surface:
- context create, configure, shutdown, and terminate
- socket create and close
- bind, unbind, connect, and disconnect
- multipart send and receive
- poll
- socket monitor
- socket options
- CURVE options
- error access
- version access
Binary payloads remain unsigned-byte vectors rather than being converted through text strings.
DONE Backpressure Findings
ZeroMQ high-water marks bound queued messages per peer.
Depending on socket type and options, a full queue may block, return EAGAIN25, or drop a message.
Recommended settings:
- finite send and receive high-water marks
- finite send timeout or non-blocking send
- finite receive polling interval
ZMQ_IMMEDIATEon outbound sockets where pre-connection queuing would hide unavailabilityZMQ_ROUTER_MANDATORYon ROUTER sockets- finite
ZMQ_LINGERso shutdown cannot wait forever - explicit heartbeat and reconnect policy
Map EAGAIN to a structured overload result.
Map EHOSTUNREACH26 to a structured route-unavailable result.
The full path must bound:
- Sento sender mailbox
- component ingress mailbox
- reactor command queue
- ZeroMQ peer queue
- remote receive queue
- remote ingress mailbox
DONE Security Findings
Use CurveZMQ for encrypted and authenticated tcp:// endpoints.
Run one ZAP handler per process at:
inproc://zeromq.zap.01
ZAP maps a remote public key to a principal27, node, allowed component exports, allowed operations, quota class, and audit metadata.
The ZAP handler starts before server sockets begin accepting peers.
Use filesystem ownership and permissions for ipc:// endpoints, while retaining application-level target and operation policy.
Do not use ZeroMQ PLAIN authentication over an untrusted network because it does not provide confidentiality.
DONE Delivery Semantics
ZeroMQ queues are memory-based. They are not durable storage28.
Default semantics:
- ROUTER/DEALER tell: at-most-once transport attempt
- ROUTER/DEALER ask: at-most-once transport attempt with application correlation
- PUSH/PULL work: at-most-once transport attempt
- PUB/SUB event: best-effort delivery
Automatic retries are disabled by default.
A retry requires an idempotency key29 and duplicate-handling policy.
Durable replay belongs in a database, event log, RabbitMQ30, or another declared durable broker.
DONE Monitoring Findings
zmq_socket_monitor emits socket lifecycle events over an internal PAIR channel.
Relevant events include:
- connected
- connect delayed
- connect retried
- listening
- bind failed
- accepted
- accept failed
- closed
- close failed
- disconnected
- handshake succeeded
- handshake protocol failure
- handshake authentication failure
A monitor collector converts these events into typed Sento runtime events.
Native file descriptors reported by monitor events are transient observations and must not become stable actor identifiers.
DONE Supervision Model
The reactor supervisor treats these as failures:
- reactor thread exit
- unexpected context termination
- socket bind failure
- repeated poll failure
- unrecoverable serializer failure
- command queue corruption
- monitor channel failure
Restart sequence:
- close admission
- mark transport not ready
- fail or cancel pending requests
- close sockets on the owning reactor thread
- join the reactor thread
- increase transport generation
- recreate sockets
- wait for endpoint and authentication readiness
- reopen admission
Old-generation replies are rejected as stale.
DONE Shutdown Model
- Reject new transport work.
- Stop subscriptions and external admission.
- Drain the command queue until the deadline.
- Resolve or cancel pending requests.
- Pause and terminate proxies.
- Close sockets on owner threads with finite linger.
- Join reactor and monitor threads.
- Stop ZAP after accepting sockets are closed.
- Shut down and terminate the ZeroMQ context.
- Continue normal Sento shutdown.
The context must not terminate while live sockets remain.
DONE Validation Requirements
- classic sockets are never used from multiple threads
- socket creation and closure occur on owner threads
- malformed multipart input is rejected before decoding
- high-water saturation returns overload results
- mandatory routing returns route errors
- finite linger prevents permanent shutdown blocking
- unauthorized CURVE keys are denied
- authorized keys map to declared principals
- old-generation replies are rejected
- pending asks expire and are removed
- proxy control works under load
- socket monitor events become Sento events
- domain reactor failure does not stop sibling domains
- subdomain reactor restart does not restart the whole process
- no socket, context, monitor, proxy, or reactor thread leaks after shutdown
DONE Recommendation
Implement the ZeroMQ path in this order:
- narrow CFFI binding
- context supervisor
- single domain reactor with command queue
- ROUTER/DEALER component messaging
- fixed schema envelope
- high-water and route errors
- socket monitoring
- CURVE and ZAP
- XPUB/XSUB event proxy
- optional PUSH/PULL work pipeline
- component-handle adapter
- benchmarks against Sento remoting and Star Router
Footnotes and Glossary
DONE Sources
- Retrieved 2026-07-19: https://github.com/zeromq/libzmq
- Retrieved 2026-07-19: https://api.zeromq.org/4-2:zmq-socket
- Retrieved 2026-07-19: https://libzmq.readthedocs.io/en/latest/zmq_socket.html
- Retrieved 2026-07-19: https://libzmq.readthedocs.io/en/latest/zmq_setsockopt.html
- Retrieved 2026-07-19: https://libzmq.readthedocs.io/en/latest/zmq_getsockopt.html
- Retrieved 2026-07-19: https://libzmq.readthedocs.io/en/latest/zmq_socket_monitor.html
- Retrieved 2026-07-19: https://libzmq.readthedocs.io/en/latest/zmq_proxy_steerable.html
- Retrieved 2026-07-19: https://rfc.zeromq.org/spec/23/
- Retrieved 2026-07-19: https://rfc.zeromq.org/spec/25/
- Retrieved 2026-07-19: https://rfc.zeromq.org/spec/26/
- Retrieved 2026-07-19: https://rfc.zeromq.org/spec/27/
- Retrieved 2026-07-19: https://github.com/orivej/pzmq
- Retrieved 2026-07-19: https://github.com/orivej/pzmq/commit/6f7b2ca02c23ea53510a9b0e0f181d5364ce9d32
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/blob/a889b8375709ca9396bba5d06b6e460689a23716/src/actor-system.lisp
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/blob/a889b8375709ca9396bba5d06b6e460689a23716/src/dispatcher.lisp
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/blob/a889b8375709ca9396bba5d06b6e460689a23716/src/router.lisp
- Retrieved 2026-07-19: https://github.com/mdbergmann/cl-gserver/blob/a889b8375709ca9396bba5d06b6e460689a23716/src/remoting/remoting.lisp
Footnotes:
ZeroMQ: A brokerless messaging library that provides asynchronous message sockets, routing patterns, and multiple transports.
Sento: The Common Lisp actor framework implemented in the mdbergmann/cl-gserver repository.
Dispatcher: A named pool of worker actors and threads used to execute actor mailbox work.
Supervision: Starting, watching, restarting, and stopping child components according to declared policy.
Overload: A state where work arrives faster than the system can safely process or queue it.
libzmq: The C and C++ core engine implementing ZeroMQ sockets and protocol behavior.
ZMTP: ZeroMQ Message Transport Protocol. The wire protocol used between ZeroMQ peers.
ZAP: ZeroMQ Authentication Protocol. The in-process protocol used by a server socket to ask an authentication handler whether a peer is permitted.
CurveZMQ: ZeroMQ's public-key encryption and authentication protocol based on Curve25519 cryptography.
High-water mark: The configured maximum number of queued messages for one ZeroMQ peer connection.
Mailbox: The queue owned by one actor for messages waiting to be processed.
Star Router: StarIntel's client-facing and application-level cross-process routing layer.
ZeroMQ context: The process-level object that owns ZeroMQ sockets and internal input and output threads.
ROUTER socket: A ZeroMQ socket that receives peer routing identities and can select a peer when sending.
DEALER socket: An asynchronous ZeroMQ request socket that permits multiple outstanding requests and out-of-order replies.
REQ socket: A ZeroMQ request socket that normally alternates strictly between one send and one receive.
XPUB socket: An extended publisher socket that also reports subscription activity.
XSUB socket: An extended subscriber socket used to receive publications and forward subscription commands.
Steerable proxy: A ZeroMQ proxy that can be paused, resumed, or terminated through a control socket.
PUB/SUB: Publish and Subscribe. A pattern where publishers send topics and connected subscribers receive topics they selected.
PUSH/PULL: A one-way pipeline pattern that distributes work from PUSH sockets to PULL sockets.
PAIR socket: A socket intended for one fixed peer, commonly used for internal control or monitoring.
REP socket: A ZeroMQ reply socket that normally alternates strictly between receiving one request and sending one reply.
CFFI: Common Foreign Function Interface. A Common Lisp library used to call C functions and represent C data.
EAGAIN: An error meaning an operation cannot complete immediately and may succeed later.
EHOSTUNREACH: An error meaning the selected routing destination is not currently reachable.
Principal: The verified user, service, or node identity used for authorization.
Durable storage: Storage designed to retain data across process restarts or machine failures.
Idempotency key: A unique request value that lets a receiver identify and safely handle a retried operation.
RabbitMQ: A message broker that routes messages through queues and exchanges and can persist messages for durable delivery.