Socket · Linux userspace / kernel ABI

Nonblocking connect and SO_ERROR

Explains why writability after EINPROGRESS does not mean only success, and how getsockopt(SO_ERROR) consumes final connection state.

Series
32 / 38
Build
cc -std=c17 -Wall -Wextra -O2 nb_connect.c -o nb_connect
Run
./nb_connect 127.0.0.1 8080
Kernel
Linux 6.18.37 LTS

When EPOLLOUT arrives, has TCP connect succeeded?

When connect on a nonblocking socket returns EINPROGRESS, the handshake is in progress. On completion, the socket can report writable/error events, but both success and failure are wakeup causes, so read SO_ERROR to determine the final result.

A connect timeout is a policy choice between waiting for all kernel TCP retransmissions and stopping at an application deadline. Preserve a monotonic deadline instead of resetting a relative epoll_wait timeout after every EINTR.

Structure diagram

Figure 1. Nonblocking-connect states and values that must be checked
observationsocket stateSO_ERRORnext action connect() = 0ESTABLISHED0proceed directly to protocol stageerrno=EINPROGRESSSYN-SENTstill undecidedwait for POLLOUT/ERRPOLLOUTcomplete or failmust read0 means successPOLLERR/HUPpending errorECONNREFUSED, etc.fd close/retrydeadline expiresstill in progressirrelevantcancel with close

POLLOUT is not a success decision. Read SO_ERROR in each state to finalize the connection object's result.

Call path

Figure 2. From userspace code to observable results
socket nonblock include CLOEXEC
connect 0 or EINPROGRESS
epoll POLLOUT completion possible
SO_ERROR 0/ECONNREFUSED, etc.
connected I/O change interest

Treat a writable event as a request to recheck state, not as the result. Mark connection state established or failed only after reading SO_ERROR.

Figure 3. Major points along the kernel-internal path
tcp_v4_connect send SYN / enter state
sk_sleep poll wait queue
handshake/error sk_state/sk_err
sock_poll writable/error
SO_ERROR error read-and-clear

This diagram is not for memorizing function names. Follow which return value, file descriptor, memory mapping, or wait queue is passed to the next stage.

Linux 6.18.37 LTS source locations

Go beyond the glibc function to the files where the syscall implementation meets kernel objects. Each link points to the original file at the same tag.

FileFunction / structureWhat to inspect
net/ipv4/tcp_ipv4.c tcp_v4_connect() Begin route lookup, local-port selection, SYN, and TCP state
net/core/sock.c sock_getsockopt() Semantics that read and clear SO_ERROR
net/socket.c sock_poll() Convert socket state to a poll readiness mask

Complete runnable example

The code below is not pseudocode with explanatory lines omitted. It is a minimal example that can be built and run as a file.

Buildcc -std=c17 -Wall -Wextra -O2 nb_connect.c -o nb_connect
01#define _GNU_SOURCE
02#include <arpa/inet.h>
03#include <errno.h>
04#include <poll.h>
05#include <stdio.h>
06#include <stdlib.h>
07#include <sys/socket.h>
08#include <unistd.h>
09
10int main(int argc, char **argv)
11{
12    if (argc != 3)
13        return 2;
14    int fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
15    struct sockaddr_in peer = {
16        .sin_family = AF_INET,
17        .sin_port = htons((unsigned short)strtoul(argv[2], NULL, 10))
18    };
19    if (fd < 0 || inet_pton(AF_INET, argv[1], &peer.sin_addr) != 1)
20        return 1;
21
22    int rc = connect(fd, (struct sockaddr *)&peer, sizeof(peer));
23    if (rc < 0 && errno != EINPROGRESS)
24        return 1;
25    if (rc < 0) {
26        struct pollfd pfd = { .fd = fd, .events = POLLOUT };
27        if (poll(&pfd, 1, 3000) != 1)
28            return 1;
29        int error = 0;
30        socklen_t length = sizeof(error);
31        if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &length) < 0 || error != 0) {
32            errno = error;
33            perror("connect completion");
34            return 1;
35        }
36    }
37    puts("connected");
38    close(fd);
39    return 0;
40}

Code notes

Source line 14SOCK_NONBLOCK | SOCK_CLOEXEC

Creates an fd with nonblocking and CLOEXEC already set at connect time, avoiding an fcntl race with other threads.

Source line 23errno != EINPROGRESS

Only the normal in-progress result moves to event waiting. Handling EALREADY/EISCONN also belongs in a state machine that may call connect again.

Source line 26.events = POLLOUT

When connect completes, the socket becomes writable/error-ready because sending is possible or an error is pending.

Source line 31SO_ERROR

Reads and clears the pending error stored on the socket. 0 means connect succeeded; a positive errno means failure.

Source line 32errno = error

SO_ERROR is an output integer, not errno from the getsockopt return; copy it to errno before passing it to perror.

Detailed behavior

01

Trying several addresses only in sequence is slow

If IPv6 from getaddrinfo times out because of a path problem before IPv4 is attempted, user-visible delay grows. Happy Eyeballs-family algorithms stagger parallel connects across address families and select the first success.

Maintain each candidate socket and timer as independent state, and close loser fds.

02

Manage deadlines and fd readiness together

The poll 3000ms example does not compute remaining time after signal EINTR. A production loop establishes an absolute CLOCK_MONOTONIC deadline and handles an epoll timerfd with connection events.

At timeout, close the socket to cancel the handshake and increment the connection-object generation.

03

Protocol setup remains after connect succeeds

TCP established does not mean a TLS handshake or application greeting is complete. Split the state machine into CONNECTING, TLS_HANDSHAKE, READY, and similar states, updating read/write interest at each stage.

EPOLLOUT can remain level-ready while the send buffer has capacity, so remove the interest when there is no data to send.

Objects and lifetimes

ObjectCreation and releaseValues to inspect
connecting socketCreated by socket/connect and retained until close after success/failure/timeoutpeer, deadline, state
sk_errSet by an asynchronous network error and consumed by reading SO_ERRORerrno value, clear semantics
event registrationAdded after EINPROGRESS and changed after the completion decisionPOLLOUT/ERR, generation

Failure conditions and common misconceptions

Observed symptomLikely causesHow to verify
send fails despite EPOLLOUTThe connect error was not checked through SO_ERRORInspect the getsockopt result
The timeout grows longerA relative timeout is reused after every EINTRmonotonic deadline
An IPv6 problem delays the entire connectionAddresses fall back seriallyInspect the connect timeline for each candidate

Verify it yourself

  1. Connect once to a port with a listener and once to a closed port, and compare SO_ERROR 0 with ECONNREFUSED.
  2. Apply a 1-second application deadline to an address dropped by a firewall and cancel before the kernel's default retransmission completes.
  3. Implement a small Happy Eyeballs connector that manages several getaddrinfo results nonblockingly at the same time.
Run./nb_connect 127.0.0.1 8080
Tracestrace -e trace=socket,connect,poll,getsockopt,close ./nb_connect 127.0.0.1 8080

Primary sources