Quick Answer

TCP sets up a connection and guarantees that bytes arrive, in order, retransmitting anything lost. UDP sends a datagram and forgets it. The difference that matters in practice is what happens when a packet is lost: TCP stalls everything behind the missing byte until it is resent, which is fatal for a live call but essential for a file. Choose UDP when late data is useless and TCP when incomplete data is useless.

A connection is state, not a wire

Nothing physical happens when TCP connects. No line is reserved between Pune and the server. A connection is agreed state on two machines: both ends remember sequence numbers, window sizes and the fact that the other end exists. Routers in between know nothing about it.

Creating that state costs a round trip. The client sends SYN, the server answers SYN-ACK, the client answers ACK, and only then does the first byte of your request move. On a slow mobile connection that setup is real, measurable delay before anything useful happens, which is why connection reuse and keep-alive matter so much for page speed.

UDP skips all of it. There is no handshake, no teardown and no notion of a peer. You call sendto and the datagram leaves. If the destination is switched off, nothing tells you.

import socket

# TCP: accept() returns only after a full handshake
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(('0.0.0.0', 9000))
srv.listen(5)

conn, addr = srv.accept()
print('client connected:', addr)
conn.sendall(b'hello\n')
conn.close()
import socket

# UDP: no listen, no accept, no connection at all
srv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
srv.bind(('0.0.0.0', 9001))

data, addr = srv.recvfrom(2048)
print('datagram from', addr, ':', data)
srv.sendto(b'hello', addr)

Notice what is missing on the UDP side. There is no listen and no accept, because there is nothing to accept. The practical consequence is that a UDP service can never tell you a client has disconnected. If your protocol needs to know, you build heartbeats and timeouts yourself.

What reliable actually means, and where it stops

TCP reliability is four mechanisms working together, and naming them is a much better interview answer than the word reliable.

Sequence numbers label every byte, so the receiver can put segments back in order and discard duplicates. Acknowledgements tell the sender how far the receiver has got. Retransmission resends anything that is not acknowledged before a timer expires, or sooner if duplicate ACKs suggest a gap. Flow control uses the advertised window so a fast sender cannot drown a slow receiver, and congestion control slows the sender down when the network starts dropping packets.

Now the part that surprises people. TCP guarantees delivery to the peer's operating system, not to the peer's application. When sendall returns, all you know is that the local kernel accepted your bytes into a buffer. The peer kernel may acknowledge a segment, and the process that was supposed to read it may then crash before it ever calls recv. TCP considers that a success. Your data is gone and nothing reports an error.

This is why anything that matters carries an application-level acknowledgement. A payment service does not consider a charge confirmed because the socket write succeeded. It waits for a response body that says so. If you are building a job queue or a webhook receiver, the same rule applies: transport-level delivery is not business-level delivery.

One more limit worth knowing. The TCP checksum is only 16 bits, so it catches ordinary corruption but is not a strong integrity check. That is why large downloads publish a SHA-256 digest alongside the file, and why TLS adds its own authentication of every record.

TCP is a byte stream, not a message queue

This is the failure that actually breaks student projects, and it has nothing to do with packet loss.

TCP delivers a stream of bytes. It does not preserve the boundaries of your send calls. Two sends can arrive as one recv, or one send can arrive split across three reads. On localhost, where everything is fast and unfragmented, this almost never happens, so the bug ships.

import json

# sock on each side is an already connected TCP socket

# sender
sock.sendall(b'{"cmd": "login"}')
sock.sendall(b'{"cmd": "fetch"}')

# receiver
data = sock.recv(4096)
msg = json.loads(data)   # works on localhost, fails in production

In production that recv may return both JSON objects concatenated, which json.loads rejects, or half of the first object, which it also rejects. The fix is framing: put a length in front of every message and read exactly that many bytes.

import struct

def send_msg(sock, payload: bytes) -> None:
    sock.sendall(struct.pack('!I', len(payload)) + payload)

def recv_exact(sock, n: int) -> bytes:
    buf = b''
    while len(buf) < n:
        chunk = sock.recv(n - len(buf))
        if not chunk:
            raise ConnectionError('peer closed early')
        buf += chunk
    return buf

def recv_msg(sock) -> bytes:
    length = struct.unpack('!I', recv_exact(sock, 4))[0]
    return recv_exact(sock, length)

HTTP solves the same problem with Content-Length or chunked encoding. Every real TCP protocol has an answer to it, because there is no way around it.

UDP behaves differently and this is a genuine advantage. One sendto equals one recvfrom, so message boundaries are preserved for free. What you lose is the guarantee that the datagram arrives at all, arrives once, or arrives in the order you sent it. Keep datagrams small as well: on a typical 1500-byte path, a payload beyond roughly 1472 bytes gets fragmented at the IP layer, and losing any one fragment discards the whole datagram.

Overhead, and the stall nobody expects

Header size is the obvious cost. A minimal IPv4 header is 20 bytes. A minimal TCP header is another 20 bytes and is often larger once options such as timestamps and selective acknowledgement are present. A UDP header is 8 bytes. For a multiplayer game sending a 16-byte position update many times a second, the headers rather than the payload dominate every packet, so trading TCP's 20 bytes for UDP's 8 is a real saving at that rate.

The less obvious cost is head-of-line blocking, and it is the real reason live applications avoid TCP.

TCP promises in-order delivery to your application. Suppose segments 1 to 20 are sent and segment 5 is lost. Segments 6 to 20 arrive fine and sit in the receiver's buffer, but your application sees nothing, because handing over segment 6 would break the ordering guarantee. Everything waits for segment 5 to be retransmitted, which takes at least one more round trip.

For a file download that is exactly what you want. For a voice call it is a freeze, and worse, the retransmitted audio is useless by the time it arrives because that moment of the conversation has passed. UDP hands segments 6 to 20 straight to the application, which conceals the gap by interpolating or simply playing on.

This same problem is why HTTP/3 moved to QUIC, which runs over UDP and reimplements reliability per stream. Under HTTP/2 on TCP, one lost packet stalls every parallel request sharing that connection, even though the requests are independent. QUIC keeps streams separate so a loss affecting one image does not hold up the CSS. It also folds the transport and TLS handshakes together, cutting a round trip from connection setup.

None of this makes UDP faster in a simple sense. A UDP transfer that has to reimplement retransmission ends up doing the same work TCP already does, usually less well. The gain comes only when you can genuinely afford to drop data.

Which applications use which, and why

The deciding question is short: is late data still useful? If yes, use TCP. If no, use UDP.

Uses UDP:

  • DNS queries. A query and its answer usually fit in one small datagram. Retrying a lost query is cheaper than paying for a handshake first. Large responses and zone transfers fall back to TCP.
  • DHCP. The client has no IP address yet, so it must broadcast. TCP cannot help you before you have an address.
  • Voice and video calls, including WebRTC. A frame that arrives 400 ms late is worse than no frame.
  • Multiplayer game state. Position update 41 supersedes update 40, so resending 40 is pointless.
  • NTP and QUIC, which carries HTTP/3.

Uses TCP:

  • HTTP/1.1 and HTTP/2, so every REST API and every normal web page.
  • SSH, SMTP, IMAP and FTP.
  • Database clients: MySQL, PostgreSQL, MongoDB, Redis. A truncated query result is worthless.
  • Any file transfer, because a file with a hole in it is not a file.

One popular claim deserves correcting. People say streaming video uses UDP. Recorded video on the large streaming platforms is normally delivered over HTTPS in chunks, so over TCP or QUIC, because the player holds a buffer of several seconds and can afford a retransmission. Live conferencing has no such buffer, so it uses UDP. Same media type, opposite choice, decided entirely by how much delay the application can absorb.

For placement answers, avoid saying UDP is unreliable as though packets are randomly discarded on purpose. UDP simply has no retransmission machinery. On a healthy local network, UDP loss is often negligible.

Frequently Asked Questions

Is UDP always faster than TCP? It has lower overhead and no handshake, so the first byte can leave sooner and the headers are smaller. But if your application then adds its own acknowledgements and retransmissions on top of UDP, you have rebuilt TCP badly and it will usually perform worse. UDP wins when you can genuinely discard lost data, not merely because it skips a handshake.
Can I make UDP reliable? Yes, and that is exactly what QUIC does. You add sequence numbers, acknowledgements, retransmission timers and congestion control in userspace. The reason to do it is control, such as per-stream reliability or handshake changes, not simplicity. For most applications the correct move is to use TCP or an existing library rather than write this yourself.
Why does my socket code work locally but fail on a server? Almost always the byte-stream problem. On localhost, one send tends to arrive as one recv, so code that assumes message boundaries appears to work. Across a real network the same bytes get split or merged. Add explicit framing with a length prefix or a delimiter, and read exactly as many bytes as the frame declares.
Does HTTP always run over TCP? HTTP/1.1 and HTTP/2 run over TCP. HTTP/3 runs over QUIC, which runs over UDP. The HTTP semantics of methods, headers and status codes are identical across all three, so your application code does not change. Only the transport underneath differs, mainly to avoid the head-of-line blocking that a single TCP connection imposes.
How do I check whether a service is listening on TCP or UDP? On Linux use ss -tulpn, where t is TCP and u is UDP. On Windows use netstat -ano -p tcp and netstat -ano -p udp. Remember that a UDP port showing as open tells you very little, because there is no handshake to confirm, which is also why UDP port scanning is unreliable.