skip to content

Wire-Level Networking

Below every HTTP client sits a socket, a readiness check and a timeout somebody forgot to set. Interviewers ask for a small TCP server, or why a client hangs forever, to see how far down you can go.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

12

Which socket.socket calls set up a listening TCP server, and what does the client call?

level: juniorimportance: must knowfreq 52%

answer

  1. Two sockets are involved, not one
  2. One socket only waits; another one talks
  3. Passive side needs three calls before data
  4. bind, then listen, then accept in a loop
  5. create_server collapses the three since 3.8

basics

~20 s

The server creates a socket, binds it to a local address, calls listen, then blocks in accept, which returns a new socket for one client; only that socket carries data. The client creates its own socket and calls connect.

solid answer

~40 s

`socket.socket(socket.AF_INET, socket.SOCK_STREAM)` creates an IPv4 TCP socket. On the server you `bind((host, port))`, then `listen(backlog)` to turn it into a passive listener, then loop on `accept()`, which blocks and returns `(conn, addr)`. `conn` is a **different**, connected socket for that one client; the listening socket never carries data. The client creates its own socket and calls `connect((host, port))` — it does not bind, the kernel picks an ephemeral source port. Close each `conn` when the exchange is over; sockets are context managers, so `with conn:` is idiomatic. Since Python 3.8, `socket.create_server(("127.0.0.1", 8080))` does socket creation, `SO_REUSEADDR` on POSIX, `bind` and `listen` in one call, and `socket.create_connection(addr, timeout=5)` is the client-side equivalent that resolves the name and tries each address.

code

python · 8 lines
python
import socket

srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", 0))
srv.listen(128)
print("listening on", srv.getsockname())
srv.close()

go deeper

for a junior

Be ready to write the five-line server from memory: socket, bind, listen, accept, then recv and sendall on the socket accept returned. Say out loud that accept hands back a new socket.

for a middle

Explain what bind and listen change about the socket's state, what the backlog queue holds, and why the accept loop is serial until you hand the connection to a thread or process. Know that create_server exists and what it sets for you.

for a senior

Show the operational details: SO_REUSEADDR before bind, closing every accepted socket so descriptors do not leak, binding to loopback rather than every interface unless exposure is intended, and using port 0 plus getsockname in tests.

for a principal

Own the decision of whether hand-written sockets belong in the codebase at all. Weigh a raw listener against an existing server framework, and set the standard for how connection lifetimes, limits and shutdown are handled across services.

A TCP server in Python involves **two distinct sockets**, and confusing them is the classic first mistake. **The listening socket.** `srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)` asks for an IPv4 (`AF_INET`) stream socket (`SOCK_STREAM`, which for `AF_INET` means TCP). It is unbound and useless until you give it a local address: `srv.bind(("127.0.0.1", 8080))`. For `AF_INET` that address is always a `(host, port)` tuple. Which host you pick is a real decision, not boilerplate: `"127.0.0.1"` accepts only connections originating on the same machine, while `""` or `"0.0.0.0"` accepts on every interface and therefore exposes the service to whatever network the box is on. Port `0` asks the operating system to allocate a free port, which you read back with `srv.getsockname()` — the standard trick for tests, since it removes port collisions between parallel runs. `srv.listen(backlog)` flips the socket from active to passive. The kernel now completes TCP handshakes on the socket's behalf and queues the finished connections. `backlog` bounds that queue — `socket.SOMAXCONN` is the platform maximum — and it is *not* a cap on concurrent clients, only on how many completed connections may sit un-accepted before the kernel starts refusing or dropping new ones. **The connected socket.** `conn, addr = srv.accept()` blocks until a connection is queued, removes one, and returns a **new** socket object connected to that peer plus the peer's `(host, port)`. All application data flows over `conn`. The listening socket is never read from or written to; calling `recv` on it is an error, not a subtle bug. So every blocking server has this shape: ```python while True: conn, addr = srv.accept() with conn: handle(conn) ``` and serving more than one client at a time means handing `conn` to a thread, a process, or a readiness loop — the accept loop itself is strictly serial. **The client.** The client creates its own socket and calls `connect(("127.0.0.1", 8080))`. It normally does not `bind`: the kernel assigns an ephemeral source port automatically. Once `connect` returns, the client socket and the server's `conn` are symmetric peers — either side may `send` and `recv` in any order. **Closing.** `close()` releases the underlying file descriptor. Since Python 3.2 a socket is a context manager, so `with conn:` closes it even when the handler raises. Leaked descriptors are a genuine production failure mode: the process eventually hits its file-descriptor limit and `accept` starts failing for every client at once. When you want to say "I am done sending" while still reading the peer's reply, `conn.shutdown(socket.SHUT_WR)` sends a FIN without destroying the object, and the peer's next `recv` returns `b""`. **Address already in use.** Restart a server that had live connections and `bind` often fails with an `OSError` reading "Address already in use", because the old connections linger in TCP's `TIME_WAIT` state. Setting `srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)` **before** `bind` tells the kernel to permit the bind anyway. It is standard on every long-lived server, and it is not the same thing as `SO_REUSEPORT`, which lets several sockets share one port for load balancing. **The shortcuts.** Python 3.8 added `socket.create_server(address, *, family=socket.AF_INET, backlog=None, reuse_port=False, dualstack_ipv6=False)`, which performs socket creation, `SO_REUSEADDR` on POSIX platforms, `bind` and `listen` in a single call, and with `dualstack_ipv6=True` gives one listener serving both IPv4 and IPv6. On the client side, `socket.create_connection(address, timeout=None, source_address=None)` resolves the host name, tries each returned address in turn, and sets the timeout on the socket before connecting. Prefer both to hand-rolled sequences: they encode the options people forget. Since 3.11, `socket.create_connection(..., all_errors=True)` raises an `ExceptionGroup` carrying every failed attempt rather than only the last error, which is far better for diagnosing a host whose IPv6 address is unreachable while its IPv4 address works. Finally, note what this layer does *not* give you. `accept` hands back a byte pipe. There are no requests, no responses, no message boundaries and no encoding — those are your protocol's job, built on top of `send` and `recv`. **A word on the address family.** `AF_INET` is IPv4 and its address is a `(host, port)` tuple; `AF_INET6` is IPv6 and its address is a four-tuple. A server bound only to `AF_INET` is invisible to clients that resolve your name to an IPv6 address, which is a surprisingly common cause of "it works from my machine but not from the cluster". `SOCK_STREAM` selects TCP for both families; `SOCK_DGRAM` would give you UDP, which has no `listen` or `accept` at all because there is no connection to accept — a useful contrast for remembering what the three server calls are actually for.

  • What does the backlog argument to socket.socket.listen actually control?
    It bounds the kernel's queue of connections that have completed the TCP handshake but that your code has not yet accepted. It is not a concurrency limit: once you accept a connection it leaves the queue regardless of how long you then spend serving it. When the queue is full the kernel refuses or drops new connection attempts, which clients see as a refused connection or a hang. `socket.SOMAXCONN` is the platform maximum, and `socket.create_server` uses a sensible default when you pass `backlog=None`.
  • Why does bind fail with "Address already in use" right after you stop and restart a server, and what removes that?
    Connections that were open when the process died sit in TCP's `TIME_WAIT` state, still holding the local address. Setting `sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)` before `bind` lets the kernel bind anyway; `socket.create_server` does this for you on POSIX platforms. It is distinct from `SO_REUSEPORT`, which allows several live sockets to share the same port and have the kernel distribute connections between them.
  • What is the difference between calling shutdown and calling close on a connected socket?
    `close()` drops your reference to the file descriptor and, once the last reference goes, tears the connection down. `shutdown(socket.SHUT_WR)` only sends a FIN for the write direction: you can still read, and the peer's next `recv` returns `b""` so it knows your message is complete. The half-close is how a request/response protocol says "that was all of my request" without discarding the reply; you still call `close()` afterwards.

The listening socket is a receptionist desk: it never has the conversation, it only hands each arriving visitor a private meeting room, and the room is the connected socket.

saying these in an interview costs you the question

  • Thinks accept returns the same socket you bound
  • Calls recv or send on the listening socket
  • Skips listen and calls accept straight after bind
  • Believes the client must bind before connect
  • Treats backlog as the maximum number of clients
  • Never closes accepted sockets, leaking descriptors

context

open as a page

Why does selectors.EVENT_WRITE fire on nearly every loop pass, and when should you register for it?

level: middleimportance: must knowfreq 45%

basics

~20 s

A socket is writable whenever its kernel send buffer has room, which is almost always. Register selectors.EVENT_WRITE only while unsent bytes are actually queued for that socket; otherwise select() returns instantly every pass and the loop spins at full CPU.

open as a page

Why can socket.socket.recv return only part of the message the peer sent, and what fixes it?

level: middleimportance: must knowfreq 62%

basics

~20 s

TCP is a byte stream with no message boundaries, so recv returns whatever has arrived, up to the size you asked for. Frame messages yourself with a length prefix or a delimiter, and loop until one message is complete.

open as a page

What is the difference between urllib.error.HTTPError and urllib.error.URLError?

level: middleimportance: must knowfreq 55%

basics

~20 s

HTTPError means the server answered with a status urllib treats as an error; URLError means no HTTP answer arrived at all, such as a name-resolution or connection failure. HTTPError is a subclass of URLError, so catch it first.

open as a page

What does selectors.DefaultSelector let one thread do that a blocking socket.recv() cannot?

level: juniorimportance: should knowfreq 35%

basics

~20 s

It asks the kernel which of many registered sockets are ready right now, so one thread can serve thousands of connections. A blocking socket.recv() parks that thread on one connection until that single connection has data.

open as a page

Using urllib.request, how do you send a POST with custom headers and a body?

level: juniorimportance: should knowfreq 45%

basics

~20 s

Build a urllib.request.Request whose data argument is a bytes body and whose headers argument is a dict, then hand that Request to urlopen. Supplying data switches the method from GET to POST; a str body raises TypeError.

open as a page

How does asyncio's selector-based event loop use the selectors module underneath?

level: middleimportance: should knowfreq 40%

basics

~20 s

It owns a selectors.DefaultSelector, registers each managed socket for EVENT_READ or EVENT_WRITE, and calls select() with a timeout set by its nearest scheduled timer. Ready events become queued callbacks that the same single thread then runs.

open as a page

What does socket.socket.send return, and why does sendall exist?

level: middleimportance: should knowfreq 55%

basics

~20 s

socket.send hands bytes to the kernel's send buffer and returns how many it actually accepted, which can be fewer than you passed. socket.sendall loops internally until every byte is queued, returns None, and raises if the connection fails.

open as a page

How do you stop urllib.request.urlopen from following a redirect?

level: middleimportance: should knowfreq 35%

basics

~10 s

Subclass urllib.request.HTTPRedirectHandler so its redirect_request returns None, pass the subclass to build_opener, and call open on that opener. With no handler willing to redirect, the 3xx surfaces as an HTTPError carrying the Location header.

open as a page

Your selectors-based webhook receiver leaks memory across a 340-case replay pack. How do you find the cause?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Measure the selector's registration count first. If it never returns to its idle value, connections are being abandoned without unregister() and close(). If it stays flat, the growth is in the per-connection buffers you attached at registration.

open as a page

Which socket timeout controls stop a blocking socket.socket.recv from wedging a worker forever?

level: seniorimportance: should knowfreq 50%

basics

~10 s

Call settimeout(seconds) so recv raises TimeoutError instead of blocking indefinitely. settimeout(None) restores plain blocking mode, and settimeout(0) or setblocking(False) makes calls raise BlockingIOError at once. socket.setdefaulttimeout sets the default for sockets created afterwards.

open as a page

A metrics scraper passes timeout=2 to urllib.request.urlopen yet a call still runs for minutes. What does that timeout actually bound?

level: seniorimportance: should knowfreq 40%

basics

~20 s

It is a socket timeout applied to each blocking operation — the connect and every individual read — not a deadline for the whole exchange. A target trickling bytes resets it indefinitely, so a total budget has to be enforced outside the call.

open as a page