Join our WhatsApp Channel
Cart / 0.00$

No products in the cart.

Visit Support Portal
No Result
View All Result
Friday, October 30, 2026
  • Home
  • Tutorials
    • Machine Learning
    • Python
    • Web Development
    • Javascript
    • Mathematics
    • Deep Learning
    • Artificial Intelligence
  • Store
  • Our ServicesBook Now
    • Managed Hosting Services
    • Web Development Services
    • Digital Marketing Services
    • Website Security Checker
    • Video Editing Services
  • Our Softwares
    • JaggoRe AIFree
    • Email ExtractorPaid
    • TimeWell AppFree
Get a Free Quote
  • Login
  • Register
Neuraldemy
Friday, October 30, 2026
Cart / 0.00$

No products in the cart.

No Result
View All Result
Neuraldemy
Get A Free Quote
Home NodeJS

DNS And TCP in Node.js Explained

Neuraldemy by Neuraldemy
October 29, 2026
in NodeJS, Javascript, Web Development
Reading Time: 90 mins read
A A

In this tutorial we will learn about DNS and TCP in Node.JsImagine that your Node.js application needs to communicate with a server at:

api.example.com

Your application knows the hostname, but computers do not route network packets using that human-friendly name alone. They need network addresses.

Even after obtaining an IP address, the application still needs a mechanism to establish a connection and exchange data reliably.

These are two different problems.

DNS solves the naming problem: it helps associate domain names with information such as IP addresses.

TCP solves the transport problem: it establishes a connection and provides a reliable, ordered stream of bytes between two endpoints.

Node.js exposes these capabilities through built-in modules:

import * as dns from "node:dns";
import * as net from "node:net";

The node:dns module provides hostname-resolution operations, while node:net provides TCP networking and related socket functionality.

Our goal is to understand what happens underneath these APIs and then build a small TCP client and server ourselves.

Table of Contents

  • Part 2: Understanding IP Addresses, Hosts, and Ports
    • 2.1 What is an IP address?
    • 2.2 What is a port?
    • 2.3 IP address versus port
  • Part 3: DNS Concepts
    • 3.1 Why was DNS needed?
    • 3.2 DNS is a distributed hierarchy
    • 3.3 What actually happens when you look up a domain?
    • 3.4 DNS does more than resolve IP addresses
  • Part 4: The Node.js DNS Module
    • 4.1 dns.lookup(): Use the system’s normal resolution mechanism
    • 4.2 Resolve multiple addresses
    • 4.3 Restrict the address family
    • 4.4 dns.resolve4() and dns.resolve6()
    • 4.5 dns.resolveMx(): Looking up mail servers
    • 4.6 Other useful DNS operations
    • 4.7 The important DNS performance detail: libuv’s thread pool
  • Part 5: DNS Resolution Is Not a Network Connection
  • Part 6: TCP Concepts
    • 6.1 Why isn’t an IP address enough?
    • 6.2 TCP is connection-oriented
    • 6.3 TCP does not preserve messages
    • 6.4 How does TCP establish a connection?
    • 6.5 What does TCP guarantee once data is sent?
  • Part 7: The Node.js net Module
    • 7.1 A Socket is a Duplex stream
    • 7.2 Building a minimal TCP server
  • Part 8: Build a TCP Client
    • What is happening under the hood?
  • Part 9: The TCP Framing Problem, Solved Properly
    • 9.1 Choose a simple protocol: newline-delimited messages
    • 9.2 Implement the line-based TCP server
    • 9.3 Implement a matching client
  • Part 10: Important Socket Events and Methods
    • 10.1 socket.end() versus socket.destroy()
    • 10.2 Idle timeouts
    • 10.3 TCP keep-alive is not the same as HTTP keep-alive
    • 10.4 The socket error event
  • Part 11: Backpressure in TCP Socket Applications
  • Part 12: How DNS and TCP Work Together in a Real Application
    • A practical detail about net.createConnection()
  • Part 13: Common Networking Errors and What They Mean
  • Part 14: Important Distinctions
    • DNS is not TCP
    • TCP is not HTTP
    • A socket write is not proof of application success
    • TCP chunks are not application messages
    • Asynchronous does not mean the work is free
  • Conclusion
  • Official Documentation and Further Reading

Part 2: Understanding IP Addresses, Hosts, and Ports

Before DNS and TCP make sense, we need to establish what it means to communicate with another computer.

2.1 What is an IP address?

An IP address identifies a network interface for communication using the Internet Protocol.

Two commonly encountered versions are IPv4 and IPv6.

An IPv4 address looks like this:

192.0.2.10

An IPv6 address looks like this:

2001:db8::10

The addresses above are illustrative examples from documentation-reserved address ranges, rather than addresses to use as real public servers.

IPv4 uses 32-bit addresses, while IPv6 uses 128-bit addresses.

You may wonder why we need IPv6 when IPv4 already exists. The main reason is that IPv4 has a limited address space, whereas IPv6 provides a much larger one.

An IP address is not necessarily a permanent identity for a physical computer. It may identify a virtual interface, a server in a cloud environment, a router interface, or another network endpoint. Addresses can change as systems move or network configurations change.

The important point is that IP provides the addressing foundation used to deliver packets across networks.

2.2 What is a port?

An IP address helps identify the network endpoint, but a computer may run many services simultaneously.

For example, one machine could host an HTTP server, a database, and a custom TCP service.

How does incoming traffic reach the right application?

Transport protocols such as TCP use port numbers to identify application-level endpoints on a host.

For example:

127.0.0.1:3000

This consists of:

  • 127.0.0.1: the IPv4 loopback address.
  • 3000: the port number.

The loopback address refers to the local machine. Connecting to it does not require contacting a remote computer.

A port is a transport-layer concept, not an HTTP concept. HTTP commonly uses port 80 for plain HTTP and 443 for HTTPS, but custom services can listen on other ports.

Node.js servers commonly use ports such as 3000, 4000, or 8080 during development.

2.3 IP address versus port

Think of the IP address as identifying the destination host or interface and the port as identifying the service endpoint on that host.

          Client
             |
             | Connect to 192.0.2.10:4000
             v
       +------------------+
       | Host             |
       | 192.0.2.10       |
       |                  |
       | Port 80   HTTP   |
       | Port 443  HTTPS  |
       | Port 4000  TCP app|
       +------------------+

These port assignments are illustrative. A service can listen on a different port if the application and network configuration permit it.

When you build a TCP server with Node.js, you specify the address and port on which it should listen. A client then connects to that endpoint.

But what happens when the application knows a hostname instead of an IP address?

That is where DNS enters the picture.

Part 3: DNS Concepts

3.1 Why was DNS needed?

Imagine having to remember an IP address for every website or service you wanted to use.

Instead of typing:

example.com

you would need to know a numeric network address.

That becomes increasingly difficult as the number of services grows and as those services change their infrastructure.

The Domain Name System, or DNS, provides a distributed naming system that associates domain names with records describing network-related information. Its most familiar function is resolving a hostname to an IP address. MDN’s DNS glossary provides a useful overview.

For example:

api.example.com
        |
        | DNS resolution
        v
An IP address or a set of IP addresses
        |
        v
Network connection

The result is not necessarily one permanent IP address. A hostname can map to multiple addresses, and its records may change over time.

DNS therefore separates the application’s stable naming convention from the changing details of network addressing.

3.2 DNS is a distributed hierarchy

DNS is not one giant database located on a single server. It is a hierarchy of delegated authorities and resolvers.

A simplified view of a typical DNS lookup is:

             Your Application
                    |
                    v
             Operating System
                    |
                    v
            Recursive Resolver
                    |
                    v
                Root DNS
                    |
                    v
               TLD DNS
              (.com, etc.)
                    |
                    v
            Authoritative DNS
                    |
                    v
        Records for example.com
                    |
                    v
             IP Address Result

This diagram illustrates the general hierarchy, not a sequence every lookup must follow. A recursive resolver may already have a cached answer and can skip parts of the hierarchy.

Let’s explore the components.

Stub resolver: The component on the client machine that requests name resolution through the system’s configured facilities.

Recursive resolver: A resolver that obtains the answer on behalf of the client. It can query other DNS servers and cache results.

Root DNS servers: The starting point for finding authoritative information about top-level domains when the resolver lacks a suitable cached answer.

Top-level domain (TLD) servers: Servers responsible for information concerning domain endings such as .com, .org, and .net.

Authoritative DNS servers: Servers that provide the authoritative DNS records for a domain or delegated zone.

The exact implementation details depend on the resolver, operating system, and network configuration. The hierarchy explains how DNS can distribute responsibility for the global namespace rather than requiring one central lookup database.

3.3 What actually happens when you look up a domain?

Suppose you request:

api.example.com

A typical resolution process may proceed as follows.

First, the client checks available local resolution mechanisms and caches.

If there is no usable result, the system may ask its configured recursive resolver.

If that resolver has a cached answer, it can return the answer directly. Otherwise, it may query the DNS hierarchy to find the relevant authoritative records.

The answer is returned to the requesting system, where it may be cached according to applicable rules.

The resulting address can then be used by a network client to establish a connection.

Application asks for api.example.com
                   |
                   v
        Is a usable answer available?
              /            \
            Yes             No
             |               |
             v               v
       Return answer    Ask resolver
                             |
                             v
                     Find authoritative
                           answer
                             |
                             v
                       Cache result
                             |
                             v
                       Return answer

DNS caching improves efficiency because repeated requests do not always require a new trip through the resolution hierarchy.

But cached answers have a limited lifetime in accordance with their Time To Live, or TTL, and the resolver’s caching rules.

The TTL is not a guarantee that every cache will forget the result at exactly the same moment. It governs how long a DNS record may ordinarily be cached before needing revalidation, with details depending on the record and resolver behavior.

3.4 DNS does more than resolve IP addresses

DNS contains different record types for different purposes.

Record typePurpose
AMaps a hostname to an IPv4 address
AAAAMaps a hostname to an IPv6 address
CNAMEIdentifies an alias for another DNS name
MXIdentifies mail servers for a domain
TXTStores text records, often used for domain verification and email policies
NSIdentifies authoritative name servers for a zone
PTRSupports reverse DNS lookups
SRVIdentifies service endpoints, including target hostnames and ports

These records are defined by DNS standards and are used by different kinds of applications.

For example, a DNS lookup for a web application’s hostname may request an A or AAAA record, while an email system may request MX records.

An important distinction is that DNS records do not all contain IP addresses. Some contain names, mail-server priorities, service ports, or text data.

Part 4: The Node.js DNS Module

Node.js provides the built-in node:dns module for name-resolution operations.

You can use its callback-based APIs:

import dns from "node:dns";

Or use the Promise-based API:

import * as dns from "node:dns/promises";

For modern asynchronous examples, the Promise-based API is convenient because we can use async and await.

However, an important engineering distinction exists within this module: dns.lookup() and dns.resolve*() do not perform name resolution in the same way. The official Node.js DNS documentation explicitly describes this distinction.

4.1 dns.lookup(): Use the system’s normal resolution mechanism

The lookup() method resolves a hostname using the operating system’s name-resolution facilities.

import { lookup } from "node:dns/promises";

async function findAddress() {
  const result = await lookup("example.com");

  console.log("IP address:", result.address);
  console.log("IP family:", result.family);
}

findAddress().catch(console.error);

The address returned can vary because DNS results, address-family preferences, and network configuration may vary.

The family value identifies the address family: 4 for IPv4 or 6 for IPv6.

What makes lookup() special is that it uses system name resolution. That may involve DNS, but it can also involve local mechanisms such as a hosts file or operating-system configuration.

For example, a local hosts-file entry can resolve a name without a DNS query to a remote DNS server.

This is why dns.lookup() is often appropriate when an application needs the same sort of hostname resolution used by ordinary system applications.

4.2 Resolve multiple addresses

A hostname can resolve to several addresses. To inspect all addresses returned by the system resolver:

import { lookup } from "node:dns/promises";

const addresses = await lookup("example.com", {
  all: true,
});

console.log(addresses);

The result is an array containing objects with address and family properties.

An example of the shape is:

[
  { address: "192.0.2.10", family: 4 },
  { address: "2001:db8::10", family: 6 }
]

These values are illustrative and are not actual resolution results for example.com.

The important point is that all: true returns the available address results rather than a single selected address. The operating system and resolver configuration influence which addresses are returned and in what order.

4.3 Restrict the address family

You can request a particular address family.

import { lookup } from "node:dns/promises";

// Request an IPv4 result.
const ipv4 = await lookup("example.com", {
  family: 4,
});

console.log(ipv4.address);

For IPv6:

const ipv6 = await lookup("example.com", {
  family: 6,
});

console.log(ipv6.address);

A lookup can fail if a usable result for the requested address family is unavailable. Production code should handle failures instead of assuming every hostname supports both families.

4.4 dns.resolve4() and dns.resolve6()

Sometimes you want to issue DNS queries for a specific kind of record.

The resolve4() method queries for IPv4 A records:

import { resolve4 } from "node:dns/promises";

const addresses = await resolve4("example.com");

console.log(addresses);

The resolve6() method queries for IPv6 AAAA records:

import { resolve6 } from "node:dns/promises";

const addresses = await resolve6("example.com");

console.log(addresses);

These functions use the DNS protocol to request the relevant records from DNS infrastructure.

That distinguishes them from lookup(), which uses the operating system’s resolution facilities.

The practical choice depends on your task.

If you need to resolve a hostname the way ordinary applications on the system typically do, use lookup(). If you specifically need DNS records, use the corresponding resolve*() method.

4.5 dns.resolveMx(): Looking up mail servers

DNS also supports mail-server records.

import { resolveMx } from "node:dns/promises";

async function findMailServers(domain) {
  const records = await resolveMx(domain);

  console.log(records);
}

findMailServers("gmail.com").catch(console.error);

An MX result contains information such as a mail server’s priority and exchange hostname.

Lower priority values generally indicate preferred servers when multiple MX records are present.

The exact records depend on the domain’s current DNS configuration. A failed query does not necessarily mean the domain is invalid; the domain may lack the requested record or the DNS query itself may have failed.

4.6 Other useful DNS operations

The module provides additional methods for specialized lookups.

resolveTxt() retrieves TXT records. These are commonly used for domain verification and email policy records.

resolveNs() retrieves name-server records.

resolveSrv() retrieves service records that can include a target hostname and port.

reverse() performs a reverse DNS query for a given IP address.

lookupService() uses system facilities to look up a hostname and service associated with an address and port.

You do not need to memorize all these methods immediately. The important idea is that DNS is more than a function that converts a website into an IP address. It is a record system used for many forms of service discovery and domain configuration.

4.7 The important DNS performance detail: libuv’s thread pool

There is a useful connection to our Node.js architecture tutorial.

Although dns.lookup() is asynchronous from JavaScript’s point of view, Node.js implements it using the operating system’s name-resolution facilities, commonly through getaddrinfo(). On the standard implementation, this work uses libuv’s worker thread pool.

By contrast, the DNS query functions such as resolve4() and resolveMx() communicate with DNS servers asynchronously without using that same libuv worker pool.

This distinction can matter when an application performs many name lookups or other operations that also depend on the shared worker pool. The official Node.js documentation describes these implementation differences and their potential performance implications.

The takeaway is that “asynchronous” describes how the operation is exposed to JavaScript, but it does not necessarily tell you which underlying mechanism performs it.

Part 5: DNS Resolution Is Not a Network Connection

This distinction is fundamental.

Suppose this code succeeds:

import { lookup } from "node:dns/promises";

const result = await lookup("example.com");

console.log(result.address);

It means the system resolved the hostname to an address.

It does not mean:

  • The server is accepting connections.
  • The destination port is open.
  • The server is running.
  • The application protocol is available.
  • The client is authorized to access the server.

DNS provides naming information, not a guarantee that a service is reachable.

The next step is usually to establish a network connection to an IP address and port.

For TCP, Node.js provides the node:net module.

Part 6: TCP Concepts

6.1 Why isn’t an IP address enough?

IP provides addressing and packet delivery across networks, but application data often requires more than individual packet delivery.

Imagine sending the following message:

Transfer 100 units

If the data arrives out of order, is lost, or is duplicated without detection, the application could receive an incorrect result.

Applications that require a reliable ordered sequence of bytes need a transport protocol that provides those guarantees.

TCP, or Transmission Control Protocol, is one such protocol.

TCP provides a reliable, ordered, bidirectional byte-stream service between two endpoints. It detects packet loss and uses acknowledgments and retransmissions to recover from missing data. It also provides flow-control and congestion-control mechanisms. These properties are defined in RFC 9293, the TCP specification.

6.2 TCP is connection-oriented

Before ordinary TCP application data is exchanged, the peers establish a connection.

This is different from simply sending an independent datagram to an IP address.

A TCP connection has state maintained by the communicating endpoints. The connection is associated with source and destination addresses and port numbers.

A simplified identifier for a connection is:

Source IP
Source Port
Destination IP
Destination Port
Transport Protocol

The source port is commonly selected from an ephemeral port range for a client connection, while the server listens on a configured port.

The exact port range and allocation rules vary by operating system.

For example:

Client: 192.0.2.20:51520
            |
            | TCP connection
            v
Server: 192.0.2.10:4000

These addresses are illustrative. The client and server have different endpoints, identified by their addresses and ports.

6.3 TCP does not preserve messages

This is one of the most important networking concepts for application developers.

TCP provides a byte stream, not a message protocol.

Suppose a client writes:

socket.write("Hello");
socket.write("World");

You might expect the server to receive two distinct messages, "Hello" and "World".

But TCP does not guarantee that.

The receiving application might observe:

"HelloWorld"

or:

"Hel"
"loWor"
"ld"

or some other grouping of bytes.

These are illustrative possibilities. The actual boundaries depend on transport, buffering, and how the application consumes data.

TCP guarantees ordered delivery of the byte stream, not preservation of individual application write() boundaries.

This is why applications built on TCP need their own message-framing rules.

HTTP/1.1, for example, uses a defined message format and framing rules over the TCP byte stream. A custom TCP application might use newline-delimited messages, a fixed-size header, or a length prefix.

We will implement a newline-delimited protocol later.

6.4 How does TCP establish a connection?

TCP commonly establishes a new connection through a three-way handshake.

The basic exchange is:

                 Client                     Server
                   |                           |
                   | -------- SYN -----------> |
                   |                           |
                   | <------ SYN + ACK ------- |
                   |                           |
                   | -------- ACK -----------> |
                   |                           |
                   |       Connection          |
                   |       Established         |

The messages have specific meanings.

SYN begins the connection-establishment process and communicates the client’s initial sequence information.

SYN-ACK acknowledges the incoming request and communicates the server’s sequence information.

ACK acknowledges the server’s response and completes the ordinary three-way handshake.

The result is a connection in which the TCP endpoints have established the necessary state to exchange data.

This description is intentionally simplified. Real TCP establishment includes sequence numbers, negotiated options, state transitions, and handling for retries or failures.

6.5 What does TCP guarantee once data is sent?

TCP provides a reliable, ordered byte stream.

If packets are lost, TCP can retransmit data. It uses sequence information to track the stream and acknowledgments to determine which data has been received.

If segments arrive out of order, TCP can reorder the received data before providing the ordered stream to the application.

TCP also uses flow control so that one endpoint does not overwhelm the receiving endpoint’s available buffer capacity. Congestion control helps regulate transmission according to conditions in the network.

However, TCP does not guarantee that the remote application has processed a message merely because the local socket accepted a write.

For example:

socket.write("Transfer 100 units");

This submits bytes to the local stream. It does not prove that the remote application received the complete message, validated it, or committed any corresponding business operation.

If the application needs confirmation of processing, it must define an application-level response or acknowledgment.

That distinction is crucial in systems that process payments, orders, or commands.

Part 7: The Node.js net Module

Node.js provides the built-in node:net module for TCP and inter-process communication sockets.

For our purposes, we will focus on TCP.

Import the module:

import * as net from "node:net";

Two functions form the foundation of a TCP client-server application:

net.createServer()

creates a TCP server.

net.createConnection()

creates a TCP client connection.

Both APIs provide Socket objects that expose events and methods for sending and receiving data.

The official Node.js net documentation describes these APIs in detail.

7.1 A Socket is a Duplex stream

A Node.js TCP Socket is a Duplex stream. It can be read from and written to.

            Client Socket
             /         \
            v           v
       Read Data      Write Data
            |           |
            v           v
       Incoming       Outgoing
          Bytes         Bytes

The same connection can receive bytes and send bytes.

This connects directly to our Streams tutorial: the socket is not a special kind of JavaScript object unrelated to streams. It exposes the readable and writable stream interfaces alongside networking-specific properties and events.

7.2 Building a minimal TCP server

Create a file called tcp-server.mjs:

import { createServer } from "node:net";

const server = createServer((socket) => {
  // This callback runs when a client connects.
  console.log("Client connected:", socket.remoteAddress, socket.remotePort);

  // Display incoming data as UTF-8 text for this demo.
  socket.setEncoding("utf8");

  socket.on("data", (chunk) => {
    console.log("Received:", chunk);

    // Send data back to the client.
    socket.write(`Server received: ${chunk}`);
  });

  socket.on("end", () => {
    // The client has finished sending its readable data.
    console.log("Client finished sending data.");
  });

  socket.on("error", (error) => {
    console.error("Socket error:", error.message);
  });

  socket.on("close", () => {
    console.log("Client connection closed.");
  });
});

// Listen only on the local machine for this exercise.
server.listen(4000, "127.0.0.1", () => {
  console.log("TCP server listening on 127.0.0.1:4000");
});

server.on("error", (error) => {
  console.error("Server error:", error.message);
});

Start the server:

node tcp-server.mjs

Expected initial output:

TCP server listening on 127.0.0.1:4000

Notice what the server does. It creates a listening socket, waits for clients, and provides a new socket object for each accepted connection.

The server’s callback runs for each client connection. The socket then emits events as data arrives and the connection changes state.

One caution: this is a minimal demonstration of the socket API, not a complete application protocol. It echoes whatever text chunks it receives. Because TCP does not preserve message boundaries, the echoed chunks should not be treated as individual messages in a production protocol.

We will correct that design below.

Part 8: Build a TCP Client

A client uses createConnection() to connect to a TCP server.

Create tcp-client.mjs:

import { createConnection } from "node:net";

// Connect to the local TCP server.
const socket = createConnection(
  { host: "127.0.0.1", port: 4000 },
  () => {
    console.log("Connected to the server.");

    // Send a short message after the connection is established.
    socket.write("Hello from the client");
  },
);

// Decode incoming data as text for this demonstration.
socket.setEncoding("utf8");

socket.on("data", (chunk) => {
  console.log("Server replied:", chunk);

  // End our outgoing side after receiving the echo.
  socket.end();
});

socket.on("end", () => {
  console.log("Server finished sending data.");
});

socket.on("close", () => {
  console.log("Connection closed.");
});

socket.on("error", (error) => {
  console.error("Client error:", error.message);
});

Keep the server running, then start this client in another terminal:

node tcp-client.mjs

The client connects to 127.0.0.1:4000, writes its message, receives the server’s response, and closes its outgoing side.

The exact order and grouping of stream events can depend on the connection and implementation details, but the essential behavior is straightforward: the client establishes a TCP connection and exchanges data through its socket.

What is happening under the hood?

When createConnection() is called, Node.js initiates a TCP connection to the requested endpoint. The callback runs once the connection has been established.

The client then sends data through socket.write().

The server receives those bytes through its socket and invokes the registered data listener. It sends its reply through its writable side, and the client receives that reply in its own data listener.

This is raw TCP. There are no HTTP methods, headers, request targets, or status codes unless we deliberately build an application protocol that includes them.

Part 9: The TCP Framing Problem, Solved Properly

The minimal example is useful, but it exposes an important limitation.

What if the client sends two messages?

socket.write("Hello");
socket.write("World");

TCP may deliver the bytes to the server as one chunk or several chunks.

If the server treats each data event as a separate application message, it may interpret the input incorrectly.

We need a way to identify where one message ends and the next begins.

9.1 Choose a simple protocol: newline-delimited messages

For our example, let’s define a simple protocol:

Every message ends with a newline character (\n).

The client can send:

Hello
How are you?

Each line represents one application message. The receiver can accumulate chunks until it finds the newline delimiter.

The data flow becomes:

           TCP Byte Stream
                  |
                  v
          Incoming Chunk 1
                  |
                  v
          Append to Buffer
                  |
                  v
        Complete Newline Found?
             /          \
           No            Yes
           |              |
           v              v
    Wait for more     Extract Message
                          |
                          v
                   Process Message

This approach is easy to understand and works well for a simple text protocol. Real protocols often use other framing methods, such as fixed-size headers or length-prefixed messages.

9.2 Implement the line-based TCP server

Create line-server.mjs:

import { createServer } from "node:net";

const server = createServer((socket) => {
  console.log("Client connected.");

  socket.setEncoding("utf8");

  // Stores incomplete data across socket data events.
  let pending = "";

  socket.write("Connected. Send one message per line.\n");

  socket.on("data", (chunk) => {
    // A chunk may contain a partial message or several messages.
    pending += chunk;

    let newlineIndex;

    // Process every complete line currently available.
    while ((newlineIndex = pending.indexOf("\n")) !== -1) {
      const line = pending
        .slice(0, newlineIndex)
        .replace(/\r$/, "");

      // Keep the data after the newline for the next iteration.
      pending = pending.slice(newlineIndex + 1);

      // Ignore empty lines in this protocol.
      if (line.length === 0) {
        continue;
      }

      console.log("Message:", line);

      // Reply once per complete application message.
      socket.write(`Server received: ${line}\n`);
    }
  });

  socket.on("end", () => {
    // If the client ended without a final newline, decide
    // how to handle the remaining incomplete message.
    if (pending.length > 0) {
      socket.write(`Server received final data: ${pending}\n`);
    }

    // Finish the server's outgoing side.
    socket.end();
  });

  socket.on("error", (error) => {
    console.error("Socket error:", error.message);
  });
});

server.listen(4000, "127.0.0.1", () => {
  console.log("Line server listening on 127.0.0.1:4000");
});

The essential improvement is that the server no longer assumes every incoming chunk is one complete message.

It appends the chunk to pending, finds delimiters, extracts all complete messages, and preserves any remaining partial message for the next data event.

For simplicity, the example treats whatever remains when the client ends its output as a final message. A stricter protocol could instead reject an unterminated final message.

9.3 Implement a matching client

Create line-client.mjs:

import { createConnection } from "node:net";

const socket = createConnection(
  { host: "127.0.0.1", port: 4000 },
  () => {
    console.log("Connected.");

    // The newline delimiters define the application messages.
    socket.write("Hello from the client\n");
    socket.write("TCP sends bytes, not messages\n");

    // Tell the server that no more client data will follow.
    socket.end();
  },
);

socket.setEncoding("utf8");

socket.on("data", (chunk) => {
  // The server's output may also arrive in arbitrary chunks.
  process.stdout.write(chunk);
});

socket.on("end", () => {
  console.log("Server finished sending data.");
});

socket.on("error", (error) => {
  console.error("Client error:", error.message);
});

Start the server:

node line-server.mjs

Then, in another terminal:

node line-client.mjs

A representative client-side output is:

Connected.
Connected. Send one message per line.
Server received: Hello from the client
Server received: TCP sends bytes, not messages
Server finished sending data.

The order of the connection message and server greeting can vary depending on when the client and server process their respective events. The important result is that the application treats newline-delimited messages as separate messages even though TCP itself only provides a continuous byte stream.

This is a fundamental rule of network protocol design: your application must define message boundaries when its transport protocol does not provide them.

Part 10: Important Socket Events and Methods

Node.js sockets inherit stream behavior and add networking-specific operations.

The following methods and events are among the most useful when implementing a TCP client or server.

APIPurpose
socket.write()Submit data to the outgoing stream
socket.end()Finish the writable side, optionally submitting a final chunk
socket.destroy()Destroy the socket and release its resources
socket.pause()Stop flowing-mode readable delivery temporarily
socket.resume()Resume readable delivery
socket.setEncoding()Decode incoming data into strings
socket.setTimeout()Configure an idle timeout event
socket.setNoDelay()Control TCP’s Nagle algorithm
socket.setKeepAlive()Configure TCP keep-alive probes
socket.remoteAddressThe remote endpoint’s IP address
socket.remotePortThe remote endpoint’s port
socket.localAddressThe local endpoint’s IP address
socket.localPortThe local endpoint’s port

Let’s examine some of these in more detail.

10.1 socket.end() versus socket.destroy()

These methods have different meanings.

socket.end() signals that the application has finished writing data. The socket can still receive data from the remote endpoint while that side of the connection remains open.

socket.destroy() terminates the socket. It is appropriate when the connection must be torn down, including certain error and cancellation cases.

For example:

socket.end("Goodbye\n");

This sends the final piece of data and finishes the outgoing side. It is not the same as abruptly destroying the connection.

TCP supports half-closed connections, in which one direction has finished sending data while the other direction can still send data. This behavior is useful for protocols in which the client signals the end of its request while still expecting a response.

10.2 Idle timeouts

A TCP socket can be configured with a timeout:

socket.setTimeout(5000);

socket.on("timeout", () => {
  console.log("Socket has been idle for too long.");

  // A timeout event does not automatically close the socket.
  socket.destroy();
});

The timeout is expressed in milliseconds.

This mechanism reports inactivity; it does not inherently guarantee that the remote application is healthy or that the network is fully available.

Node.js documents that the timeout event does not automatically destroy the socket. The application must close or destroy it if that is the intended policy. See the Node.js Socket timeout documentation.

10.3 TCP keep-alive is not the same as HTTP keep-alive

You will see the term “keep-alive” in both TCP networking and HTTP.

They refer to different mechanisms.

TCP keep-alive uses transport-level probes to help detect certain conditions involving an idle connection.

HTTP persistent connections allow multiple HTTP exchanges to reuse a transport connection under the rules of the relevant HTTP version.

Enabling TCP keep-alive does not automatically implement an HTTP session, guarantee that the peer application is responding, or make a broken application-level protocol valid.

10.4 The socket error event

Socket operations can fail because a connection is refused, a peer disconnects, the network fails, or another problem occurs.

A network application should register error handling deliberately:

socket.on("error", (error) => {
  console.error("Socket error:", error.message);
});

In larger applications, errors also need to be propagated to the surrounding connection lifecycle so that resources are released and pending operations are handled appropriately.

Do not assume that every connection attempt will succeed.

Part 11: Backpressure in TCP Socket Applications

This connects to our Streams tutorial.

The return value of socket.write() indicates whether the writable buffer is below the threshold that permits normal continued writing.

const canContinue = socket.write(chunk);

If it returns false, the producer should stop sending additional chunks until the writable side emits drain.

A simplified pattern is:

import { once } from "node:events";

async function sendChunks(socket, chunks) {
  for (const chunk of chunks) {
    // Submit the next chunk to the outgoing TCP stream.
    const canContinue = socket.write(chunk);

    if (!canContinue) {
      // Wait until the writable buffer can accept more data.
      await once(socket, "drain");
    }
  }

  // Finish writing after every chunk has been submitted.
  socket.end();
}

This demonstrates how backpressure affects a network client. If the remote side or underlying network cannot keep up, data may accumulate in the local writable buffer.

By respecting the return value of write(), the application avoids continually submitting data after the stream has signaled that its buffer is under pressure.

For production use, this helper must also coordinate errors, connection closure, and cancellation. An error or disconnect can occur while the sender is waiting for drain. For common source-to-socket pipelines, Node.js stream utilities can simplify this handling.

Backpressure is especially important when one service forwards large files to another service over TCP. Without it, a slow destination can cause excessive memory consumption even though the source is being read incrementally.

Part 12: How DNS and TCP Work Together in a Real Application

We can now connect the two main parts of this tutorial.

Suppose your Node.js program needs to connect to:

api.example.com:4000

The logical sequence is:

             Hostname and Port
                     |
                     v
                DNS Lookup
                     |
                     v
              Obtain IP Address
                     |
                     v
             TCP Connection Setup
                     |
                     v
              Connection Established
                     |
                     v
              Exchange TCP Bytes

Let’s implement this sequence directly.

Create connect-to-host.mjs:

import { lookup } from "node:dns/promises";
import { createConnection } from "node:net";

async function connectToHost(hostname, port) {
  try {
    // Resolve the hostname through the system resolver.
    const result = await lookup(hostname);

    console.log("Resolved address:", result.address);
    console.log("Address family:", result.family);

    // Establish a TCP connection to the returned IP address.
    const socket = createConnection(
      { host: result.address, port },
      () => {
        console.log("TCP connection established.");
        console.log(
          "Connected to:",
          socket.remoteAddress,
          socket.remotePort,
        );

        // Close the connection after demonstrating connectivity.
        socket.end();
      },
    );

    socket.on("error", (error) => {
      console.error("Connection failed:", error.message);
    });

    socket.on("close", () => {
      console.log("Socket closed.");
    });
  } catch (error) {
    // DNS resolution can fail before a socket is created.
    console.error("DNS lookup failed:", error.message);
  }
}

connectToHost("example.com", 80);

This example illustrates DNS resolution followed by TCP connection establishment. It is not sending an HTTP request. It simply creates a TCP connection to the selected endpoint and then closes the connection.

The external server may refuse connections or network access may be unavailable, so its success is not guaranteed in every environment.

Also, a resolved hostname can have multiple addresses. Real connection strategies may choose among multiple results, retry other addresses, or use the networking API’s own hostname-resolution behavior.

A practical detail about net.createConnection()

You do not always need to call dns.lookup() yourself before using net.createConnection().

You can pass a hostname directly:

import { createConnection } from "node:net";

const socket = createConnection({
  host: "example.com",
  port: 80,
});

Node.js’s networking APIs normally resolve the hostname through their configured lookup mechanism.

The manual version is useful for learning because it exposes the individual stages. In ordinary application code, combining the stages into one API call may be simpler.

Part 13: Common Networking Errors and What They Mean

When working with DNS and TCP, it is helpful to recognize common failures.

ErrorCommon interpretation
ENOTFOUNDName resolution failed or no usable result was found
ECONNREFUSEDThe connection attempt was actively refused
ETIMEDOUTAn operation or connection attempt timed out
ECONNRESETThe connection was reset by the peer or network path
EHOSTUNREACHThe destination host was reported as unreachable
ENETUNREACHThe destination network was reported as unreachable
EADDRINUSEThe requested local address or port is already in use

The precise code and behavior depend on the operating system and circumstances.

For example, ENOTFOUND does not prove that a domain name does not exist. Resolution can fail for other reasons, including resolver or resource problems. The Node.js DNS documentation explicitly warns about this.

Likewise, a successful DNS lookup does not prove that the TCP port is reachable. The server may not be running, the port may be blocked, or the network route may be unavailable.

A useful debugging strategy is to isolate the layers:

First, check name resolution.

Next, check whether the TCP endpoint can be reached.

Finally, check whether the expected application protocol is working.

This avoids treating every network failure as a DNS problem or every connection failure as an application bug.

Part 14: Important Distinctions

Before we conclude, let’s connect the most important concepts into one model.

DNS is not TCP

DNS resolves names to records. TCP establishes a connection between network endpoints and carries an ordered stream of bytes.

A DNS answer can be valid even when the service at the resulting address is unavailable.

TCP is not HTTP

TCP transports bytes. HTTP defines application-level semantics and message framing.

A TCP socket does not automatically provide HTTP methods, request headers, status codes, or paths.

A socket write is not proof of application success

Calling socket.write() means the application has submitted data to the writable stream. It does not prove that the remote application has processed it.

If the application requires an acknowledgment, it must design a protocol-level response.

TCP chunks are not application messages

The data events emitted by a TCP socket do not preserve the sender’s write boundaries. An application must define how to reconstruct messages.

This can be done with delimiters, length prefixes, fixed-size records, or another framing protocol.

Asynchronous does not mean the work is free

DNS lookups, network connections, and socket processing have real costs. Node.js allows the application to handle them through an event-driven interface, but CPU-heavy synchronous work, excessive buffering, and poorly controlled concurrency can still cause performance problems.

These distinctions form the foundation for understanding networking systems built on Node.js.

Conclusion

We began with a simple question: how does a Node.js application communicate with another computer when all it knows is a hostname?

We learned that DNS provides a distributed naming system, that IP addresses identify network endpoints, and that port numbers identify transport-level services.

We explored the DNS hierarchy and record types, then examined Node.js’s dns.lookup() versus the DNS query methods. The distinction matters because these APIs use different name-resolution mechanisms.

We then moved to TCP and learned how a connection is established, why TCP provides a reliable ordered byte stream, and why it does not preserve application-message boundaries. Finally, we built TCP servers and clients with node:net, implemented a line-based protocol, and connected DNS resolution to TCP connection establishment.

The key takeaway is that DNS answers where a name can lead, TCP provides a connection to an endpoint, and the application protocol defines what the exchanged bytes mean.

Once you understand those separate responsibilities, the behavior of Node.js networking APIs becomes much more predictable, and you gain the foundation needed to reason about HTTP, HTTPS, network protocols, and distributed applications.

Official Documentation and Further Reading

  • Node.js DNS Module
  • Node.js Networking Module
  • RFC 9293: Transmission Control Protocol
  • MDN: Domain Name System
  • MDN: What Is a Domain Name?
  • Node.js Streams
  • Node.js HTTP Module

Support

Buy author a coffee

Support
SummarizeSendShareTweetScan
Previous Post

HTTPS and TLS in Node.js Explained : Encryption, Certificates, Handshakes, and Secure Servers

Next Post

Node.js EventEmitter Explained: Events, Listeners, and Event-Driven Architecture

Neuraldemy

Neuraldemy

This is Neuraldemy support. Subscribe to our YouTube channel for more.

Related Posts

Node.js EventEmitter Explained: Events, Listeners, and Event-Driven Architecture

HTTPS and TLS in Node.js Explained : Encryption, Certificates, Handshakes, and Secure Servers

HTTP Explained: History, Protocol Internals, and Building a Web Server with Node.js

Node.js Streams Explained: Readable, Writable, Transform, and Backpressure

Node.js Buffer Explained: Bytes, Binary Data, and Character Encoding

Node.js File System Explained: A Beginner’s Guide to fs and fs/promises

Next Post

Node.js EventEmitter Explained: Events, Listeners, and Event-Driven Architecture

Support

Buy author a coffee

Support

Support Neuraldemy

Newsletter

Top rated products

  • Probability and Statistics for Machine Learning and Data Science Probability and Statistics for Machine Learning and Data Science
    Rated 5.00 out of 5
    30.00$ Original price was: 30.00$.12.99$Current price is: 12.99$.
  • SVM Notes: Optimization & Implementation SVM Notes: Optimization & Implementation
    Rated 5.00 out of 5
    9.99$ Original price was: 9.99$.4.99$Current price is: 4.99$.
  • spidy mail hunter software Spidy Mail Hunter: Powerful And Intelligent Website & PDF Email Extractor Software (Windows App)
    Rated 4.78 out of 5
    149.00$ Original price was: 149.00$.49.00$Current price is: 49.00$.
  • Linear Algebra For Machine Learning And Data Science Linear Algebra For Machine Learning And Data Science
    Rated 4.71 out of 5
    40.00$ Original price was: 40.00$.20.99$Current price is: 20.99$.
  • Javascript DOM Notes Javascript Document Object Model Notes And Codes (Free) 0.00$

Recent Posts

Node.js EventEmitter Explained: Events, Listeners, and Event-Driven Architecture

DNS And TCP in Node.js Explained

HTTPS and TLS in Node.js Explained : Encryption, Certificates, Handshakes, and Secure Servers

October 2026
M T W T F S S
 1234
567891011
12131415161718
19202122232425
262728293031  
« Aug    

Neuraldemy

Neuraldemy

Neuraldemy

Software Development

Neuraldemy helps you learn ML, AI, Web Dev and data science from scratch. Addtionally, we also provide web development and hosting services for businesses, and individuals

[convertkit form=7049059]
  • Get Started
  • Contact
  • Book Services
  • Privacy Policy
  • Terms Of Use
  • Support Portal
  • Managed Hosting Terms
  • Web Development Terms
  • Refund Policy
Neuraldemy

© 2026 - A learning platform by Odist Magazine

Welcome Back!

Login to your account below

Forgotten Password? Sign Up

Create New Account!

Fill the forms below to register

*By registering into our website, you agree to the Terms & Conditions and Privacy Policy.
All fields are required. Log In

Retrieve your password

Please enter your username or email address to reset your password.

Log In
No Result
View All Result
Join Our WhatsApp Channel
  • Home
  • Tutorials
    • Machine Learning
    • Python
    • Web Development
    • Javascript
    • Mathematics
    • Deep Learning
    • Artificial Intelligence
  • Store
  • Our Services
    • Managed Hosting Services
    • Web Development Services
    • Digital Marketing Services
    • Website Security Checker
    • Video Editing Services
  • Our Softwares
    • JaggoRe AI
    • Email Extractor
    • TimeWell App
  • Login
  • Sign Up
  • Cart
Order Details

© 2026 - A learning platform by Odist Magazine

This website uses cookies. By continuing to use this website you are giving consent to cookies being used.
0