| pick your distro, get ZFS on root
kldload — your platform, your way, free
Source

Channels, back planes, and the infrastructure underneath.

When people learn about building services, they focus on the ingress side. How do I accept connections? How do I parse requests? How do I handle load? Good questions. But only half the picture.

A service that can only receive is a dead end. Real systems are conversations. Your service receives a request, processes it, and then talks to other things — databases, queues, other services, hardware. The send side is just as important as the receive side, and the patterns you use for it change everything about reliability, latency, and debuggability.

This page starts with the protocol patterns — control channels, back channels, push vs pull. Then it goes deeper: what happens when the most powerful channel is the infrastructure itself — when storage, networking, and observability are kernel primitives baked into the image, and your services get capabilities they never asked for.

Part 1: Channels and control planes

Here's a pattern that shows up everywhere, and once you see it, you can't unsee it: active and passive communication.

FTP: The original two-channel protocol

FTP uses two connections: a control channel (port 21) where commands are sent, and a data channel (a separate port) where actual file data flows. In active mode, the server connects back to the client on a port the client specified. In passive mode, the client connects to a port the server specified.

This separation of control and data is everywhere. VDI protocols (Citrix ICA, VMware PCoIP) use a control channel for session management and separate channels for display, audio, USB, and clipboard — each optimized differently. Kubernetes uses the API server as a control plane and kubelet connections as the data plane.

It's like a phone call where you discuss the plan (control), and then a delivery truck brings the actual goods (data). Different channels for different purposes.

The back channel: reverse communication

Sometimes the most powerful pattern is the one that goes backwards. Instead of the client always initiating, the server pushes. Instead of polling, you subscribe. Instead of the installer pulling packages from the internet, the ISO carries everything with it.

An installer is nothing more than a process where hardware and services communicate to accomplish a feat. And the smartest installers use back channels — the target system reporting its state back to the installer, the bootloader telling the init system what to mount, the kernel passing parameters to userspace through /proc/cmdline.

Back channels are how the kitchen tells the waiter the special is sold out before they promise it to the table.

Part 2: The back plane

The back plane is the feature your API didn't know it needed.

Most developers build APIs that do one thing: accept a request, process it, return a response. Request in, response out. That's the front plane. But every serious system has a back plane — a channel that runs alongside your application, handling things your API shouldn't have to think about.

Think about what's happening underneath a typical web service:

Front plane (your API):
  POST /api/upload → accept file → store → return 200

Back plane (what the infrastructure handles):
  → ZFS checksums every block as it's written (data integrity)
  → ZFS compresses the data transparently (storage efficiency)
  → Snapshot fires automatically before the write (undo capability)
  → WireGuard encrypts the replication stream (secure distribution)
  → zfs send pushes the dataset to DR site (disaster recovery)
  → eBPF traces the write latency (observability)

Your API wrote one line: store(file). The back plane did six things
your API never asked for, never configured, and can't break.

This is what "baked into the image" actually means. When the back plane is in the kernel — not in a sidecar, not in an agent, not in a config file that gets missed during deployment — every application on the machine gets it for free. Your API doesn't import a ZFS library. It doesn't call a snapshot endpoint. It doesn't configure replication. It writes a file. The kernel handles the rest.

Now think about what you can add to your API when you know the back plane exists:

# Your API can now offer these features with zero additional infrastructure:

# Point-in-time recovery for any customer
GET /api/customer/acme/restore?snapshot=2026-04-01T14:00
→ zfs rollback rpool/customers/acme@2026-04-01T14:00

# Instant staging environment from production data
POST /api/staging/create?from=production
→ zfs clone rpool/production@latest rpool/staging/ticket-4521
→ returns in 0.2 seconds regardless of dataset size

# Encrypted replication to customer's own infrastructure
POST /api/customer/acme/replicate?target=acme-dr.example.com
→ zfs send | ssh wg-peer zfs recv
→ your customer gets a verified copy, never decrypted in transit

# Real-time I/O observability without APM agents
GET /api/debug/slow-queries
→ eBPF traces disk latency per dataset, returns top offenders

None of these features required a new dependency. No new database. No new queue. No new service to deploy and monitor. They're API wrappers around kernel primitives that already exist on the machine because they were baked into the image at build time.

This is the power of embedding the back plane at image creation: every machine you deploy has these capabilities from second zero. You don't configure them per-host. You don't install them post-deploy. You don't forget them on the staging server. They're structural. They're in the image. They're everywhere, always.

Protocol brokers: the right tool for the job

Ansible is great at push. "Go to these ten machines and run this playbook." But it's stateless — it doesn't know what happened between runs. Salt is a protocol broker. It maintains persistent connections (ZeroMQ under the hood), receives events in real-time, and can react. It does what Ansible does, plus everything Ansible can't.

The trick isn't choosing one. It's understanding that push and pull, active and passive, request-reply and pub-sub — these are all tools. A hammer and a screwdriver aren't competing. You use the one that fits. The person who only has a hammer thinks everything is a nail. The person who understands the toolbox builds things that work.

Salt is the nervous system (always connected, real-time). Ansible is the postal service (reliable, but you send a letter and wait). Both deliver messages. Neither replaces the other.

So that's how services talk to each other — control channels, back channels, push and pull. But there's a deeper question: what channel does the infrastructure itself provide? What if the most powerful back channel isn't one your application creates, but one the kernel gives you for free?

The image is the deployment. The deployment is the infrastructure.

Traditional infrastructure has stages: provision the machine, install the OS, configure networking, add storage, deploy the app, bolt on monitoring, set up backups. Each stage is a separate tool, a separate config, a separate failure mode. Miss one and you have a machine that's 90% right — which is the same as wrong.

When you bake the back plane into the image, the stages collapse:

Traditional:
  provision → install OS → configure networking → add storage
  → deploy app → add monitoring → configure backups → harden
  → pray nothing was missed
  (8 stages, 8 tools, 8 places to make a mistake)

kldload:
  boot the image
  (the image IS the OS + networking + storage + snapshot policy + security hardening)
  → deploy your app
  (2 stages. The first one is turning the machine on.)

This isn't about saving time, although it does. It's about eliminating the gaps between stages where things go wrong. The machine that doesn't have monitoring because someone forgot the Ansible role. The server that doesn't have backups because the cron job wasn't in the golden image. The production box running without encryption because the WireGuard config was "TODO."

When it's in the image, it's everywhere. When it's a post-deploy step, it's everywhere you remembered to put it.

Part 3: Datasets as service boundaries

Datasets are service boundaries. Design them that way.

Any service that manipulates files — an upload processor, a transcoder, a log aggregator, a CI runner — is reading and writing to a path. That path is a directory. On ext4, all directories share one filesystem, one set of I/O characteristics, one failure domain. A runaway log writer starves the upload processor. A transcoder's sequential 4GB writes thrash the CI runner's random 4K reads. Everything competes for the same disk, the same cache, the same throughput.

On ZFS, you separate them into datasets. The service doesn't know the difference — it still reads and writes to a path. But now each path has its own tuning:

# Upload processor — large incoming files, compress well, limit space
zfs create -o recordsize=1M -o compression=zstd -o quota=500G \
  rpool/srv/uploads

# Transcoder — huge sequential reads/writes, skip compression (already encoded)
zfs create -o recordsize=1M -o compression=off -o atime=off \
  rpool/srv/transcode

# Log aggregator — append-only, compress heavily, rotate via snapshots
zfs create -o recordsize=1M -o compression=zstd-19 -o atime=off \
  rpool/srv/logs

# CI runner — small random I/O, fast metadata, throw away between builds
zfs create -o recordsize=16K -o compression=lz4 -o sync=disabled \
  rpool/srv/ci-workspace

# Database — tuned to page size, protect every write
zfs create -o recordsize=8K -o logbias=throughput -o compression=lz4 \
  rpool/srv/postgres

Each service gets I/O characteristics matched to its workload. The transcoder's 1M sequential writes don't pollute the database's 8K random reads in the ARC. The CI runner's throwaway workspace has sync=disabled because losing it on power failure doesn't matter — but the database has full sync because every write matters. Never use sync=disabled on anything you can't afford to lose on power failure. The log aggregator compresses at zstd-19 because it's write-once-read-rarely and saves 80% of disk space.

Now the interesting part — you can optimize the routing between them:

# User uploads a video. Traditional approach:
#   write to /tmp → copy to /uploads → copy to /transcode → copy to /output
#   Three copies of a 4GB file. 12GB of I/O for one upload.

# ZFS approach:
#   write to rpool/srv/uploads (one write)
#   zfs clone rpool/srv/uploads@snap rpool/srv/transcode/job-123 (instant, zero I/O)
#   transcode in place on the clone
#   zfs send the result to rpool/srv/output (block-level, only changed blocks)
#   destroy the clone

# One write. One clone (free). One incremental send. Total I/O: ~4GB instead of 12GB.
# The upload processor, transcoder, and output service never copied a file.
# They worked on datasets. ZFS moved the blocks.

This is what happens when you stop thinking of services as programs that read files and start thinking of them as consumers of datasets. The file manipulation becomes a dataset operation. The copy becomes a clone. The transfer becomes a send. The backup is a snapshot that already happened. You cut the I/O in half or better, and every stage is atomic, checksummed, and reversible.

The services never know. They read a path. They write to a path. But underneath, each path is a tuned, isolated, snapshotted, replicable storage domain — and the data flows between them at the block level instead of being copied through userland.