“Connected” means the transport handshake completed.
For TCP, a client’s successful connect means the local stack established a connection with a peer endpoint. It does not prove that TLS negotiation succeeded, that the peer parsed a request, or that the application returned a useful response. Those are later steps, with their own evidence.
Record the endpoint pair, timestamp, process, and the exact event that was observed. A client log, server log, socket table, and packet capture each describe a different vantage point. Align them before treating a gap between two events as proof that a specific component dropped the connection.
The handshake synchronizes state at both endpoints.
The usual open begins with a client SYN, a server SYN-ACK, and a client ACK. Sequence numbers let both TCP implementations track ordered byte positions. The final ACK usually completes establishment, though packet captures can show data accompanying an acknowledgment or packet aggregation that makes the exchange look different.
Requests a connection and proposes its initial sequence number.
Acknowledges the client sequence and proposes its own.
Acknowledges the server sequence; the connection is established.
Now the transport can carry bytes in either direction.
TCP preserves byte order, not your write boundaries.
If an application writes "HEL" and then "LO\n", the receiving
program might read both together, in smaller pieces, or split at another byte. TCP
presents an ordered stream. The application protocol must define where a message ends—for
example, with a length prefix, delimiter, or connection close.
Likewise, a packet capture shows transport segments at one capture point. Offload features can change how host captures appear, and segmentation does not map one-to-one to application reads. Diagnose the protocol framing at the application layer and use captures to answer transport questions.
- Sender writes
HELthenLO\n- Receiver may read
HELLO\n,HE+LLO\n, or another split- Application needs
- A message boundary rule and a loop that handles partial reads.
FIN closes one sending direction; the other direction can continue.
TCP is full duplex. When a client has finished sending its request, it can send FIN while continuing to read. The server observes end-of-input on that direction, sends its response, and then closes its own sending direction. This is a half-close: each side has an independent byte stream and close state.
The endpoint that actively closes a connection commonly passes through TIME-WAIT after the orderly exchange. It waits so delayed segments from the old connection cannot be
mistaken for a new one and so the final acknowledgment can be retransmitted if needed. Either
endpoint can be the active closer; the role depends on which side sends the first FIN.
The application sends its request.
Client will send no more bytes.
Server can still finish the reply.
Orderly shutdown; the active closer enters TIME-WAIT.
A reset interrupts a connection; a refusal usually rejects the open.
A TCP RST abruptly rejects or aborts state. It can appear when a listener is absent, an application aborts a socket, or a network device rejects traffic, depending on the point and circumstances. “Connection refused” commonly corresponds to an active rejection during connection setup. A timeout means no conclusive response arrived before the caller’s deadline.
These errors narrow the next question, but they do not uniquely name the cause. Compare client error, server listener state, route and firewall evidence, and packet timing. A middlebox may generate a response that looks like it came from the destination.
| Observation | What it suggests | Next evidence |
|---|---|---|
| Connect refused | An endpoint actively rejected the open. | Check listener address and port; identify whether a proxy or firewall answered. |
| Reset after connect | Established state was aborted somewhere on the path. | Correlate RST timing with application logs and a capture at each available side. |
| Connect timed out | No usable response reached the caller before its deadline. | Check the route, filtering, peer health, and timeout budget from that client. |
Watch a real loopback connection half-close.
The paired examples open an ephemeral listener on 127.0.0.1. The client
connects, sends HELLO, and closes only its write side. The server observes
EOF on that direction, sends READY, and closes. No public host or external
target is involved.
Both programs use loopback and demonstrate EOF after a half-close.
import { createConnection, createServer, type AddressInfo } from 'node:net';
async function main() {
// This example binds only to loopback and sends no traffic beyond this process.
const server = createServer((socket) => {
socket.setEncoding('utf8');
console.log(`server accepted one local connection from port ${socket.remotePort}`);
socket.on('data', (chunk) => {
console.log(`server received application bytes: ${JSON.stringify(chunk)}`);
});
socket.on('end', () => {
console.log('server observed the client FIN; its write side is still open');
socket.end('reply: READY\n');
});
});
await new Promise<void>((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', resolve);
});
const address = server.address() as AddressInfo;
const client = createConnection({ host: '127.0.0.1', port: address.port });
client.setEncoding('utf8');
client.on('connect', () => {
console.log('client connect event: the TCP handshake completed');
// end(data) writes these bytes and then half-closes the client write side.
client.end('HELLO\n');
});
client.on('data', (chunk) => {
console.log(`client received application bytes: ${JSON.stringify(chunk)}`);
});
client.on('end', () => {
console.log('client observed the server FIN after reading the reply');
});
await new Promise<void>((resolve, reject) => {
client.once('close', () => resolve());
client.once('error', reject);
});
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
void main().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
package main
import (
"fmt"
"io"
"net"
)
func main() {
// This example binds only to loopback and sends no traffic beyond this process.
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
panic(err)
}
defer listener.Close()
serverDone := make(chan error, 1)
go func() {
serverSide, err := listener.Accept()
if err != nil {
serverDone <- err
return
}
defer serverSide.Close()
payload, err := io.ReadAll(serverSide)
if err != nil {
serverDone <- err
return
}
fmt.Printf("server received application bytes: %q\n", payload)
fmt.Println("server observed the client FIN; its write side is still open")
_, err = io.WriteString(serverSide, "reply: READY\n")
serverDone <- err
}()
client, err := net.Dial("tcp", listener.Addr().String())
if err != nil {
panic(err)
}
defer client.Close()
clientTCP := client.(*net.TCPConn)
fmt.Println("Dial returned: the TCP handshake completed")
if _, err := io.WriteString(clientTCP, "HELLO\n"); err != nil {
panic(err)
}
if err := clientTCP.CloseWrite(); err != nil {
panic(err)
}
reply, err := io.ReadAll(clientTCP)
if err != nil {
panic(err)
}
fmt.Printf("client received application bytes: %q\n", reply)
fmt.Println("client read EOF after the server closed its write side")
if err := <-serverDone; err != nil {
panic(err)
}
}
Use the lifecycle to choose the next measurement.
For the protocol state machine and close behavior, read RFC 9293, Transmission Control Protocol. The loopback APIs are documented in the Node.js node:net reference and the Go net package. Packet and
socket tools show a local observation; the endpoints and full path may have additional
state.