Why shutdown() Exists
The regular close() call closes both the reading and writing channels of a socket at once — a full-duplex close. But sometimes you want more control. What if you want to say “I’m done sending, but I can still receive”? That is exactly what shutdown() lets you do.
Imagine you are copying a file over a network. Once you finish sending, you want to signal “no more data from me” but still wait for the server to send back a checksum or confirmation. With close() you would kill both directions. With shutdown(SHUT_WR) you close only the write side while keeping the read side open.
Key Terms to Know
#include <sys/socket.h>
int shutdown(int sockfd, int how);
/* Returns 0 on success, -1 on error */
The how parameter controls which direction gets closed:
| how value | Meaning | TCP Effect | Use Case |
|---|---|---|---|
SHUT_RD |
Close read side only | No TCP FIN sent. Behavior varies by OS. Avoid for TCP. | UNIX domain sockets only |
SHUT_WR |
Close write side only | Sends FIN to peer. Initiates active close. Read side stays open. | Done sending, still need to receive |
SHUT_RDWR |
Close both sides | Sends FIN. Similar to active close via close(), but works even if multiple FDs refer to the socket. | Force close regardless of dup() copies |
- Decrements file descriptor reference count
- Only sends FIN when count reaches 0
- If dup() or fork() was used, FIN may NOT be sent yet
- Both read and write channels closed
- Ignores file descriptor reference count
- Sends FIN immediately regardless of dup()/fork()
- Peer sees EOF on their read side
- You can still read() data that peer sends back
Critical difference with dup() / fork(): If you call dup(sockfd) to create a second file descriptor pointing to the same socket, then calling close() on the original does NOT send FIN — the FD count just drops from 2 to 1. The socket stays open. But shutdown() sends the FIN immediately regardless of how many FDs exist for that socket.
When you call shutdown(sockfd, SHUT_WR):
- TCP sends a FIN segment to the peer
- The peer’s
read()returns 0 (EOF) — they know you are done sending - Your local TCP moves into FIN_WAIT1, then FIN_WAIT2
- The peer moves into CLOSE_WAIT
- You can still call
read()and receive data the peer sends before it also closes
shutdown(sockfd, SHUT_RD) is supposed to close only the read side of a TCP socket. In theory, after calling it, your read() calls should return EOF. But the actual behaviour is inconsistent across operating systems and the TCP spec does not define it clearly.
| OS / Behaviour | What happens after SHUT_RD on TCP |
|---|---|
| Linux | read() returns 0 (EOF) after existing data is drained. BUT: if peer continues to write, you can still read that data on Linux! No FIN is sent to the peer. |
| BSD (macOS, FreeBSD) | read() always returns 0. But if peer keeps writing, the receive buffer fills up, and peer’s write() eventually blocks. No FIN sent. |
| UNIX domain sockets | Peer gets SIGPIPE and EPIPE if it writes after you call SHUT_RD. This is the one case where SHUT_RD behaves predictably. |
SHUT_RD with TCP sockets in portable programs. The behaviour varies and is not well-defined by TCP. Use it only with UNIX domain sockets where it works as expected.This is a classic pattern used in programs like netcat — read from stdin and send to server, then when stdin reaches EOF, signal the server you are done sending but still receive the server’s response.
half_close_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT 9090
#define BUF_SIZE 1024
int main() {
int sock_fd;
struct sockaddr_in addr;
char buf[BUF_SIZE];
int n;
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
if (connect(sock_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("connect failed");
exit(1);
}
printf("Connected to server.\n");
/* Send some data */
const char *msg = "GET /index.html HTTP/1.0\r\n\r\n";
write(sock_fd, msg, strlen(msg));
printf("Sent request to server.\n");
/*
* HALF-CLOSE: We are done sending.
* Sends FIN to server — server's read() returns EOF.
* But we keep our read side open to receive the response.
*/
if (shutdown(sock_fd, SHUT_WR) == -1) {
perror("shutdown SHUT_WR failed");
exit(1);
}
printf("Sent FIN (SHUT_WR). Still listening for server response...\n");
/*
* Now read whatever the server sends back.
* This works because our read side is still open.
*/
while ((n = read(sock_fd, buf, BUF_SIZE - 1)) > 0) {
buf[n] = '\0';
printf("From server: %s", buf);
}
if (n == 0) {
printf("\nServer closed connection (EOF). Done.\n");
} else {
perror("read error");
}
/* Full close — both sides now */
close(sock_fd);
return 0;
}
What this demonstrates:
Client Server
------ ------
connect() ──────────────────────> accept()
write(request) ─────────────────> read()
shutdown(SHUT_WR) ───FIN────────> read() returns 0 (EOF)
Server knows client is done sending
<───data──── write(response)
read(response) ◄────────────────
<───FIN───── close()
read() returns 0 (EOF)
close()
Demonstration: close() vs shutdown() with dup()
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void demo_close_vs_shutdown(int sock_fd) {
/* dup() creates a second file descriptor for same socket */
int dup_fd = dup(sock_fd);
printf("Original fd: %d, Dup fd: %d\n", sock_fd, dup_fd);
/*
* close(sock_fd) here:
* - Decrements reference count from 2 to 1
* - NO FIN is sent because dup_fd still refers to socket
* - The peer does NOT see EOF
*/
close(sock_fd);
printf("close(sock_fd): FD count is now 1. No FIN sent yet.\n");
/*
* shutdown(dup_fd, SHUT_WR) here:
* - Ignores reference count entirely
* - FIN is sent immediately
* - Peer sees EOF NOW
*/
shutdown(dup_fd, SHUT_WR);
printf("shutdown(dup_fd, SHUT_WR): FIN sent regardless of dup.\n");
close(dup_fd); /* Final cleanup */
}
When you are done with the socket completely — no more reading or writing. This is the normal case for simple clients and servers.
When you need to signal “done sending” (send EOF / FIN) but still need to receive the peer’s response. Also use when dup() or fork() was used and you need FIN sent immediately.
When you want to forcibly close a socket and send FIN immediately, even if other FDs (from dup/fork) refer to it. Unlike close(), this triggers FIN regardless of refcount.
Behaviour is undefined and OS-dependent for TCP. Only use with UNIX domain sockets where it works consistently.
Q1. What is the difference between close() and shutdown() on a socket?
close() decrements the file descriptor’s reference count and only sends FIN when the count reaches zero. shutdown() ignores the reference count and immediately affects the socket’s communication channel. close() always closes both directions; shutdown() can close just one direction (half-close).
Q2. If you call dup(sock_fd) and then close(sock_fd), does the peer receive a FIN?
No. dup() creates a second file descriptor pointing to the same socket object. The reference count becomes 2. close(sock_fd) drops it to 1. Since the count is not zero, TCP does not send FIN. The peer does not see EOF. To force FIN immediately, use shutdown(sock_fd, SHUT_WR).
Q3. What happens on the peer side when you call shutdown(SHUT_WR)?
A FIN segment is sent to the peer. The peer’s read() call returns 0 (EOF), signalling that no more data will arrive from your side. However, the peer can still send data back to you because only the write half of your connection was shut down.
Q4. Why is shutdown(SHUT_RD) unreliable for TCP?
TCP has no protocol mechanism to tell the peer “stop sending”. SHUT_RD only affects the local socket’s receive behaviour. On Linux, read() returns EOF but the peer can still send data and it can still be read. On BSD systems, the buffer fills and the peer’s writes eventually block. No RST or FIN is sent. The behaviour is implementation-defined, making it non-portable.
Q5. Describe a real-world use case for shutdown(SHUT_WR).
The classic use case is an HTTP/1.0 client. The client sends a complete HTTP request, then calls shutdown(SHUT_WR) to signal the end of the request (server gets EOF). The client then reads the full HTTP response. Without shutdown(), you would have to either close the socket entirely (losing the response) or implement your own end-of-request signalling in the application protocol.
Q6. After calling shutdown(SHUT_WR), can you still call write() on the socket?
No. Calling write() after shutdown(SHUT_WR) will return -1 with errno set to EPIPE, and the process will also receive a SIGPIPE signal (which by default terminates the process). You must handle or ignore SIGPIPE if you want the error returned rather than a signal.
Next: Why TIME_WAIT exists and what the 2MSL timer does
