
SOCK_STREAM is Not TCP: Understanding Socket Types vs. Protocols
- May 18, 2026
- 9 min read
- Operating systems
Table of Contents
If you’ve written network code in C/C++ or Python, you’ve probably internalized a rule that feels almost universal:
SOCK_STREAM is commonly associated with TCP and SOCK_DGRAM with UDP.
That’s because most examples, tutorials, and even the official documentation (TCP and UDP man pages synopses) create TCP and UDP sockets like this:
1// TCP socket
2socket(AF_INET, SOCK_STREAM, 0);
3
4// UDP socket
5socket(AF_INET, SOCK_DGRAM, 0);
The only difference between the two calls is the second parameter, so it’s natural to associate SOCK_STREAM with TCP and SOCK_DGRAM with UDP.
However, that association is a convenient shortcut, not a fundamental truth.
If we make that explicit, the relationship becomes clearer:
1// TCP socket
2socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
3
4// UDP socket
5socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
When you call socket(domain, type, protocol), you are combining:
- A
domain(or address family) that defines where communication happens. - A
typethat defines how it behaves. - A
protocolthat defines how it is actually implemented.
The protocol is specified in the third parameter.
SOCK_STREAM and SOCK_DGRAM represent the socket type.
What Socket Types Actually Define
The socket type doesn’t define how data moves across the network. It defines how your program interacts with that data.
A stream socket (SOCK_STREAM) gives you a connection-oriented interface, where: bytes arrive in order and there are no message boundaries (it’s a continuous byte stream).
You read and write as if you were working with a continuous flow.
However, reliability depends on the underlying transport protocol.
This animation shows how data is sent and received at the API level. Each “send” call is represented as a separate message from the sender.
This visualization is at the socket API level and does not represent network packet-level behavior.
Here is what that looks like in code:
1// SENDER
2send(sender_socket, "Hello ", 6, 0);
3send(sender_socket, "world!", 6, 0);
4
5// RECEIVER
6// The receiver gets "Hello world!" as
7// one continuous stream, not two separate messages.
8char buffer[13] = {0};
9recv(receiver_socket, buffer, 12, 0);
10printf("%s\n", buffer); // Output: "Hello world!"
The receiving code would still read all 12 bytes of sent data even if called multiple times to receive smaller chunks as follows:
1// RECEIVER
2char buffer[13] = {0};
3recv(receiver_socket, buffer, 9, 0);
4printf("%s\n", buffer); // Output: "Hello wor"
5recv(receiver_socket, buffer, 6, 0);
6printf("%s\n", buffer); // Output: "ld!"
If recv() asks for more data than is available, it does not wait for the full amount.
It blocks only when no data is available; otherwise, it returns immediately with whatever bytes are ready.
Stream sockets don’t preserve message boundaries — partial reads are the norm, not the exception.
Note: The second recv() in the last example returns 3 bytes because that’s all that was available at the time.
In practice, this is timing-dependent: it could return fewer bytes than requested, or more if additional data has already arrived in the socket buffer.
recv() returns based on availability, not the requested size.
None of that implies TCP (although that is the way it’s most commonly implemented). It only describes behavior.
Similarly, a datagram socket (SOCK_DGRAM) gives you a message-oriented interface, where: messages may arrive out of order, may be lost, and have clear boundaries.
You read and write in discrete chunks.
Notice how the same sequence of sends is received differently here compared to the stream socket.
1// SENDER
2send(sender_socket, "Hello ", 6, 0);
3send(sender_socket, "world!", 6, 0);
4
5// RECEIVER
6// The receiver gets two separate messages: "Hello " and "world!".
7char buffer[13] = {0};
8recv(receiver_socket, buffer, 12, 0);
9printf("%s\n", buffer); // Output: "Hello "
10recv(receiver_socket, buffer, 12, 0);
11printf("%s\n", buffer); // Output: "world!"
Even though the buffer size passed to recv() is the same, the behavior is completely different from SOCK_STREAM.
If you call recv() with a size smaller than the sent message, you will only get part of the message.
Unlike stream sockets, where unread data remains available, the remaining bytes of that datagram are discarded.
Each recv() call returns at most one message.
If the buffer is larger than the datagram, recv() still returns a single message.
The extra buffer space is simply unused.
Datagram boundaries are preserved, and messages are never merged into one read.
If no datagram is available, recv() blocks until a packet arrives (or returns immediately with an error in non-blocking mode).
Note: Blocking/non-blocking is an I/O mode of the socket and applies to all socket types.
It controls whether recv() may wait for data, but it does not affect the underlying delivery semantics of the protocol.
There are other socket types, like SOCK_RAW which bypasses transport protocols entirely to give you direct access to lower-level network packets (typically IP packets).
But for most applications, the choice comes down to how you want your program to handle its data flow.
When SOCK_STREAM is Not TCP
In practice, most code looks the same:
1socket(AF_INET, SOCK_STREAM, 0);
The protocol parameter is set to 0 to select the default protocol for the given domain and type.
For the combination of AF_INET and SOCK_STREAM, the default protocol is TCP.
For the combination of AF_INET and SOCK_DGRAM, the default protocol is UDP.
The kernel fills in that choice for you, and over time, the distinction disappears from view.
Different Domain, Same Socket Type
1socket(AF_UNIX, SOCK_STREAM, 0);
From the outside, this behaves like any other stream socket. You still get ordered, reliable, connection-oriented communication. Your code still reads and writes the same way.
Underneath, everything has changed.
There is no TCP, no IP, and no concept of ports. The communication never even leaves the machine.
What you are using is a Unix domain socket, which is a completely different transport layer.
Since communication is local, the kernel bypasses most of the IP networking stack. There is no need for IP headers, routing, or TCP-specific mechanisms like congestion control and window management, which makes communication significantly simpler and faster.
The abstraction holds, but the implementation is purely a memory-to-memory copy.
A TCP connection involves retransmissions, congestion control, and routing across networks. A Unix domain socket is local intra/inter-process communication, often faster and simpler.
Different Protocol, Same Socket Abstraction
SCTP (Stream Control Transmission Protocol) is a reliable, connection-oriented transport protocol. Unlike TCP, it is fundamentally message-oriented and supports multiple independent streams within a single association (SCTP’s equivalent of a connection between two endpoints).
In the socket API, SCTP is typically used with SOCK_SEQPACKET, which preserves message boundaries:
1socket(AF_INET, SOCK_SEQPACKET, IPPROTO_SCTP);
However, RFC 6458 defines a one-to-one socket style where SCTP can, on some systems, be exposed through a SOCK_STREAM socket interface, even though it remains a message-oriented protocol internally:
1socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP);
In this case, the application observes a stream-like byte interface where message boundaries are not exposed. This does not change SCTP’s underlying semantics; it only changes how the operating system presents data to the application through the socket API.
This highlights an important distinction: the socket type defines the application-facing interface, while the protocol defines the transport behavior underneath.
By explicitly specifying IPPROTO_SCTP, SCTP is used as the underlying transport protocol, while the application interacts through a stream-oriented socket interface.
Tip
SOCK_SEQPACKET is another socket type.
It is a hybrid: connection-oriented and reliable (like SOCK_STREAM), while preserving message boundaries (like SOCK_DGRAM).
It is commonly used with SCTP and is also a standard tool for local IPC over AF_UNIX.
When SOCK_DGRAM is Not UDP
The exact same concept applies to datagrams. If you write this:
1socket(AF_UNIX, SOCK_DGRAM, 0);
You are no longer using UDP. You are using a local Unix domain datagram socket.
Because it is SOCK_DGRAM, it preserves message boundaries: if you write 100 bytes, the receiver gets exactly 100 bytes.
However, unlike UDP over the internet, a Unix domain datagram is reliable.
Because the kernel handles the transfer locally, packets can’t be dropped by a congested router or arrive out of order, though delivery can still fail if local buffers are exhausted.
In fact, if the receiver’s buffer is full, the sender will simply block or return EAGAIN—ensuring that data is never silently lost in transit.
It’s a message-oriented interface with reliable local (kernel-level) delivery, but without TCP’s end-to-end delivery guarantees.
The socket type dictates the interface (message boundaries), but the domain dictates the reality (local, reliable delivery). In other words: socket type defines behavior, protocol defines mechanics, and domain defines scope.
If you’ve seen how assumptions about address families and socket behavior can break real systems, I covered a concrete case here: It Worked Before: How an OS Upgrade Broke My Rust Sockets.
Newsletter
Subscribe to our newsletter and stay updated.
Why the Abstraction Matters
It’s easy to look at standard networking code and think the socket API is overly verbose. Why ask for domain, type, and protocol if 99% of the time developers just want TCP over IP?
The answer is future-proofing and separation of concerns.
The engineers who designed Berkeley sockets in the 1980s knew that network topologies would evolve.
By decoupling the application’s requirement (e.g., “I need a reliable stream of bytes”) from the network’s implementation (TCP, SCTP, or local IPC), they created an API that has survived for over four decades.
SOCK_STREAM does not mean TCP.
It is a contract between your application and the operating system.
The protocol determines how that contract is fulfilled.
As long as the OS fulfills the contract, the internal details of how bytes are transmitted are not the concern of the application.
This separation is what allows the socket API to remain independent of any single transport protocol.
Key Takeaways
SOCK_STREAM≠ TCPSOCK_DGRAM≠ UDP- Socket type = interface semantics (how data is exposed)
- Protocol = transport semantics (how data is transmitted)
- Domain = communication scope (where communication happens)
- Socket type (
SOCK_STREAM,SOCK_DGRAM,SOCK_SEQPACKET, etc.) is a local API abstraction and is not negotiated between peers; the client and server do not need to use the same socket type, only socket types compatible with the same underlying protocol.


