DEV Community

Cover image for From TCP to Express: Understanding Request Lifecycles and Cancellation
Ali nazari
Ali nazari

Posted on

From TCP to Express: Understanding Request Lifecycles and Cancellation

Understanding how HTTP requests map to TCP connections gives backend engineers the ability to build efficient, responsive systems.

In Node.js, the req.socket property exposes the underlying TCP socket for each request, allowing servers to detect disconnects, cancel work, and free up resources when clients abort requests.

This article explores how req.socket works, how to use it to implement cancellation logic, and how to propagate abort signals through your application layers.

What req.socket Actually Is

Every Express request object (req) is an instance of Node.js’s IncomingMessage, which represents the HTTP request stream.

IncomingMessage holds a socket property that refers to the underlying net.Socket — the raw TCP connection between the client and the server.

// Example: inspecting a request's socket
app.get('/', (req, res) => {
  console.log(req.socket instanceof require('net').Socket); // true
  res.send('ok');
});
Enter fullscreen mode Exit fullscreen mode

The socket is responsible for reading and writing bytes over the network. Express and the Node.js HTTP server simply interpret those bytes as HTTP.

Why This Matters for Request Cancellation

When a client (for example, a browser using fetch with an AbortController) cancels a request or closes a tab, the TCP connection is terminated.

Node.js reflects this event by emitting 'close' on the socket.

Without handling this event, your backend code continues executing:

  • Database queries continue running.
  • Heavy computations continue consuming CPU.
  • External API calls still happen.
  • Queued work is not canceled.

Detecting and acting upon socket closure allows the backend to reclaim resources early.

Key Socket Properties and Events

Property Type Description
remoteAddress string IP address of the client.
remotePort number Client port.
localAddress string Server’s IP address used for this connection.
localPort number Server’s listening port.
destroyed boolean Indicates whether the socket is closed.
readyState string TCP connection state: open, readOnly, writeOnly, closed.

Events:

  • 'close': Connection closed (client aborted or timeout).
  • 'end': Client finished sending data.
  • 'error': A socket-level error occurred.

Detecting Client Disconnects

To observe cancellations, listen for the close event on the request’s socket:

app.get('/heavy', async (req, res) => {
  req.socket.once('close', () => {
    console.log('Client disconnected before completion.');
  });

  // Simulate long-running work
  await new Promise(r => setTimeout(r, 10000));

  if (req.socket.destroyed) return; // connection is gone
  res.send('Completed successfully.');
});
Enter fullscreen mode Exit fullscreen mode

This pattern ensures that you don’t attempt to write a response after the connection is gone and provides a hook to clean up or cancel ongoing work.

Creating an Abort Signal for Unified Cancellation

A robust approach is to create an AbortController tied to the socket’s lifecycle:

import { AbortController } from 'node:abort_controller';

function createRequestAbortController(req, res) {
  const controller = new AbortController();
  const onClose = () => controller.abort();

  req.socket.once('close', onClose);
  res.once('finish', () => req.socket.off('close', onClose));
  res.once('close', () => req.socket.off('close', onClose));

  return controller;
}

Enter fullscreen mode Exit fullscreen mode

You can then integrate it in your routes:

app.get('/report', async (req, res) => {
  const controller = createRequestAbortController(req, res);

  try {
    const data = await generateReport({ signal: controller.signal });
    if (!req.socket.destroyed) res.json(data);
  } catch (err) {
    if (controller.signal.aborted) {
      console.log('Request aborted by client.');
      return;
    }
    res.status(500).send('Error generating report.');
  }
});
Enter fullscreen mode Exit fullscreen mode

Every layer that supports AbortSignal (e.g., fetch, pg v8+, node-mongodb-driver v5+) can now respond cooperatively to the cancellation.

Integrating with NestJS via Interceptor

You can generalize this logic in NestJS using an interceptor:

import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class RequestAbortInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const req = context.switchToHttp().getRequest();
    const res = context.switchToHttp().getResponse();

    const controller = new AbortController();
    const onClose = () => controller.abort();

    req.socket.once('close', onClose);
    req.signal = controller.signal;

    return next.handle().pipe(
      tap(() => req.socket.off('close', onClose))
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Registering this interceptor globally ensures every request gains a signal property automatically.

Production-Ready Guidelines

  • Isolate CPU-intensive tasks in worker threads or child processes.
  • For background jobs, use distributed cancellation flags.
  • Log and monitor aborted request rates to optimize timeouts and capacity.

Cancellation is not automatic — it requires explicit cooperation from every layer. Once implemented, it improves server stability, scalability, and performance in high-load environments.

Top comments (0)