QUESTION
Is connecting only to the first getaddrinfo result sufficient?
getaddrinfo is not a DNS-only function. According to /etc/nsswitch.conf policy, it consults sources such as files, DNS, and mDNS and creates an addrinfo list matching address-family and socket-type constraints.
The returned order reflects destination-address selection policy, but the first address is not guaranteed reachable. Try the results while managing a total timeout, or parallelize IPv6 and IPv4 candidates.
STRUCTURE
Structure diagram
addrinfo results form a candidate list. If the first row fails or stalls, try another family/address within the overall deadline.
CALL PATH
Call path
Maintain name-resolution results and actual network-connection results in different caches/state. One address list does not mean one service instance.
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.
SOURCE COORDINATES
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.
| File | Function / structure | What to inspect |
|---|---|---|
| net/socket.c | __sys_connect() | Use a resolver-produced sockaddr in a real socket operation |
| net/ipv6/af_inet6.c | inet6_create(), inet6_bind() | IPv6 socket-family path |
| net/ipv4/af_inet.c | inet_create() | IPv4 socket-family path and protocol selection |
COMPLETE PROGRAM
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.
cc -std=c17 -Wall -Wextra -O2 resolve.c -o resolve01#define _POSIX_C_SOURCE 200809L
02#include <arpa/inet.h>
03#include <netdb.h>
04#include <stdio.h>
05#include <string.h>
06
07int main(int argc, char **argv)
08{
09 if (argc != 3)
10 return 2;
11 struct addrinfo hints;
12 memset(&hints, 0, sizeof(hints));
13 hints.ai_family = AF_UNSPEC;
14 hints.ai_socktype = SOCK_STREAM;
15 hints.ai_protocol = IPPROTO_TCP;
16
17 struct addrinfo *results;
18 int error = getaddrinfo(argv[1], argv[2], &hints, &results);
19 if (error != 0) {
20 fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(error));
21 return 1;
22 }
23 for (const struct addrinfo *item = results; item != NULL; item = item->ai_next) {
24 char host[NI_MAXHOST], service[NI_MAXSERV];
25 int rc = getnameinfo(item->ai_addr, item->ai_addrlen,
26 host, sizeof(host), service, sizeof(service),
27 NI_NUMERICHOST | NI_NUMERICSERV);
28 if (rc == 0)
29 printf("family=%d %s:%s\n", item->ai_family, host, service);
30 }
31 freeaddrinfo(results);
32 return 0;
33}
CODE NOTES
Code notes
memset(&hints, 0Starts unused addrinfo fields and padding at 0 so only explicitly requested constraints reach the resolver.
AF_UNSPECAllows both IPv4 and IPv6. Flags such as AI_ADDRCONFIG can reduce results according to host-interface configuration, so choose them to match requirements.
int error = getaddrinfoThe return value is an EAI_* code, not errno. Interpret it with gai_strerror; errno has additional meaning only for EAI_SYSTEM.
item = item->ai_nextWalks the full list of connectable candidates. Pass ai_addr and ai_addrlen unchanged to connect on the matching family socket.
NI_NUMERICHOST | NI_NUMERICSERVFormats only numeric addresses and ports without performing reverse DNS and service lookup again during output.
DETAILS
Detailed behavior
NSS lookup can be a blocking operation
The thread calling getaddrinfo may wait for resolver timeouts, NSS modules, and network responses. Calling it directly in an event-loop thread stops all connection handling.
Choose a dedicated resolver pool, an asynchronous extension such as getaddrinfo_a, or an application DNS client according to latency requirements.
AI_PASSIVE and wildcard addresses
For a server bind, a NULL node with AI_PASSIVE creates a wildcard address. Without AI_PASSIVE it may produce loopback. Do not mix client and server hints in the same helper.
Check IPv4-mapped behavior of an IPv6 wildcard socket against IPV6_V6ONLY settings and OS policy.
Cache TTL and connection lifetime differ
Expiration of a DNS-record TTL does not automatically move an established TCP connection to a new address. Give resolver cache, connection pool, and retry policy separate lifetimes.
Define how to drain stale pools under negative caching and deployment address rotation.
OBJECTS
Objects and lifetimes
| Object | Creation and release | Values to inspect |
|---|---|---|
addrinfo list | Allocated by getaddrinfo and released as a whole by freeaddrinfo | family, socktype, protocol, sockaddr |
NSS query state | Created per source during the resolver call and cleaned after the result/error | timeout, search domain, cache |
connection candidate | A socket/deadline is created per addrinfo entry and closed on success or failure | address, attempt time, error |
FAILURE PATH
Failure conditions and common misconceptions
| Observed symptom | Likely causes | How to verify |
|---|---|---|
| EAI_AGAIN | A transient resolver failure/timeout | Inspect NSS sources and retry budget |
| The first address stalls for a long time | Candidate connects are serial | Inspect attempt timelines by family |
| event loop stall | Blocking getaddrinfo was called in the loop thread | Inspect thread stacks and resolver latency |
LAB
Verify it yourself
- Use strace on localhost and the real hostname to compare /etc/hosts, nsswitch, and DNS syscalls.
- Compare results for AF_INET/AF_INET6/AF_UNSPEC hints and record each candidate's connect error.
- Create a resolver worker thread and result eventfd so the main epoll loop does not block.
./resolve localhost 80strace -f -e trace=openat,read,connect,sendto,recvfrom ./resolve localhost 80PRIMARY REFERENCES