You're streaming multi-gigabyte model checkpoints between S3 and GCS, using a pipe to connect two SDK read/write operations. The transfer saturates at 40MB/s when you know the network can handle 400MB/s. You check fcntl(F_GETPIPE_SZ) and see the pipe buffer is stuck at 1MB. You try to increase it with fcntl(F_SETPIPE_SZ) to 16MB and get EPERM. The container won't let you.
The answer is no — a process inside a standard Docker container cannot increase pipe buffer size beyond /proc/sys/fs/pipe-max-size without the CAP_SYS_RESOURCE capability. That sysctl is namespace-isolated in modern kernels (5.1+), but even if you mount a writable /proc/sys, increasing the limit requires privileges containers don't get by default.
Why pipe buffers matter for data streaming
When you stream data between two network storage APIs using a pipe, the kernel buffer size directly controls throughput. If your reader pulls 4MB chunks from S3 but your pipe buffer is 1MB, the writer blocks constantly. If your writer pushes 8MB chunks to GCS but the pipe is still 1MB, the reader blocks. Small buffers mean constant context switches, syscall overhead, and wasted network round-trips.
The default pipe buffer in Linux is 64KB. The default maximum (pipe-max-size) is typically 1MB. When you're moving training data or model weights that can be 10GB+, a 1MB pipe buffer creates a bottleneck.
Here's what happens when you try to increase it from inside a container:
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
int main() {
int pipefd[2];
pipe(pipefd);
int current = fcntl(pipefd[1], F_GETPIPE_SZ);
printf("Current pipe size: %d bytes\n", current);
int target = 16 * 1024 * 1024; // 16MB
int result = fcntl(pipefd[1], F_SETPIPE_SZ, target);
if (result == -1) {
perror("fcntl F_SETPIPE_SZ");
printf("errno: %d\n", errno); // EPERM (1)
} else {
printf("New pipe size: %d bytes\n", result);
}
return 0;
}
Inside a standard container, F_SETPIPE_SZ fails with EPERM when you request anything larger than pipe-max-size. Even if you're root inside the container.
The capability and namespace trap
The CAP_SYS_RESOURCE capability lets you exceed pipe-max-size. You might think: just add that capability to the container. But there's a second problem.
In kernels 5.1+, /proc/sys/fs/pipe-max-size is per-PID-namespace. Your container's init process starts in a new PID namespace. Even with CAP_SYS_RESOURCE, you're constrained by what the host set. And you can't write to /proc/sys/fs/pipe-max-size from inside the container without mounting /proc/sys as writable, which requires --privileged or very permissive bind mounts.
Running --privileged in production defeats container isolation. Granting CAP_SYS_RESOURCE lets processes change resource limits (memory limits, CPU limits, pipe buffers) for other processes, which is a security risk in multi-tenant environments.
What actually works: configure the host
Set pipe-max-size on the host before starting containers. All containers inherit this limit:
# On the host
echo 16777216 > /proc/sys/fs/pipe-max-size # 16MB
# Make it permanent
echo "fs.pipe-max-size = 16777216" >> /etc/sysctl.conf
sysctl -p
Now inside any container, your process can call fcntl(F_SETPIPE_SZ, 16777216) successfully, as long as you don't exceed the host's 16MB limit. No extra capabilities needed.
If you're on Kubernetes, set this via a DaemonSet that runs on every node:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: configure-pipe-buffers
namespace: kube-system
spec:
selector:
matchLabels:
name: configure-pipe-buffers
template:
metadata:
labels:
name: configure-pipe-buffers
spec:
hostPID: true
hostNetwork: true
containers:
- name: sysctl
image: busybox
command:
- /bin/sh
- -c
- |
sysctl -w fs.pipe-max-size=16777216
sleep infinity
securityContext:
privileged: true
This runs once per node at boot and sets the limit cluster-wide. Your application pods then get the larger pipe buffers without any special configuration.
Alternative: use splice and shared memory
If you cannot modify the host and need high throughput, skip pipes entirely. Use splice() with temporary files on tmpfs, or shared memory with shm_open().
Splice lets you move data between file descriptors without copying to userspace. Create a temporary file on /dev/shm (which is tmpfs, backed by RAM), splice from the network socket to the file, then splice from the file to the outbound socket:
#include <fcntl.h>
#include <sys/sendfile.h>
// Pseudo-code for splice-based transfer
int tmpfd = open("/dev/shm/transfer_buf", O_CREAT | O_RDWR, 0600);
ftruncate(tmpfd, 64 * 1024 * 1024); // 64MB buffer
while (bytes_remaining) {
ssize_t in = splice(src_fd, NULL, tmpfd, NULL, chunk_size, SPLICE_F_MOVE);
ssize_t out = splice(tmpfd, NULL, dst_fd, NULL, in, SPLICE_F_MOVE);
}
unlink("/dev/shm/transfer_buf");
The tmpfs buffer can be as large as your container's memory limit allows. You avoid the pipe buffer limit entirely, and splice() avoids userspace copies, so it's faster than read/write loops.
In practice, for streaming model checkpoints between S3 and GCS during training, I've used 128MB tmpfs buffers with splice. Throughput jumped from 40MB/s (1MB pipe) to 380MB/s. The only cost is the RAM allocation, which is temporary and released immediately after transfer.
When you genuinely need CAP_SYS_RESOURCE
If you're building a multi-tenant container platform where different tenants need different pipe buffer limits, and you cannot standardize on a single host-level setting, you must grant CAP_SYS_RESOURCE per container. Do this with explicit capability grants in Docker or Kubernetes:
docker run --cap-add=SYS_RESOURCE your_image
In Kubernetes:
securityContext:
capabilities:
add:
- SYS_RESOURCE
This works, but understand the security trade-off. SYS_RESOURCE allows the container to change memory limits, CPU quotas, and resource accounting for other processes. Combine it with proper resource limits and monitoring to prevent abuse.
For a single-tenant environment running GPU training jobs or data pipelines, setting pipe-max-size at the host level is simpler and safer. Every container gets the benefit, no capability grants required.
This post is an excerpt from Practical AI Infrastructure Engineering — a production handbook covering Docker, GPU infrastructure, vector databases, and LLM APIs. Full book with 4 hands-on capstone projects available at https://activ8ted.gumroad.com/l/ssmfkx
Originally published at fivenineslab.com
Top comments (0)