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');
});
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.');
});
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;
}
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.');
}
});
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))
);
}
}
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)