Filter concepts by levelShowing all levels.

Python · Section 35

Networking

Level
intermediate
Read
140 min
Concepts
9

The TCP/IP stack underneath every Python network call: addressing (IP addresses, ports, TCP vs UDP), sockets, DNS, where HTTP/HTTPS/TLS sit in the layering, the infrastructure that sits between client and server (reverse proxies, load balancers, proxies, NAT), connection lifecycle management (pooling, timeouts), and the two ways to push data from server to client (WebSockets, SSE).

Python overview

What is true here

  1. An IP address routes to a HOST; a port routes to a PROCESS on that host — a service is really the (IP, port) pair together.
  2. TCP sets up a connection and guarantees ordered delivery; UDP sends immediately with no guarantees — choose UDP only when a late packet is worse than a lost one.
  3. HTTPS inserts a TLS handshake between the TCP handshake and the first HTTP byte — see Web and HTTP Fundamentals for what HTTP and TLS actually do at the application layer.
  4. A reverse proxy hides backend server(s) from the client; a load balancer is a reverse proxy that specifically spreads requests across multiple instances.
  5. Every network call needs an explicit timeout, and every repeated connection benefits from pooling — a dead/slow peer otherwise hangs the caller with no error.

What you will be able to do

  • Explain what an IP address and a port each identify, and why a service needs both
  • Open a real TCP client/server pair with the socket module, and a UDP pair, and explain the difference in guarantees
  • Resolve a hostname to an IP address with the socket module, and explain what DNS caching (TTL) implies for a changed IP
  • Explain where HTTP, HTTPS, and TLS each sit in the protocol stack, distinct from what HTTP methods/headers/status codes mean
  • Distinguish a reverse proxy from a load balancer from a forward proxy from NAT, and say what layer each operates at
  • Set an explicit timeout and reuse connections (pooling) on any network call, and explain what happens without either
  • Choose between a WebSocket and SSE for a given data-flow direction, and explain the handshake each is built on

Addressing and transport

How a packet finds the right host and the right process on it, the two transport-layer guarantees to choose between, and how DNS turns a name into an address.

TCP/IP, IP addresses, and ports

corebeginner

TCP/IP is the layered set of protocols the internet runs on: IP delivers a packet to the right HOST using an IP address, and a port number then delivers it to the right PROCESS on that host. TCP is the layer on top that makes delivery reliable and ordered.

Think of it as

An IP address is a street address — it gets a letter to the right building. A port number is the apartment number on that letter — it gets the letter to the right resident inside the building. TCP is the postal service adding tracking, delivery confirmation, and reordering pages that arrived out of sequence, on top of an IP layer that only promises "best effort, no guarantees."

python
import ipaddress

ip = ipaddress.ip_address('192.168.1.10')      # ValueError if not a valid IPv4/IPv6 address
net = ipaddress.ip_network('192.168.1.0/24')   # a whole subnet
port = 443                                     # just an int in [0, 65535] -- no dedicated type

What we're doing: Validate an IP address and a subnet with the ipaddress module, and show that a service is really identified by an (IP, port) pair, not the IP alone.

tcp_ip_addressing.pypython
import ipaddress

v4 = ipaddress.ip_address('192.168.1.10')
v6 = ipaddress.ip_address('2001:db8::1')
print(type(v4).__name__, v4.is_private)
print(type(v6).__name__, v6.version)

net = ipaddress.ip_network('192.168.1.0/24')
print('network:', net, 'num addresses:', net.num_addresses)
3
ip_address() parses and validates -- it raises ValueError on anything that is not a real IPv4/IPv6 address, rather than silently accepting a bad string.
5
.is_private confirms 192.168.1.10 falls in a reserved private range (RFC 1918) -- not routable on the public internet directly.
8
ip_network parses a whole CIDR block; num_addresses is computed from the prefix length (/24 = 2^(32-24) = 256).
Output
IPv4Address True
IPv6Address 6
network: 192.168.1.0/24 num addresses: 256

Why this works: ip_address() and ip_network() do real structural validation, not just string formatting -- they reject malformed addresses and compute real network math (address count from prefix length), which is what makes them safer than string-splitting an address by hand.

Treating an IP address alone as identifying a running service

Wrong

python
# "the server at 192.168.1.10 is down" -- down for WHAT?
# a host can run a web server on :443, SSH on :22, and a database on :5432
# all at once, all independently reachable or unreachable

Better

python
endpoint = ('192.168.1.10', 443)   # (host, port) -- the actual thing you connect to
# port 22 (SSH) on the same host can be perfectly reachable while :443 is not

What you see: A vague "the server is unreachable" report that turns out to mean only one specific port/service on that host is down, wasting time investigating the wrong process.

Why: A host can run many independent services simultaneously, one per port. Reachability, load, and failures are properties of a specific (IP, port) endpoint, not of the IP address alone -- diagnosing "the server" without naming the port skips the information that actually narrows down the problem.

IP finds the host; the port finds the process on it

Application data

an HTTP request, a DNS query, ...

TCP/UDP + port

e.g. destination port 443 — picks the process

IP + IP address

e.g. destination 192.168.1.10 — picks the host

Link layer

Ethernet/Wi-Fi frame — picks the device on this wire

  1. Application data — an HTTP request, a DNS query, ...
  2. TCP/UDP + port — e.g. destination port 443 — picks the process
  3. IP + IP address — e.g. destination 192.168.1.10 — picks the host
  4. Link layer — Ethernet/Wi-Fi frame — picks the device on this wire

The four layers a web request actually crosses

The four layers a web request actually crosses
LayerExample protocolWhat it addresses
ApplicationHTTP, DNS, SSHthe meaning of the data (a request, a query, a login)
TransportTCP, UDPa process on the host, via a port number
InternetIP (v4/v6)a host, via an IP address
LinkEthernet, Wi-Fia device on the local physical network

Together

python
import ipaddress

addr = ipaddress.ip_address('192.168.1.10')
print(type(addr).__name__, addr.is_private)   # Internet layer: a host address

port = 443                                    # Transport layer: a process on that host
endpoint = (str(addr), port)                  # (IP, port) together identify one service
print(endpoint)

Remember: An IP address routes to a HOST; a port routes to a PROCESS on that host; a service is really identified by the (IP, port) pair together, not by either alone.

See also: tcp vs udp · sockets · dns

Sockets

coreintermediate

A socket is a programming handle for one end of a network connection -- socket.socket() creates it, .bind()/.listen()/.accept() set up a server side, and .connect() sets up a client side. Every HTTP request, database driver, and SSH session is built on sockets underneath.

Think of it as

A socket is a phone handset, not the phone call itself. Creating one (socket.socket()) just gets you a handset. A server picks up (.bind() claims a number, .listen() means "I will answer", .accept() actually answers). A client dials (.connect()). Only once both sides are connected can either side .send()/.recv() -- talk into or listen on the line.

python
import socket

# server
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', 8080))
s.listen(5)
conn, addr = s.accept()      # blocks until a client connects

# client
c = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
c.connect(('example.com', 8080))

What we're doing: Run a real TCP echo server and client over the loopback interface, and prove the connection actually completed by reading back each side's address.

tcp_echo.pypython
import socket, threading, time

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('127.0.0.1', 0))
port = server.getsockname()[1]
server.listen(1)

def run_server():
    conn, addr = server.accept()
    data = conn.recv(1024)
    conn.sendall(b'echo:' + data)
    conn.close()

threading.Thread(target=run_server).start()
time.sleep(0.1)

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', port))
client.sendall(b'hello')
reply = client.recv(1024)
client.close()
print('bound port:', port)
print('client received:', reply)
5
bind(('127.0.0.1', 0)) with port 0 asks the OS to assign any free port -- getsockname() reads back which one it picked.
9
accept() blocks the server thread until the client actually connects -- it returns a brand-new socket dedicated to this one connection.
18
connect() performs the real TCP three-way handshake against the server's bound port before this line returns.
20
recv(1024) blocks until bytes arrive -- here, the server's echoed reply.
Output
bound port: 57773
client received: b'echo:hello'

Why this works: The client only receives the echoed bytes because a full TCP connection was established (connect() succeeded, accept() returned a live socket) -- recv() would hang or raise if no connection existed, so the printed reply is direct proof the handshake and data transfer both actually happened.

Assuming recv() returns the whole message in one call

Wrong

python
data = conn.recv(1024)
message = data.decode()   # assumes ALL the data arrived in this single recv() call

Better

python
chunks = []
while True:
    chunk = conn.recv(4096)
    if not chunk:          # empty bytes means the other side closed the connection
        break
    chunks.append(chunk)
    if b'\n' in chunk:     # or: stop once your own message-boundary marker arrives
        break
message = b''.join(chunks).decode()

What you see: Messages silently truncated under load or over a slow connection -- works fine in local testing where everything arrives in one packet, then breaks in production against a slower network path.

Why: TCP is a byte STREAM, not a message protocol -- recv() returns whatever bytes have arrived so far, which can be less than what was sent (split across multiple packets) or, less obviously, more than one send() worth if messages were sent back-to-back. Code must loop until it has a full message by its own definition, not assume one recv() equals one send().

The TCP handshake a socket performs before any data moves
Client
Server
  1. 1. bind() + listen()claims the address, starts waiting
  2. 2. connect() -> SYN
  3. 3. SYN-ACK
  4. 4. ACK -- handshake done
  5. 5. accept() returnsa new socket for this one connection
  6. 6. send()/recv() data
  1. Server → Server: bind() + listen() (claims the address, starts waiting)
  2. Client → Server: connect() -> SYN
  3. Server → Client: SYN-ACK
  4. Client → Server: ACK -- handshake done
  5. Server → Server: accept() returns (a new socket for this one connection)
  6. Client → Server: send()/recv() data

Core socket methods, server vs. client side

Core socket methods, server vs. client side
MethodSidePurpose
socket.socket(family, type)bothcreate the socket object (not yet connected)
.bind((host, port))serverclaim a local address to listen on
.listen(backlog)servermark the socket ready to accept incoming connections
.accept()serverblock until a client connects; returns (new_socket, addr)
.connect((host, port))clientperform the TCP handshake to a listening server
.send(bytes) / .recv(bufsize)bothwrite/read bytes once connected
.close()bothrelease the OS socket resource

Together

python
import socket, threading

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 0))          # port 0 = let the OS pick a free port
port = server.getsockname()[1]
server.listen(1)

def run_server():
    conn, addr = server.accept()
    conn.sendall(conn.recv(1024))      # echo back whatever it receives
    conn.close()

threading.Thread(target=run_server).start()
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', port))
client.sendall(b'hello')
print(client.recv(1024))

Remember: A socket is bytes-in/bytes-out once connected: server calls bind()+listen()+accept(), client calls connect() -- and .recv() can return a partial message, so loop until your protocol says you have the whole thing.

See also: tcp ip ip addresses and ports · tcp vs udp · connection pooling and timeouts

TCP vs UDP

standardintermediate

TCP sets up a connection first, then guarantees ordered, complete delivery -- at the cost of setup time and retransmission delays. UDP sends datagrams with no setup and no delivery guarantee, but with lower latency and less overhead, so it fits real-time data where a late packet is worse than a lost one.

Think of it as

TCP is a phone call: you dial, wait for pickup, then talk knowing every word arrives in order -- but dialing costs time. UDP is dropping postcards in a mailbox one at a time: no dial-in, no confirmation any specific one arrives, but nothing waits on the others either.

python
import socket

tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)   # reliable, ordered, connection-based
udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)    # best-effort, connectionless

What we're doing: Send a UDP datagram with no prior connection setup, unlike the TCP example elsewhere in this section that requires connect()/accept() first.

udp_no_handshake.pypython
import socket

udp_server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_server.bind(('127.0.0.1', 0))
udp_port = udp_server.getsockname()[1]

udp_client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_client.sendto(b'ping', ('127.0.0.1', udp_port))
data, addr = udp_server.recvfrom(1024)
print('UDP received:', data, 'from port:', addr[1])
7
No .connect() call at all -- sendto() ships the datagram directly to an address with zero handshake.
9
recvfrom() returns both the data and the sender's address, since UDP has no persistent connection object to already know it.
Output
UDP received: b'ping' from port: 55378

Why this works: The datagram arrives despite no connect()/accept() pair ever running -- proof that UDP genuinely skips the setup step TCP requires. Nothing here confirms the OS did not also drop it; UDP simply gives no mechanism to know either way.

TCP vs UDP, side by side

TCP vs UDP, side by side
PropertyTCPUDP
ConnectionRequired (three-way handshake)None -- send immediately
Delivery guaranteeGuaranteed, retransmits on lossBest-effort, no retransmission
OrderingGuaranteed in-orderNot guaranteed
Overhead20+ byte header, ACKs, retransmit logic8 byte header, no ACKs
socket typeSOCK_STREAMSOCK_DGRAM
Typical useHTTP, databases, file transferDNS, video/voice, gaming

Together

python
import socket

# TCP: connect() performs a real handshake before send/recv is possible
tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# UDP: no connection step -- sendto() ships a datagram immediately
udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp.bind(('127.0.0.1', 0))
port = udp.getsockname()[1]

sender = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sender.sendto(b'ping', ('127.0.0.1', port))   # no connect() call needed at all
data, addr = udp.recvfrom(1024)
print(data, addr[1] > 0)

Remember: TCP: handshake first, then guaranteed ordered delivery. UDP: no handshake, no guarantees, lower latency -- choose UDP only when a late packet is worse than a lost one.

See also: tcp ip ip addresses and ports · sockets · dns

DNS

standardbeginner

DNS (Domain Name System) translates a human-readable hostname like www.python.org into the IP address a computer actually needs to open a connection. Python resolves it via the OS resolver with socket.gethostbyname() or the more complete socket.getaddrinfo().

Think of it as

DNS is a phone book, not the phone network itself -- looking up a name gets you a number (an IP address), but the actual call still travels over TCP/IP once you have it. Every socket.connect(('example.com', 443)) call does a phone-book lookup first, invisibly, before the real connection happens.

python
import socket

ip = socket.gethostbyname('www.python.org')          # one IPv4 address
infos = socket.getaddrinfo('www.python.org', 443)     # full list, IPv4 + IPv6

What we're doing: Resolve a real hostname to an IP address and confirm the result is a valid dotted-quad IPv4 address.

dns_lookup.pypython
import socket

ip = socket.gethostbyname('www.python.org')
print('www.python.org resolves to (an IPv4):', ip, '-- valid dotted quad:', len(ip.split('.')) == 4)
3
gethostbyname() sends the actual DNS query through the OS resolver and blocks until it gets an answer or the query times out.
Output
www.python.org resolves to (an IPv4): 167.82.56.223 -- valid dotted quad: True

Why this works: The printed address is a genuine live DNS answer, not a hardcoded value -- it can change over time as python.org repoints its infrastructure, which is exactly the indirection DNS exists to provide: the hostname stays stable while the underlying IP is free to change.

DNS resolution calls in the socket module

DNS resolution calls in the socket module
CallReturnsWhen to use
socket.gethostbyname(host)a single IPv4 stringquick lookup, IPv4 only, one result
socket.getaddrinfo(host, port)list of (family, type, proto, canonname, sockaddr) tuplesproduction code -- handles IPv4/IPv6, multiple results
socket.gethostbyname_ex(host)(hostname, aliaslist, ipaddrlist)need every IPv4 address, not just one
socket.gaierrorexceptionraised when a name fails to resolve at all

Together

python
import socket

ip = socket.gethostbyname('www.python.org')
print(ip)

infos = socket.getaddrinfo('www.python.org', 443, proto=socket.IPPROTO_TCP)
print(len(infos), 'result(s)')

try:
    socket.gethostbyname('this-domain-does-not-exist-abc123.invalid')
except socket.gaierror as e:
    print('gaierror:', e)

Remember: DNS turns a hostname into an IP address before any connection can open -- gethostbyname() for a quick single result, getaddrinfo() for production code that needs IPv4/IPv6 and multiple results.

See also: tcp ip ip addresses and ports · sockets

Advertisement

HTTP/TLS in the stack, and the infrastructure between client and server

Where HTTP and TLS sit in the layering (the application-level detail lives in Web and HTTP Fundamentals), and the reverse proxies, load balancers, proxies, and NAT that traffic passes through on the way.

HTTP, HTTPS, and TLS in the protocol stack

referenceintermediate

HTTP is an application-layer protocol that runs over a TCP connection. HTTPS is that same HTTP, sent over a connection wrapped in TLS -- so the TCP handshake happens first, then a TLS handshake negotiates encryption, and only then does the HTTP request/response exchange begin.

Think of it as

Picture the layers stacking, bottom to top: TCP opens the pipe, TLS (for HTTPS) seals it, then HTTP is the conversation that travels through it. Skipping TLS does not change the conversation (HTTP) at all -- it only removes the seal around the pipe it travels through.

python
import ssl, socket

ctx = ssl.create_default_context()                          # sensible defaults: verify cert, check hostname
with socket.create_connection((host, 443)) as sock:          # TCP handshake first
    with ctx.wrap_socket(sock, server_hostname=host) as tls: # TLS handshake wraps the open TCP socket
        ...                                                  # only now is it safe to send HTTP bytes

What we're doing: Perform a real TLS handshake directly on top of a raw socket -- confirming TLS is a distinct step wrapping an already-open TCP connection, not something HTTP does itself.

tls_handshake.pypython
import socket, ssl

ctx = ssl.create_default_context()
with socket.create_connection(('www.python.org', 443), timeout=5) as sock:
    with ctx.wrap_socket(sock, server_hostname='www.python.org') as tls:
        print('TLS version negotiated:', tls.version())
        cert = tls.getpeercert()
        print('cert has subject:', 'subject' in cert)
        print('cipher:', tls.cipher()[0])
4
socket.create_connection() is a plain TCP connect -- no encryption yet, identical to what an http:// request would use.
5
wrap_socket() takes that already-open TCP socket and performs the TLS handshake on top of it -- a genuinely separate step, not part of opening the socket.
Output
TLS version negotiated: TLSv1.3
cert has subject: True
cipher: TLS_AES_128_GCM_SHA256

Why this works: wrap_socket() only returns successfully once a real TLS handshake completed and the certificate passed validation against the trusted CA store -- reaching 'TLS version negotiated' output is proof both the TCP and TLS handshakes finished, in that order, before any HTTP request was ever sent.

Where each protocol sits in the stack

Where each protocol sits in the stack
LayerProtocolDefault port
ApplicationHTTP80
Application + SecurityHTTPS (HTTP over TLS)443
Transport-securityTLSn/a -- wraps the transport connection
TransportTCPn/a -- carries both

Together

python
import socket, ssl

# plain HTTP: only a TCP handshake, no encryption layer
plain = socket.create_connection(('example.com', 80), timeout=5)
plain.close()

# HTTPS: TCP handshake, THEN a separate TLS handshake, before any HTTP bytes move
ctx = ssl.create_default_context()
with socket.create_connection(('example.com', 443), timeout=5) as sock:
    with ctx.wrap_socket(sock, server_hostname='example.com') as tls:
        print(tls.version())

Remember: HTTP needs a reliable byte stream (TCP) underneath; HTTPS adds a TLS handshake between the TCP handshake and the first HTTP byte. For HTTP methods, status codes, headers, and the application-security view of HTTPS/TLS, see Web and HTTP Fundamentals.

See also: http methods · status codes · https tls and same origin policy · tcp ip ip addresses and ports · sockets

Reverse proxy and load balancer

standardintermediate

A reverse proxy sits in front of one or more backend servers and forwards client requests to them, hiding the backend from the client. A load balancer is a reverse proxy whose main job is splitting traffic across multiple backend instances so no single one is overwhelmed.

Think of it as

A reverse proxy is a receptionist: a visitor only ever talks to the receptionist, who decides which office to actually route the request to -- the visitor never learns or cares which office handled it. A load balancer is that same receptionist with a rule for choosing among several equivalent offices, so no one office gets overloaded while others sit idle.

text
# nginx reverse proxy + load balancer, conceptual config
upstream app_servers {
    server 10.0.0.1:8000;
    server 10.0.0.2:8000;
}
server {
    listen 443 ssl;
    location / {
        proxy_pass http://app_servers;   # nginx picks an instance and forwards the request
    }
}

Reverse proxy vs. load balancer vs. forward proxy

Reverse proxy vs. load balancer vs. forward proxy
ComponentSits in front ofPrimary job
Forward proxyclientshide/control the client, e.g. corporate egress filtering
Reverse proxyserver(s)hide the backend, handle TLS termination, route by path/host
Load balancermultiple server instancesdistribute requests so no one instance is overwhelmed
nginx / HAProxyserver(s)real software that can act as either or both at once

Remember: A reverse proxy hides servers from clients; a load balancer is a reverse proxy that specifically spreads requests across multiple backend instances, using health checks to skip dead ones.

See also: proxies and nat · connection pooling and timeouts · https tls and same origin policy

Proxies and NAT

standardintermediate

A proxy is an application-level intermediary a client deliberately routes traffic through, often to filter, cache, or hide its identity. NAT (Network Address Translation) is a router-level technique that lets many devices on a private network share one public IP address, rewriting addresses in each packet as it crosses the boundary.

Think of it as

A proxy is a middleman you choose to talk through on purpose -- like using a forwarding mail service. NAT is invisible plumbing your router does automatically -- every device in a house shares one street address, and the router silently keeps track of which internal room each piece of mail is really for.

python
import httpx

# routing a Python HTTP client's traffic through a forward proxy
with httpx.Client(proxy='http://proxy.internal:8080') as client:
    response = client.get('https://example.com')

Proxy vs. NAT

Proxy vs. NAT
PropertyProxyNAT
Operates atapplication layer (HTTP-aware)network layer (IP packet headers)
Client awarenessclient is typically configured to use itusually fully transparent to the client
Typical purposefiltering, caching, anonymizing, access controlsharing one public IP among many private-IP devices
Can inspect request contentyes -- URLs, headers, bodiesno -- only IP/port headers

Remember: A proxy is a deliberate, request-aware intermediary a client is configured to use; NAT is transparent packet-address rewriting at a network boundary so many private devices can share one public IP.

See also: reverse proxy and load balancer · tcp ip ip addresses and ports

Advertisement

Connection lifecycle and server-to-client streaming

Managing a live connection's cost and failure modes, and the two standard ways a server pushes data to a client after the initial request.

Connection pooling and timeouts

standardintermediate

Connection pooling keeps a set of already-open TCP connections ready to reuse, instead of opening (and TLS-handshaking) a new one per request. A timeout is a limit on how long code waits for a connection or response before giving up and raising an error instead of hanging forever.

Think of it as

A connection pool is a fleet of taxis already running at a taxi stand -- grabbing one is instant, versus calling a fresh taxi (a new TCP + TLS handshake) from scratch every single trip. A timeout is telling the dispatcher "if no taxi answers in 5 seconds, stop waiting and tell me it failed" -- without it, code waits at the curb indefinitely.

python
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)             # 5 seconds for connect() AND subsequent recv() calls
s.connect((host, port))     # raises socket.timeout if the handshake takes too long

What we're doing: Prove a socket timeout actually fires and raises, rather than the code hanging forever, using a blocking recv on a socket nothing will ever send to.

socket_timeout.pypython
import socket, time

udp_t = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_t.bind(('127.0.0.1', 0))
udp_t.settimeout(0.2)
start = time.time()
try:
    udp_t.recvfrom(1024)
    print('no timeout raised (unexpected)')
except socket.timeout as e:
    elapsed = time.time() - start
    print('socket.timeout raised after ~', round(elapsed, 1), 's:', repr(e))
udp_t.close()
5
settimeout(0.2) applies to every subsequent blocking call on this socket, not just the next one.
8
recvfrom() would block forever without a timeout -- nothing is ever going to send to this socket.
Output
socket.timeout raised after ~ 0.2 s: TimeoutError('timed out')

Why this works: The timeout fires at approximately the configured 0.2s, not immediately and not never -- confirming settimeout() genuinely bounds the wait rather than just being an ignored hint. socket.timeout is a real alias for the built-in TimeoutError in modern Python.

Making a network call with no timeout at all

Wrong

python
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))   # no settimeout() -- can hang indefinitely if the host is unreachable

Better

python
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)            # fail fast instead of hanging
s.connect((host, port))

What you see: A request that just never returns -- no exception, no crash, no log line -- until something external (a load balancer, the OS) eventually kills it, if anything does at all.

Why: A blocking socket call with no timeout waits as long as the OS allows, which for a host that accepts the TCP connection but never sends a response can be effectively forever. Every production network call needs an explicit timeout so a slow or dead peer produces a clear, catchable error instead of an invisible hang.

Timeout types worth setting separately

Timeout types worth setting separately
Timeout kindWhat it boundsCommon default risk
Connect timeouttime to complete the TCP (+TLS) handshakeunreachable host hangs instead of failing fast
Read timeouttime waiting for response data once connecteda slow/stalled server hangs the caller indefinitely
Pool checkout timeouttime waiting for a free connection from an exhausted poola saturated pool silently queues callers with no bound

Together

python
import socket, time

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('127.0.0.1', 0))
s.settimeout(0.2)          # bound how long a blocking recv can wait
start = time.time()
try:
    s.recvfrom(1024)       # nothing will ever arrive -- nothing is sending
except socket.timeout as e:
    print('raised after', round(time.time() - start, 1), 's:', repr(e))
s.close()

Remember: Reuse connections (a pool) instead of paying handshake cost per call; set an explicit timeout on every network call, or a dead/slow peer hangs the caller with no error at all.

See also: content negotiation and performance · sockets · reverse proxy and load balancer

WebSockets and Server-Sent Events (SSE)

coreintermediate

WebSockets upgrade a single HTTP connection into a persistent, two-way channel -- server and client can both send messages at any time. SSE (Server-Sent Events) is simpler and one-way only: the server streams a sequence of text events to the client over a single long-lived HTTP response.

Think of it as

SSE is a radio broadcast: the server talks, the client only listens, over one connection that stays open. A WebSocket is a phone call: once connected, either side can speak at any moment, in either direction, on the same open connection.

python
# WebSocket server handler (e.g. with the third-party 'websockets' package)
async def handler(ws):
    async for message in ws:      # receive, full-duplex
        await ws.send(f'echo: {message}')

# SSE: a plain streaming HTTP response (e.g. with a WSGI/ASGI framework)
def sse_view():
    def event_stream():
        yield 'data: {"progress": 10}\n\n'
        yield 'data: {"progress": 42}\n\n'
    return Response(event_stream(), content_type='text/event-stream')

What we're doing: Confirm the SSE wire format is genuinely plain HTTP by writing a real streaming HTTP response by hand over a socket, using this section's own socket/TLS machinery rather than a third-party SSE library.

sse_over_raw_socket.pypython
import socket, threading, time

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 0))
port = server.getsockname()[1]
server.listen(1)

def run_server():
    conn, _ = server.accept()
    conn.recv(1024)   # discard the client's request line
    conn.sendall(
        b'HTTP/1.1 200 OK\r\n'
        b'Content-Type: text/event-stream\r\n\r\n'
        b'data: {"progress": 10}\n\n'
        b'data: {"progress": 42}\n\n'
    )
    conn.close()

threading.Thread(target=run_server).start()
time.sleep(0.1)
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', port))
client.sendall(b'GET /events HTTP/1.1\r\n\r\n')
print(client.recv(4096).decode())
client.close()
10
Content-Type: text/event-stream is the only thing that marks this as SSE -- otherwise it is an ordinary HTTP response, unlike a WebSocket's protocol-level Upgrade.
12
Each event is plain UTF-8 text ending in a blank line (\n\n) -- no binary framing, no separate protocol.
Output
HTTP/1.1 200 OK
Content-Type: text/event-stream

data: {"progress": 10}

data: {"progress": 42}

Why this works: Nothing here is WebSocket framing or a protocol upgrade -- it is one ordinary HTTP response whose body is simply written incrementally and never explicitly ended, which is the entire SSE mechanism. Any HTTP client can read it; no specialized library is structurally required, unlike a WebSocket connection.

Reaching for a WebSocket when the data only ever flows server-to-client

Wrong

python
# a live progress-bar feed implemented as a full WebSocket connection
# -- client never sends anything back over it, ever
async def handler(ws):
    for pct in range(0, 101, 10):
        await ws.send(f'{{"progress": {pct}}}')

Better

python
# SSE: plain HTTP, browser auto-reconnects for free, no upgrade handshake needed
def progress_stream():
    for pct in range(0, 101, 10):
        yield f'data: {{"progress": {pct}}}\n\n'

What you see: Extra client-side reconnect logic to hand-write (WebSockets do not auto-reconnect), a WebSocket-capable server/proxy configuration to maintain, and a protocol upgrade handshake to pay for -- all to carry data that only ever goes one direction.

Why: A WebSocket is the right tool when the client also needs to send data back over the same open connection. For a one-way feed, SSE gets automatic browser reconnection (EventSource), works over plain HTTP with no special proxy/load-balancer configuration for the upgrade, and needs no separate framing protocol to implement.

Full-duplex vs. one-way streaming, both built on HTTP

WebSocket

  • +HTTP Upgrade -> 101 Switching Protocols
  • +server and client can both send, anytime
  • +app must detect a dropped connection and reconnect

SSE (EventSource)

  • plain HTTP response, Content-Type: text/event-stream
  • server -> client only, one direction
  • browser auto-reconnects, resumes via Last-Event-ID
  • WebSocket
    • HTTP Upgrade -> 101 Switching Protocols
    • server and client can both send, anytime
    • app must detect a dropped connection and reconnect
  • SSE (EventSource)
    • plain HTTP response, Content-Type: text/event-stream
    • server -> client only, one direction
    • browser auto-reconnects, resumes via Last-Event-ID

WebSockets vs. SSE

WebSockets vs. SSE
PropertyWebSocketsSSE
Directionfull-duplex (both ways)one-way, server -> client only
ProtocolHTTP upgrade to a distinct framed protocolplain HTTP, kept open (text/event-stream)
Message formatbinary or text frames, app-definedUTF-8 text lines (data:, event:, id:)
Browser reconnectmanual, application handles itautomatic, built into EventSource
Typical usechat, multiplayer, live collaborative editinglive feeds, notifications, progress updates

Together

text
# WebSocket handshake (client request -> server response)
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

# --- vs. SSE: a normal HTTP response that just never ends ---
GET /events HTTP/1.1

HTTP/1.1 200 OK
Content-Type: text/event-stream

data: {"progress": 10}

data: {"progress": 42}

Remember: WebSockets: one HTTP Upgrade handshake, then full-duplex messaging either side can start; SSE: plain HTTP response kept open, server-to-client only, with automatic browser reconnect built in.

See also: http https and tls in the stack · http methods · status codes

Advertisement