Prompt and scope
A Node endpoint performs several external calls and normally takes 5–35 seconds. Node allows 60 seconds, while NGINX keeps its default proxyreadtimeout. Production users occasionally see 502; the application log later says the business write completed. Under load, long-lived connections also consume the pool.
Draw a timeline for the client, NGINX, Node, downstream dependencies, and job store. Explain the source of the 502, why work can finish after the client sees failure, and when to use a synchronous response, streaming, or an asynchronous queue. The numbers are interview assumptions; the core skill is cross-layer timeout semantics, cancellation, side-effect boundaries, isolation, and proof of the fix, so this is a backend question.
What the interviewer evaluates
Strong candidates separate connection close, application deadline, and business completion instead of retrying every 502. They know NGINX read timeout measures idle time between upstream reads, not necessarily the whole response, and that AbortSignal.timeout() only notifies operations that actually listen to the signal.
They also inventory defaults in proxies, gateways, load balancers, SDKs, and clients; decouple HTTP requests from long jobs when appropriate; and prove that the repair does not create duplicate writes, leaked connections, or retry amplification.
Questions to clarify first
- Is the 502 generated by NGINX or returned by Node and rewritten? Compare headers, proxy error logs, and upstream access logs.
- Was a business side effect committed when the timeout happened? An unknown result cannot be blindly replayed.
- Must the result be returned in this request? If only the final result matters, a 35-second synchronous connection is unnecessary.
- Is the upstream sending bytes continuously? Read-idle timeout and total request deadline then have different meanings.
- Does cancellation reach the database, HTTP client, and external SDK? Closing a browser connection does not stop every operation.
- How do concurrency, connection-pool use, event-loop delay, and queue depth change? Confirm the bottleneck before changing numbers.
A 30-second answer
“I would correlate one trace ID across the client, NGINX, Node, downstream calls, and storage to identify which layer emits the 502 and when. NGINX proxyreadtimeout limits idle time between reads; Node’s 60-second request setting does not guarantee that a job stops after the client disconnects. For a synchronous result, I would define one end-to-end deadline and leave cleanup margin across proxy, application, and downstream. If the job exceeds the interaction budget, persist it, return an ID, and let a worker execute it. I would inject failures to verify no duplicate side effects, connection exhaustion, or retry amplification.”
Step-by-step solution
Start with the timeline. The client sends a request, NGINX forwards it, and Node begins work. If Node sends no response bytes for long enough, NGINX’s read timer can expire and close the upstream connection, producing a 502 or 504. Node may not receive the cancellation at the same instant; it may already have committed a write and then finish computation. A user-visible failure and a completed business operation can therefore coexist.
Correlate NGINX access/error logs, Node request start/end/abort events, downstream calls, and database commits with trace, request, and job IDs. Compare upstreamresponsetime, handler duration, client receipt time, and side-effect commit time. This distinguishes proxy idle timeout, application deadline, downstream timeout, and client disconnect. An application “success” log alone is insufficient.
NGINX’s proxyreadtimeout defaults to 60 seconds and measures the longest idle interval between two reads; receiving bytes restarts that interval. Increasing it changes proxy tolerance, not client deadlines or connection cost. If you change it, record proxy, application, and client limits in one table and reserve time for response transmission, cleanup, and network jitter.
Node can create a deadline with AbortSignal.timeout() and pass the signal to fetch, database, or SDK calls that support cancellation. Cancellation is cooperative: a library that ignores the signal may continue. An already committed transaction cannot be made nonexistent by aborting. Every side effect therefore needs an idempotency key, a state machine, or a compensation path with explicit states such as accepted, running, succeeded, failed, and unknown.
If the job’s p99 is far beyond the interaction budget, create an asynchronous boundary. The API validates input, writes a job record or queue message, and returns 202 plus a job ID. A worker leases the job, executes retries, and persists the result; the client polls or subscribes to status. Job creation and completion must be idempotent, worker restarts must be recoverable, and duplicate delivery must not duplicate payment or resource creation. The queue adds storage, workers, and dead-letter operations that need capacity and alerts.
Streaming is appropriate only when results can be safely produced in chunks, every intermediary supports long-lived responses, and the total duration is bounded. Heartbeats do not replace a total deadline, output limit, or cancellation path. Do not use streaming to conceal unbounded work.
Verify the repair by setting a known proxy idle timeout and injecting downstream silence beyond it; disconnect clients at different phases; restart workers; and run concurrency tests. Assert at most one successful side effect per logical job, no retry beyond the deadline, bounded connection and queue usage, and a trace that reconstructs every layer’s timestamps.
Model answer
“I would not start by changing 30 seconds to five minutes. First I would correlate NGINX, Node, downstream, and database timestamps to determine whether the proxy generated the 502 or Node returned it. proxyreadtimeout is an idle interval between reads, while the Node request deadline and business completion are separate events; the proxy can close the connection while Node continues and commits a side effect.
For a synchronous result, I would define one end-to-end deadline, propagate remaining budget downstream, use AbortSignal.timeout(), and verify that each SDK observes cancellation. An unknown write result requires an idempotency key and status lookup. If the job’s p99 exceeds the interaction budget, the API should persist the job and return 202 with an ID; workers execute, retry, and persist status. I would inject proxy idle timeouts, client disconnects, worker restarts, and duplicate delivery, then check for no duplicate side effects, bounded connection use, and a complete trace.”
Common mistakes
- Only increasing NGINX timeout → long connections and concurrency pressure remain → define a business deadline and async boundary.
- Retrying every 502 → the original job may have committed → query status and reuse an idempotency key.
- Treating
proxyreadtimeoutas total response time → small continuous chunks can keep a request alive indefinitely → add a total deadline and output cap. - Assuming a client close stops Node → many libraries ignore cancellation → verify abort, connection, and transaction behavior per layer.
- Using heartbeats to hide an unbounded job → resources still leak → cap total time, bytes, and concurrency.
- Running a 35-second job in a synchronous handler → HTTP connections carry all work → persist a job and use workers.
- Looking only at Node logs → the proxy’s error and timing disappear → collect proxy access/error and upstream timings.
- Adding a queue without idempotent state → duplicate delivery causes duplicate charges → use a unique job key and conditional state updates.
Follow-up questions and responses
Follow-up 1: Is changing proxyreadtimeout to 60 seconds enough?
No. It only controls idle time between reads; another layer may have a shorter deadline, and the connection still consumes resources. Define the interaction budget first, then align every layer.
Follow-up 2: How does Node stop after a client disconnects?
Observe the request close event, abort a controller, and pass its signal to cancellable operations. For non-cancellable calls, isolate capacity and discard late results safely. Committed side effects still require idempotency and reconciliation.
Follow-up 3: When is streaming appropriate?
When results are safely chunkable, all intermediaries support long-lived responses, and total duration is bounded. Keep a heartbeat interval, total deadline, output limit, and cancellation path.
Follow-up 4: How does a queue avoid duplicate execution?
Derive one job key from the business request and enforce uniqueness in storage. Workers use leases, completion uses conditional updates, and external side effects reuse the same idempotency key.
Follow-up 5: How do you prove the repair works?
Inject proxy idle timeouts, slow downstream responses, client disconnects, network resets, and worker restarts in staging. Compare layer timestamps and inspect error source, connection usage, duplicate effects, and queue delay.
Follow-up 6: Why can application logs say success while the user gets 502?
The proxy may have closed the connection first, or the response may be lost later. A completion log proves code finished, not that the response was delivered. Correlate proxy status, Node write result, and client observation.
Follow-up 7: What does asynchronous execution cost?
It adds state storage, workers, retries, dead letters, and eventual consistency. In exchange, HTTP lifetime is decoupled from job duration and concurrency and replay can be controlled independently.