Fix "could not record an app's bytes" warnings
When Keploy records with its eBPF capture (keploy record --low-latency, a
Kubernetes Sidecar with low_latency_mode, and the DaemonSet agent), it
records what your app's send and receive calls copy between the app's memory
and its sockets. A few calls move bytes without copying them through the
app's memory, so there is nothing for Keploy to read. Keploy does not record
those bytes. It marks the gap on the connection so the protocol parser
resyncs past it (or stops on that connection) instead of reading garbage, and
it tells you once per app and cause in a warning like this:
🐰 Keploy: 2026-10-07T10:06:43.043391152Z WARN proxyless capture could not record an app's bytes: the app sent them with sendfile(2), which moves a file's pages straight into the socket — those bytes are missing from the recording; the gap is marked on the connection so its parser resyncs or stops there {"pod": "web-7c9d6b5f4-x2kqp", "process": "nginx", "pid": 4242, "connID": 2563235, "connection": "10.0.0.5:8080", "direction": "response", "lostBytes": 16384, "ktls": false, "toRecordIt": "turn sendfile off for traffic Keploy records — nginx: sendfile off; …", "docs": "https://keploy.io/docs/running-keploy/capture-loss/#sendfile"}
The fields:
pod,processandpidname the app.podis empty outside the Kubernetes DaemonSet agent.connectionis the server end of the connection: the address the app dialed, or, on a connection the app accepted, its own listening address.connIDis Keploy's ID for the connection.direction, on a TCP connection, isrequest(client to server) orresponse(server to client). On a connection using the kernel's TLS ("ktls": true), it is the app'ssentorreceived. Page references also come from UDP and Unix socket calls, andMSG_TRUNCfrom UDP; theredirectionis the call's (requestfor a send,responsefor a receive) andconnectionmay not be a server's address.lostBytesis the size of the one call that triggered the warning. Later losses are counted in the periodic warning below.toRecordItsays what to change so the bytes are recorded.docslinks the section of this page on the cause.
Every minute in which more bytes were lost, Keploy also logs a periodic warning for each cause, across all apps:
🐰 Keploy: 2026-10-07T10:07:43.043391152Z WARN proxyless capture: payloads are missing from the recordings: the app sent them with sendfile(2), which moves a file's pages straight into the socket; each was marked lost on its connection, whose parser resyncs or stops there {"newlyLost": 3, "lost": 12, "lostBytes": 196608, "toRecordIt": "turn sendfile off for traffic Keploy records — nginx: sendfile off; …", "docs": "https://keploy.io/docs/running-keploy/capture-loss/#sendfile"}
newlyLost counts the calls since the last report, and lost and lostBytes
count every call and byte since the agent started.
Find the cause and fix it
Find the words your warning uses, and go to that section:
| The warning says | Section |
|---|---|
| "the app sent them with sendfile(2)" | Turn off sendfile |
| "the app moved them from a pipe into the socket with splice(2)" | Copy through a buffer instead of splicing into the socket |
| "the app took them out of the socket into a pipe" | Copy through a buffer instead of splicing out of the socket |
| "a send or receive of the app's moved them by page references" | Use plain send and receive calls |
| "the app received them with MSG_TRUNC" | Leave MSG_TRUNC receives as they are |
| "their page was not resident to the capture hook" | Report a page-not-resident error |
| "Failed to attach do_splice_direct and do_splice", at startup | Check for unmarked losses at startup |
| "Failed to attach sock_splice_read", at startup | Check for unmarked losses at startup |
Turn off sendfile
The app sent a file's pages straight into the socket, so they were never in its memory. Turn sendfile off for the traffic Keploy records:
-
nginx:
sendfile off; -
Apache:
EnableSendfile Off -
Tomcat:
useSendfile="false"on the Connector (on by default) -
gunicorn:
--no-sendfile -
aiohttp: set
AIOHTTP_NOSENDFILE=1 -
Java: don't use
FileChannel.transferToor Netty'sFileRegionfor this traffic. -
Go, on plain TCP (
crypto/tlsnever uses sendfile). These use sendfile:http.ServeFile,http.FileServeroverhttp.Dir,http.ServeContentof an*os.File,io.Copyfrom an*os.Fileinto anet.Conn, or into anhttp.ResponseWriterwhose response is not chunked (Content-Lengthis set, or the client speaks HTTP/1.0), and anhttp.Clientrequest whoseBodyis an*os.FilewithContentLengthset.io.CopyNandio.CopyBuffertake the same path; passing your own buffer does not avoid it. Hide the socket or the file from the copy:// http.ServeFile
http.ServeFile(struct{ http.ResponseWriter }{w}, r, name)
// http.FileServer
h := http.FileServer(root)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.ServeHTTP(struct{ http.ResponseWriter }{w}, r)
})
// io.Copy from a file, into a connection or a ResponseWriter
io.Copy(struct{ io.Writer }{conn}, f)
io.Copy(struct{ io.Writer }{w}, f)
// an http.Client request body read from a file (io.NopCloser(f) alone is
// not enough: net/http unwraps it)
req.Body = io.NopCloser(struct{ io.Reader }{f})
Copy through a buffer instead of splicing into the socket
The app moved bytes from a pipe into the socket with splice(2) or io_uring's
IORING_OP_SPLICE. Copy through a buffer instead:
- Go, never on
crypto/tls:io.Copyinto a TCP connection from another TCP or Unix stream connection splices. Hide both ends from it:io.Copy(struct{ io.Writer }{dst}, struct{ io.Reader }{src}). - HAProxy: leave
option splice-*off.
Copy through a buffer instead of splicing out of the socket
The app took received bytes out of the socket with splice(2) or
sendfile(2), typically on to a file or another socket, without ever holding
them in its own memory. Copy through a buffer instead:
- Go, never on
crypto/tls:io.Copyfrom a TCP connection into another TCP or Unix stream connection, as a forwarding proxy does, or into an*os.File, splices. Hide both ends from it:io.Copy(struct{ io.Writer }{dst}, struct{ io.Reader }{src}). - HAProxy: leave
option splice-*off.
Use plain send and receive calls
The bytes moved by page references the kernel holds, not through the app's memory. This happens with io_uring registered (fixed) buffers. It also happens, from kernel 6.5, with a sendfile or splice send when Keploy could not attach its splice hooks (see Check for unmarked losses at startup).
Use plain send/recv, or io_uring without registered buffers, for the
traffic Keploy records. For sendfile or splice, turn them off as described
above.
Leave MSG_TRUNC receives as they are
The app received with MSG_TRUNC, which discards the bytes without copying
them. There is nothing to change: the app never read them, and the
connection's parser resyncs past the gap.
Report a page-not-resident error
This one is logged as an error, once per agent rather than once per app: a
page of the payload could not be read without faulting it in, which Keploy's
hook cannot do. This is a limitation of the capture, not something the
app chose. The error names no app; its fields are shard, connID,
direction (0 for request, 1 for response), lostBytes, destPort,
ktls and docs.
If it recurs, report it with the agent's logs on Slack or as a GitHub issue.
Check for unmarked losses at startup
Keploy marks sendfile and splice losses through hooks on the kernel's sendfile and splice paths. When one of them cannot attach, the agent warns once at startup, and some losses are then not marked at all, with no warning for each connection:
Failed to attach do_splice_direct and do_splice: before kernel 6.5, what sendfile and splice send goes unmarked. From 6.5, the kernel hands those bytes to Keploy's data hooks, which mark them as page references.Failed to attach sock_splice_read: what splice takes out of a socket goes unmarked, on any kernel.
If you see either, turn sendfile and splice off for the traffic Keploy records, as described above, even when no capture loss warning follows.
Know what TLS changes
TLS through a library (OpenSSL, Go's crypto/tls, Java's JSSE) encrypts in
the app's memory, so it does not take the sendfile or splice paths, unless the
library hands encryption to the kernel. OpenSSL 3 does that with its kTLS
option, for example nginx with ssl_conf_command Options KTLS;. That is the
kernel's TLS (kTLS): Keploy marks those bytes on the kTLS stream
("ktls": true), and turning sendfile off applies there too.
Understand why these bytes cannot be read
sendfile and splice move page references inside the kernel: from a file's page cache, or from a socket's receive queue, into a pipe or another socket. The bytes never pass through a buffer of the app's that Keploy's hook could read. Keploy hooks these calls on every kernel its eBPF capture runs on (x86-64 from 5.10, arm64 from 6.4). Where the hooks attach (the agent warns at startup when one does not), Keploy knows exactly which bytes were lost and on which connection, and says so instead of recording a stream with silent holes.