iPerf3 Errors: Bad File Descriptor and the Rest

Bad file descriptor, control socket closed, server busy. Learn which iperf3 stage failed, what the errno means, and what to check first on each one.

Real iperf3 terminal output showing connection refused, a closed control socket and a successful TCP test with the Retr column
Real runs of the commands shown: two failures from different stages, followed by a successful TCP test. The complete line matters more than the final errno alone.

You typed a command, got a line of red, and pasted it into a search box. That is how most people arrive here, and the search usually goes badly. Many iperf3 errors come from a failed system call, and those are assembled from two halves. People search for the wrong half.

The short answer

bad file descriptor is not an iperf3 error at all. It is EBADF, your operating system’s errno, appended after iperf3’s own message. It says only that the descriptor iperf3 passed to a system call was no longer valid in the local process. It does not say why, and on its own it does not prove that the peer or a firewall closed anything. The same errno turns up behind several different messages, and the message is what tells you which stage failed.

If you arrived with one specific line, jump to it:

How to read any iperf3 error

iperf3 keeps a table of its own error codes with a message for each. For codes that involve a system call, it appends whatever strerror(errno) returns after a colon. The shape, from src/iperf_error.c:

iperf3: error - unable to send control message: bad file descriptor
        └──────────────┬───────────────┘  └──────────┬─────────┘
              iperf3's own message              your OS's errno

Not every error has both halves. Some, control socket has closed unexpectedly among them, carry no errno at all, and that absence is itself informative. Where both are present they answer different questions: the left tells you which step failed, the right why the system refused. Searching the whole line finds little, because the combination is specific to your moment; searching either half alone finds too much. Read them as a pair and most errors resolve in one step.

Similar-looking errors from different stages

These are the lines people paste most often. Three of them can carry the same bad file descriptor suffix, but they come from different sockets at different points in the run:

Error stringWhere it comes fromFirst thing to check
control socket has closed unexpectedlyReading the next state byte from the control socket came back empty. No errno is appended to this one. On the client this is a plain read(), so it is a clean end-of-file from the peer. On the server the read goes through Nread, whose select() also returns zero after a 10-second wait, so the same message can follow a timeout.The other side, and the state exchange around the moment it failed.
unable to send control message: <errno>Writing to the control socket failed.What happened to the control connection between the last exchange and this one.
unable to receive cookie at server: <errno>Server side, reading the cookie from a freshly accepted data-stream socket in iperf_tcp_accept, not from the control socket. The connection was already accepted by this point, and data streams use the same port as the control connection.The appended errno and the client log. ECONNRESET means the client went away before sending the whole cookie; EBADF points at a descriptor-lifetime problem on the local side.
select failed: <errno>A descriptor in the select() set was rejected. It is not necessarily the control socket.Run with --debug to see which stage the run reached before this fired.

The distinction that matters: only the first line makes a peer close a plausible first reading, because iperf3 raises it when the read comes back with nothing. Even there, read it as “nothing arrived” rather than “the peer hung up”, since on the server a ten-second timeout produces the same message. The other three hand you the kernel’s reason instead, and EBADF among those reasons is a statement about iperf3’s own process, not about the network.

That said, a broken connection is a common way to end up there: iperf3 closes sockets during its own error handling, and a later code path can still reach for one. Walk the causes below as candidates to test, not as a ranked verdict:

A stateful firewall or NAT dropped an idle connection. The control socket goes quiet while the data streams run. A firewall that expires idle TCP flows after 60 or 120 seconds removes it silently, and a long -t 300 test fails at the end, right when iperf3 goes back to the control socket for results. Easy to confirm: run -t 30 and -t 300 back to back. Short passes and long fails is the signature.

The other side stopped. The server was killed, restarted, crashed, or its host slept. On a laptop this is often just the lid closing. If you are seeing control socket has closed unexpectedly, start here.

A mobile device went to sleep or lost the foreground. Phones suspend background sockets aggressively. A test running while the screen locks, or while you switch apps, can lose the connection with no hint that a phone was involved.

Wi-Fi roamed. Moving between access points mid-test breaks TCP flows established on the old path.

Version mismatch. An old iperf3 client against a much newer server, or vice versa, can fail during the handshake. Check iperf3 --version on both ends. iperf2 and iperf3 do not talk to each other at all, and the failure is not always polite about saying so.

Note

unable to send control message - port may not be available, the other side may have stopped running, etc. is iperf3’s fuller wording for the same code. If you see the long form, the advice above is unchanged.

the server is busy running a test. try again later

Not a bug, and not your network. iperf3 runs one test at a time per server, a deliberate difference from iperf2, which accepted parallel clients. A second client gets refused with this message.

Three things produce it:

  1. Somebody else really is testing against that server. Public iperf3 servers hit this constantly.
  2. Your own previous test did not exit cleanly, and the server still considers it live. Restart the server.
  3. You are running two tests from a script without waiting for the first to finish.

If you need concurrent tests, run several servers on different ports (-p 5202, -p 5203) and point each client at its own. If you want the server to handle exactly one test and exit, start it with -1.

unable to start listener for connections

The server could not bind its port. The errno after the colon says which flavour:

  • address already in use: something is already on port 5201. Another iperf3, or a leftover process. Find it with lsof -i :5201 or ss -lptn 'sport = :5201', or just pick another port with -p.
  • permission denied: you asked for a port below 1024 without privileges. Use a high port.
  • cannot assign requested address: the -B address you gave does not exist on this machine. Check with ip addr or ifconfig.

connection refused

The client reached the host and the host actively said no. That is a useful answer: routing works, the packet arrived, and nothing was listening on that port.

Check in this order: is the server running (iperf3 -s), is it on the port you are dialing, is a host firewall rejecting that port. On Linux, sudo ufw allow 5201/tcp or the firewalld equivalent. Contrast this with a timeout, where the packet never got an answer at all, which points at the network path or a firewall configured to drop rather than reject.

socket buffer size not set correctly

You passed -w and the kernel gave you less than you asked for. iperf3 reads SO_SNDBUF and SO_RCVBUF back and raises this only when the returned value is smaller than the requested one. Equal is fine, and so is larger.

The limit lives in the OS, not in iperf3. On Linux, net.core.rmem_max and net.core.wmem_max cap it; raise them with sysctl if you genuinely need a large window. On macOS the equivalent is kern.ipc.maxsockbuf. This is why Linux doubling the value for bookkeeping is not a problem: ask for 2 MB, get 4 MB back, and the check passes.

Before reaching for -w at all: on any modern kernel, TCP window auto-tuning beats a hand-picked value for almost every path. The usual reason to set it manually is a long-fat-network test where you have calculated the bandwidth-delay product on purpose.

unable to create a new stream: permission denied

This one is not about your -F file, which is a common guess and a wrong one. A failure opening the -F file sets a different code and prints unable to open -F file.

unable to create a new stream comes from iperf_new_stream, and it covers six operations: two allocations, then mkstemp, unlink, ftruncate and mmap on a temporary buffer file. iperf3 creates that file, immediately unlinks it, sizes it to the block size and maps it into memory. All six set the same error code, and the message does not record which syscall failed, so permission denied narrows the area rather than the line.

That area is the temporary directory, not your data file. The standard CLI picks it in this order:

  1. $TMPDIR
  2. $TEMP
  3. $TMP
  4. /tmp, or /data/local/tmp on Android

Then it creates iperf3.XXXXXX inside. Check that the directory exists and is writable by the user running iperf3, that the filesystem is not read-only, and that no sandbox stands in the way. With SELinux or AppArmor in enforcing mode, the policy can deny a path that looks perfectly writable from a shell. Pointing TMPDIR at a directory you own is the fastest way to confirm the diagnosis.

Note that a noexec mount is not the problem here: the mapping is created read-write, without an execute bit.

Programs that embed libiperf rather than shelling out to the binary can supply their own template through iperf_set_test_template(). There is no command-line flag for it.

Retr is not an error

Retr shows up in the TCP output next to bandwidth, and it looks alarming when it is not zero. It counts TCP retransmissions: segments the sender had to send again.

A handful over a long test is normal. A large and growing number means packets are being lost somewhere: a saturated link, a duplex mismatch on copper, a failing cable, or Wi-Fi. The number is a symptom, not a fault. If it is high, run a UDP test, which reports loss and jitter directly instead of hiding them behind retransmissions.

Real iperf3 TCP output with the Retr column beside bitrate and congestion window
Retr sits beside bitrate in real TCP output. Zero here is expected on loopback; on a network path, watch whether the counter stays small or keeps climbing.

When nothing errors and it still looks wrong

Two cases produce no error at all and still send people searching.

The number is far below the link speed. Check the slowest thing in the path before blaming the network. A single TCP stream on a fast link often needs -P 4 to fill it, and one end on Wi-Fi caps the whole test at the Wi-Fi rate regardless of what the wired side can do.

Upload and download disagree wildly. That is real and worth knowing. Run both directions explicitly rather than assuming symmetry: iperf3 tests one direction at a time unless you ask for -R or --bidir.

Skipping the class of errors that comes from the CLI

A fair share of what people paste into search is not a network fault at all. It is a flag typed wrong, a server left running from yesterday, a port that was fine at the office and blocked at home, or a phone that suspended the socket while the screen was off.

iPerf3 Client & Server runs the same iperf3 engine with those edges handled: the server keeps running while the app is in the foreground, the address and port are shown rather than remembered, and results are saved so a failed run is something you can compare against rather than retype. When a test does fail, the app reports it in its own terms rather than handing you an errno to search for.

Bottom line

bad file descriptor is a verdict, not an operation: it means the descriptor was no longer valid inside iperf3’s own process, and the message in front of it is what tells you which stage that happened at. control socket has closed unexpectedly is the one line with no errno, and an empty read is a peer close on the client but can be a ten-second timeout on the server. Everything in the table is a starting point for a check, not a diagnosis. Retr is not an error. And if the number is simply low with no error at all, check the slowest hop before you blame the network.