Node.js interview questions: HTTP and networking

HTTP interviews reward engineers who can cross abstraction layers without losing the request. A prompt about keep-alive may turn into socket reuse, parser framing, proxy headers, timeouts, or a slow client that never finishes a body. The opening answer usually describes endpoints and status codes. Deeper rounds ask what bytes arrive, who owns each timeout, which hop terminates TLS, and how backpressure travels when a server streams a response through a proxy to a client.

These questions use that path as the spine. The follow-up trees move from Node's request objects to connection pools, intermediaries, protocol boundaries, and failure timing. Practice drawing the hops before choosing a fix. State which component observes the failure, whether the request can be retried, and what resource remains allocated while everyone waits. The unlocked sample shows how a precise network model produces better application decisions. By the end, you should be able to defend limits, forwarding rules, shutdown behavior, and streaming semantics without treating the network as a transparent pipe.

Covered in Volume 3: HTTP internals and networking
Question 01Fully unlocked sample

So a client opens a TCP connection to your Node server. Walk me through what actually happens between that moment and your request handler running.

A strong answer follows one request from the accepted socket through llhttp parsing into the JS handler and can say which layer owns what.

What an AI-prepared candidate might say

So the client connects, and Node accepts the TCP connection and starts reading bytes off the socket. It parses the HTTP request line, the headers, the body, and builds a request object and a response object out of that, and those get handed to your handler as req and res. You pull whatever you need off req, the URL, the method, headers, maybe the body, do your work, and write the response back with res.write and res.end. Node sends those bytes back over the same socket. And the event loop is basically how one thread handles a lot of connections at once, while one request is waiting on I/O, Node just works on the others. The http module wires most of this up for you, you register a callback with createServer and Node does the accepting and parsing and dispatching.

Senior

It's really a handoff chain across three layers. The kernel finishes the TCP handshake on its own and parks the connection on the listen socket's accept queue. libuv is watching that listen fd through epoll or kqueue, it accepts the connection and wraps it in a net.Socket. At this point no HTTP has happened at all. What you've got is a duplex byte stream, that's it.

Then bytes start arriving, the socket emits data, and Node feeds each chunk into llhttp, which is a C parser compiled from a state machine. And the thing about llhttp is it's incremental. It doesn't sit around waiting for a complete request, it fires callbacks as it crosses boundaries, headers complete, each body chunk, message complete. When the headers finish, Node builds the IncomingMessage and ServerResponse, that's your req and res, and calls your handler. The timing detail most people miss is right here. Your handler runs after the headers are parsed but often before the body has fully arrived. req is a readable stream the parser is still actively feeding, and res is a writable you can start writing to immediately.

Your writes go back through the socket into the kernel send buffer. On keep-alive the socket stays open and llhttp just resets its state for the next request on the same fd. All of this parks in the event loop's poll phase. An idle socket costs a file descriptor and a little kernel memory, it doesn't hold a thread, which is how one process sits on tens of thousands of connections. Once you can name the layers you can predict where a failure is going to surface, accept queue, parser, or handler.

Staff

Under load every stage in that chain fails differently, and the job is knowing which counter moves. Take the accept queue. If completed handshakes arrive faster than your loop accepts them, and that queue is bounded by the listen backlog, 511 by default, capped by somaxconn, the kernel starts dropping or resetting new connections. Clients see connect timeouts while your CPU graph looks idle. Nothing looks wrong at the network layer, the loop is just too busy to drain the queue. I catch that by watching event-loop delay from monitorEventLoopDelay next to the OS listen-overflow counter.

The parser has its own guardrails and I don't turn them off casually. server.headersTimeout is 60s by default, server.requestTimeout is 300s. They exist so a client dribbling bytes can't pin a parser and a socket open forever. I've watched someone disable them to work around a proxy quirk, and that's a slowloris hole reopened, plain and simple.

And the tradeoff I'll actually argue for is keeping the handler's synchronous stretch tiny. Anything CPU-heavy between the parser handing you req and your first await stalls every other connection parked in the poll phase. p99 climbs, median stays flat, and everyone stares at the wrong graph. So I track event-loop lag instead of average response time, and I keep body parsing streaming rather than buffering whole payloads into memory. When latency gets ambiguous I timestamp at accept, at headers-complete, and at handler entry. Then I can say which layer owns the delay instead of guessing.

Follow-up chain

  1. So where does backpressure kick in on this path if a client's uploading a big body faster than your handler reads it?
  2. And what's actually happening to the TCP connection while you're not reading req?
  3. Okay, keep-alive means a second request comes down that same socket. What has to get reset between the two?
  4. Say latency is creeping in somewhere. How do you prove it's the parser adding it and not your handler?
Question 02First answer included

Say your service makes a ton of outbound HTTP calls to the same few downstreams. How does Node actually reuse those connections, and what goes wrong if you get that part wrong?

Strong candidates get concrete about connection reuse, agents, socket pools, keep-alive, and know exactly how undici differs from the old http.Agent.

What an AI-prepared candidate might say

So Node supports HTTP keep-alive, meaning it keeps the TCP connection open after a response so the next request to that server can reuse it instead of doing the whole TCP handshake and TLS negotiation again. On the client side there's an agent that manages this. The http.Agent and https.Agent hold a pool of sockets, and when you make a request the agent gives you an idle socket to that host if one's free, otherwise it opens a new one. Reusing the connection saves you the setup round trips, so lower latency, and less load on the downstream too. I think recent Node versions turn keep-alive on by default on the global agent. For heavy outbound traffic you basically want pooling on so you're not paying handshake cost on every single call, and you can tune how many sockets the agent keeps around.

Senior

What an agent really holds, those free and in-use socket lists keyed by host, and why undici's pipelined connection pool acts so differently under concurrency than http.Agent.

Staff

The failures that actually show up at scale, maxSockets queueing and head-of-line stalls on a pooled connection, and how to read the socket counters that give them away.

Follow-up chain

  1. So with the default agent, is there anything capping how many sockets you open to one host? And what happens once you hit that cap?
  2. And how does that queue play with your request timeout?
  3. Node's global fetch doesn't go through http.Agent at all. So what's actually behind it, and how would you tune that pool?
  4. Why does one slow response on a keep-alive connection hold up the others, and when does that actually bite you?
Question 03First answer included

A Node HTTP server ships with a handful of server-side timeouts. What are they, and which attack is each one actually there to stop?

The strong answer names all three server-side timeouts and ties each one to the specific slow-client attack or leak it exists to bound.

What an AI-prepared candidate might say

There are a few timeouts on the server object. server.timeout is how long a socket can sit idle before Node destroys it. Then there's keepAliveTimeout, which is how long an idle keep-alive connection stays open waiting for the next request, I think that one defaults to 5 seconds. And newer Node versions added headersTimeout and requestTimeout to guard against clients that send data really slowly. That slow trickle thing is basically what a slowloris attack is, the attacker opens a ton of connections and dribbles bytes to exhaust the server's capacity. The general idea with all of these is capping how long any single client can tie up a connection, so a slow or malicious client can't hold resources open forever. You set them on the server object and tune them against how long your legitimate requests and uploads actually take.

Senior

The three separate clocks, headersTimeout, requestTimeout, and keepAliveTimeout, what each one actually measures, the real default values, and how they stack.

Staff

Why bumping a timeout to paper over a proxy quirk quietly reopens slowloris, and how your own metrics can tell a slow legitimate upload from an attack.

Follow-up chain

  1. Say a legitimate upload takes four minutes and suddenly it's coming back 408. Which timeout fired, and how do you fix that without turning the protection off?
  2. What's keepAliveTimeout actually protecting you from, and why does it start to matter once there's a load balancer in front of you?
  3. And how does that turn into random 502s at the load balancer?
  4. Slowloris dribbles headers one byte at a time. Which timeout actually catches that, and why isn't server.timeout the answer?
Question 04First answer included

If you moved a Node service from HTTP/1.1 to HTTP/2, when would that actually buy you something, and where would it not?

A good answer keeps application-layer multiplexing separate from transport-level head-of-line blocking, and knows which one HTTP/2 fixes and which stays.

What an AI-prepared candidate might say

The main improvement in HTTP/2 is multiplexing. With HTTP/1.1 a connection carries one request and response at a time, which is why browsers open several connections in parallel, and a slow response can hold up whatever's waiting behind it on the same connection. HTTP/2 lets a bunch of requests and responses share one connection as independent streams that interleave, so one slow response doesn't block the rest anymore. It also compresses headers with HPACK, and there's server push. If you're serving lots of small resources that cuts latency and connection overhead. Node has a built-in http2 module for this. I'd say the move usually helps when you've got many concurrent requests going to the same origin, because you get the concurrency without paying for several TCP and TLS handshakes.

Senior

The two layers where head-of-line blocking lives, why HTTP/2 kills one and leaves the other alone, and how per-stream flow control changes what you tune.

Staff

Where HTTP/2 actually pays off in real deployments and where it's neutral or worse, plus the failure a single lossy connection creates that HTTP/1.1 pooling dodges.

Follow-up chain

  1. Okay, so HTTP/2 puts all these streams on one connection. When a packet gets lost, what's still blocking every single one of them?
  2. So on a lossy network, could plain HTTP/1.1 with six connections actually beat HTTP/2 on one?
  3. Say you terminate HTTP/2 at a proxy and it speaks HTTP/1.1 to Node behind it. What did you actually get out of that?
  4. What's HTTP/2 flow control doing, and how is that different from the windowing TCP already does?
Question 05First answer included

For a Node service, where would you terminate TLS, and what does that handshake actually cost you?

Strong answers place TLS termination from real constraints and can put numbers on what a handshake costs in round trips and CPU.

What an AI-prepared candidate might say

So TLS termination is just the point where the encrypted traffic gets decrypted. You can do that at a load balancer or reverse proxy sitting in front of Node, or Node can terminate it itself with the https module. Putting it on the proxy is the common choice, it moves the crypto work off your app and keeps certificate management in one place. The handshake is the expensive part because it uses asymmetric cryptography to exchange keys and verify the server certificate, and the symmetric encryption protecting the actual data afterward is much cheaper. To cut the cost you use keep-alive so connections get reused across requests, and session resumption so returning clients can skip part of the handshake. TLS 1.3 needs fewer round trips than 1.2, so it's faster. Usually you just terminate at the edge and reuse connections so you're not paying full handshake cost over and over.

Senior

The handshake taken apart, its round trips and its asymmetric-crypto cost, why 1.3 gets it done in one RTT, and what session resumption really reuses.

Staff

How to pick termination placement from real constraints, internal encryption, cert management, CPU headroom, and why handshake cost gets measured apart from throughput.

Follow-up chain

  1. So a full TLS 1.3 handshake fits in one round trip. What's actually happening inside it, and what does 0-RTT resumption get to skip?
  2. And what's the security catch that makes people nervous about 0-RTT?
  3. Okay, you terminate TLS at the load balancer. What does your Node app suddenly get wrong about the client, and how do you get that back?
  4. During a surge of brand-new clients your CPU spikes on handshakes. Why is it new clients specifically, and what would you tune?
Question 06First answer included

How does a request body actually arrive in Node, and how would you accept a really large upload without the process falling over?

A strong answer knows how a request body gets framed on the wire and streamed into Node, and why holding whole bodies in memory will eventually hurt.

What an AI-prepared candidate might say

So in Node the request object is a readable stream, the body comes in as data events unless you've got a body parser in front. With Express you add express.json() or express.urlencoded() and then you just read req.body. With raw http you listen for the data chunks, concatenate them, and handle the full body on end. The client says how big the body is with the Content-Length header, or it uses chunked transfer encoding when it doesn't know the size ahead of time. For big uploads you're supposed to stream instead of buffering everything in memory, so pipe the request to a file or a storage service rather than building one giant Buffer. And you want a size limit so a client can't send an unbounded body and exhaust memory. Most parsers let you configure a max body size, Express defaults to a 100KB limit for JSON I believe.

Senior

How the body gets framed, Content-Length versus chunked, how req hands it to you as a readable stream, and where the parser passes off backpressure.

Staff

Streaming an upload straight into storage with the size cap enforced mid-read, plus the metrics that spot a memory blowup before it OOMs the process.

Follow-up chain

  1. With chunked transfer encoding there's no Content-Length at all. So how does the server ever know the body's finished?
  2. Say you want a 10MB cap on uploads. Where do you actually enforce that, and why isn't checking Content-Length enough?
  3. And what if a client just lies, declares one Content-Length and keeps sending bytes past it. What happens then?
  4. Piping req straight to a file works fine until the disk can't keep up with the upload. What's backpressure doing for you there?
Question 07First answer included

Say your Node app sits behind a proxy or a load balancer. How do you get the real client IP without getting spoofed, and how does a WebSocket upgrade survive that hop?

Good candidates treat forwarded headers as untrusted until a proxy they control rewrites them, and can walk a WebSocket upgrade through the stack.

What an AI-prepared candidate might say

Behind a proxy the TCP connection Node sees is coming from the proxy, so req.socket.remoteAddress is going to hold the proxy's address, not the client's. The real client IP shows up in the X-Forwarded-For header, which proxies add to record the original client address. In Express you set trust proxy and then req.ip reads from that header. For WebSockets, they start out as a normal HTTP request that carries an Upgrade: websocket header, and the server answers with a 101 status to switch protocols. After that the same TCP connection just carries WebSocket frames. Libraries like ws do the handshake for you by listening on the server's upgrade event. Your proxy has to pass those upgrade headers through or the WebSocket connection fails. And you should only trust the forwarded headers when they come from your own proxy, I think that's the main gotcha.

Senior

Why X-Forwarded-For stays a client-controlled string until a trusted hop rewrites it, how the trust boundary picks which entry to believe, and what the Upgrade handshake actually does.

Staff

The spoofing and rate-limit-bypass messes you get from trusting forwarded headers the wrong way, and what kills WebSockets at a proxy that buffers or times out.

Follow-up chain

  1. So a client shows up sending its own X-Forwarded-For header. Why can it now walk straight past your rate limiter, and what's the fix?
  2. Walk me through the WebSocket upgrade itself. What HTTP actually goes back and forth before that connection turns into a raw socket?
  3. Say the proxy in front of Node keeps killing idle WebSockets after 60 seconds. Why is it doing that, and how do you keep them alive?
  4. Now put two proxy hops in front. Which X-Forwarded-For entry is the real client, and how do you pick it without getting burned?
Question 08First answer included

How does Node actually turn a hostname into an IP, and why might DNS be quietly capping your outbound throughput?

The strong answer keeps dns.lookup, getaddrinfo on the thread pool, apart from dns.resolve, c-ares over the network, and knows Node caches nothing by default.

What an AI-prepared candidate might say

Node does hostname resolution through the dns module. The common path is dns.lookup, that's what runs under the hood when you make an HTTP request to a hostname, it turns the name into an IP address. There's also dns.resolve, plus dns.resolve4 and dns.resolve6, which query DNS servers directly. The difference as I understand it is that dns.lookup uses the operating system's resolver, so it respects /etc/hosts and system configuration, while dns.resolve talks to DNS servers over the network instead. Node doesn't cache DNS results by default, so every new connection can trigger a fresh lookup, and under high outbound volume that adds latency and load. To cut that down you can add a DNS cache, either in the application or by running a local caching resolver. Which method you pick kind of depends on whether you want system behavior or raw DNS records.

Senior

The two resolution paths, getaddrinfo on the thread pool against c-ares over the network, and why the one every http request takes can starve fs and crypto.

Staff

How an uncached, thread-pool-bound lookup turns into latency your downstream gets blamed for, and where caching belongs if you don't want to break failover.

Follow-up chain

  1. So every outbound HTTP request goes through dns.lookup by default. Where does that actually run, and what else is fighting for those threads?
  2. And how would you actually prove it's DNS adding the latency and not the downstream?
  3. Node doesn't cache DNS between calls at all. What starts breaking under load, and what are your two ways out?
  4. When would you deliberately reach for dns.resolve over dns.lookup, knowing it's going to ignore /etc/hosts?
Question 09First answer included

Your logs are filling up with ECONNRESET. What's actually going on down at the socket, and how do you tell a real problem apart from noise?

A strong answer reads socket errors as protocol events, knows which side sent the RST, what half-open means, and treats ECONNRESET as having a cause.

What an AI-prepared candidate might say

ECONNRESET means the connection got reset by the other side, so the peer sent a TCP RST instead of closing cleanly. You see it a lot when a client disconnects before the server finishes responding, or when a timeout closes a connection, or a proxy drops an idle one. The way you handle it is attaching an error listener to the socket or the request, so an unhandled error doesn't crash the process. A half-open connection is when one side has closed its end but the other hasn't, so data can still flow one way. And the listen backlog is the queue of pending connections waiting to be accepted, if that fills up new connections can get refused. In practice you log these errors, figure out whether they point at a real client problem, and try not to treat every reset as fatal, since some of them are basically just clients going away.

Senior

What an RST really is, how a clean FIN close differs from a reset, and how a half-open connection hangs around until some write finally fails.

Staff

How to sort harmless resets from a real fault, why the listen backlog drops connections while your CPU graph looks fine, and what's worth measuring.

Follow-up chain

  1. What's actually different between a connection that closes with a FIN and one that gets reset with an RST, and which one hands you ECONNRESET?
  2. Say the client hangs up halfway through your response. What error shows up, and why do you need a handler on res and not just req?
  3. Give me your definition of a half-open connection. How does one just sit there with neither side noticing, and what finally surfaces it?
  4. Connections are getting dropped before your handler ever runs. How would the listen backlog cause that, and how do you confirm it's the culprit?
Question 10First answer included

Express versus Fastify versus raw http. How much is the framework layer actually costing you, and when does that cost even matter?

Strong candidates measure framework overhead against a real workload and can point at where Express and Fastify actually spend their time.

What an AI-prepared candidate might say

Raw Node http is the fastest since nothing sits on top of it, but then you're writing routing and parsing yourself. Express is the most popular framework, it gives you routing and middleware and a lot of convenience at some performance cost, because every request passes through its middleware stack. Fastify is the one built for performance, it benchmarks faster than Express, mostly down to a more efficient router and fast JSON serialization. In benchmarks it handles more requests per second than Express on simple routes. Whether you'd actually notice depends on the workload though. If each request does a database query or calls another service, that I/O usually dominates and the framework overhead is a small fraction of the total. So basically you pick a framework for its libraries and developer experience, and you only worry about raw overhead when you're serving very high volumes of lightweight requests.

Senior

Where the framework's time really goes, route matching, the middleware chain, serialization, and why schema-compiled JSON is Fastify's actual edge over Express.

Staff

When framework overhead is a rounding error next to your real latency and when it genuinely matters, measured on your own workload instead of a hello-world.

Follow-up chain

  1. So Fastify's headline win isn't really the router, it's serialization. What's it doing there that Express doesn't bother with?
  2. And what can go wrong with schema-compiled serialization? How do you make sure you're not leaking fields?
  3. Express middleware is just a straight chain of functions. How does that cost grow, and where does it start to hurt?
  4. A hello-world benchmark says Fastify's three times faster. Why is that number close to useless for your actual API?

THE BASELINE GETS YOU THROUGH QUESTION ONE.

Raw Mode trains the follow-ups.

Unlock every Senior and Staff answer, every tree answer, the debug repos, hallucination drills, design scenarios, and the framework capstone.Get NodeBook Raw Mode