Summary

  • TLS 1.3 close_notify is an authenticated statement that its sender will send no more messages on that connection. It closes one TLS write direction. It does not close the peer's write direction or acknowledge an application request, response, stream or durable side effect.
  • A transport EOF before close_notify preserves ambiguity: the receiver cannot know that all intended TLS data arrived. Compatibility modes may convert that EOF into a clean library result only when the application protocol independently detects and checks truncation.
  • Defensible completion needs four separately recorded boundaries: application framing or acknowledgement, local and peer TLS closure, underlying transport termination, and the durable outcome. A single closed=true field cannot govern retry or attribution.

The shutdown log that answered the wrong question

The incident ledger showed shutdown=success. Operators first read that as proof that the response and transaction had completed. The field came from a TLS wrapper after its local shutdown path ran.

The server had written a success response before committing the state transition. It then sent close_notify. The alert was genuine and ordered after the TLS records the server chose to send. The database rejected the commit after a uniqueness conflict.

The client could prove receipt of an authenticated end to the server's TLS sending direction. It could not derive a commit identifier from that proof. The system had assigned application authority to a transport event that never carried it.

Direction is the first boundary

TLS 1.3 defines close_notify as orderly closure of one direction. The sender says it will send no more messages on that connection. Any data received after that alert must be ignored.

Each party must send the alert before closing its write side unless it already sent an error alert. Sending it does not close the read side. A peer that receives it may still have application data to send before closing its own direction.

This differs from older TLS behavior that required an immediate reply and discarded pending writes. TLS 1.3 removed that requirement because it could truncate the receiver's remaining output. A clean ending is therefore not one atomic connection event. It is two ordered directional decisions.

TCP has a similar half-close shape but supplies different evidence. A TCP FIN orders the end of one byte stream. It is not an authenticated TLS alert, does not name the last complete TLS record and does not describe application meaning.

An alert and an EOF are not the same

TLS requires shared knowledge of ending to resist truncation. If the underlying transport closes before close_notify, the receiver cannot know that every item the peer intended to send arrived.

That uncertainty does not prove an attack. A process may crash, a proxy may time out or a non-compliant implementation may close only TCP. The important fact is that the stronger cryptographic boundary is absent.

An error alert is different again. It aborts the connection and forbids further data. A timeout, reset, fatal alert, unexpected EOF, received closure alert and local closure attempt belong to different evidence states even if an application exposes them all as “disconnected.”

The IANA registry identifies alert value 0 as close_notify. A codepoint assignment proves how to parse the signal. It cannot prove that a production process sent it, that the peer received it or that the application treated preceding bytes correctly.

Ordered bytes still need an application ending

Receiving close_notify after application records proves that those protected records preceded the peer's authenticated TLS ending. TLS treats their contents as opaque. It cannot determine whether they contained a complete message, a final response, a checksum, a commit receipt or half a document.

Applications therefore need their own terminal evidence. That may be a declared length, delimiter, terminal chunk, stream FIN, final response code, acknowledgement, request identifier or commit record. The correct choice belongs to the protocol above TLS.

Buffering adds another boundary. An application write can place bytes in a TLS or BIO buffer without proving dispatch to the transport. A local alert attempt can be recorded before its bytes leave a nonblocking process. Operating-system reliability assumptions do not establish that a database action completed.

The evidence ledger must preserve order: final application frame built, application write accepted, TLS record emitted, alert queued, alert dispatched, peer alert received, transport ended and durable result recorded. Collapsing these moments loses the reason retry is safe or dangerous.

The API return value is a state transition

OpenSSL implements TLS shutdown in two stages. A return of 0 normally means the local close_notify has been sent while the peer alert remains outstanding. It is not an error and not bidirectional completion. A return of 1 means both alerts were sent and received.

The first call closes TLS writing, not reading, and leaves the underlying TCP connection open. OpenSSL recommends continuing to read until peer closure so final application data and post-handshake messages are processed. Calling shutdown again before draining pending application data can fail.

Its flags require equal care. The sent flag is set after an attempt to send the alert; the received flag records peer closure. Quiet shutdown marks the connection closed without sending an alert and is explicitly non-compliant. A dashboard that labels every path “graceful” destroys this provenance.

Since OpenSSL 3.0, an unexpected EOF is a meaningful TLS error. SSL_OP_IGNORE_UNEXPECTED_EOF treats it as though peer closure arrived. The documentation permits that only when the application protocol unambiguously detects truncation and the application actually performs that check.

GnuTLS exposes the same directional choice. GNUTLS_SHUT_WR sends closure for the write side; GNUTLS_SHUT_RDWR also waits for the peer. Its manual warns that transport-only shutdown prevents distinction between malicious truncation and normal termination.

BoringSSL's running state machine likewise tracks read and write shutdown separately, dispatches the alert and treats quiet shutdown as local state. These controls establish implementability, not which branch a fleet executed.

HTTP shows why TLS cannot finish the message

HTTP/1.1 supplies explicit completeness rules. A Content-Length response needs the declared octets. Chunked transfer needs the terminal zero-sized chunk. If either is missing, connection closure does not promote the response into completeness.

A close-delimited response is the awkward case. Its end is the connection end, so an incomplete TLS close leaves the HTTP response incomplete. RFC 9112 recommends length- or encoding-delimited messages because a network failure can otherwise resemble a successful close.

HTTP can also accept some completed messages before an incomplete close. If a client already received the declared length or terminal chunk, those message boundaries remain evidence. The missing peer alert does not corrupt authenticated bytes already received; it leaves possible subsequent data ambiguous.

This is the precise compatibility test for ignored EOF. The application must demonstrate that every message it will act on has an independent, verified boundary. Merely setting the OpenSSL option is not that demonstration.

Multiplexing needs a request boundary

On HTTP/2, many streams share one TLS connection. close_notify cannot identify which request the server began to process. GOAWAY adds a last-stream boundary for streams that were or might have been acted on.

Even that signal is bounded. Streams at or below the identifier might have been processed; higher streams can be retried. If the connection ends without GOAWAY, a non-idempotent in-flight request remains ambiguous. TLS closure cannot manufacture a request-level acknowledgement.

HTTP/3 uses the same application idea over a different transport. Its GOAWAY identifies the range of requests or pushes accepted before shutdown. Without it, the client must assume that any request sent in whole or part might have been processed.

The operational consequence is severe. Retrying every request after a clean connection close can duplicate payments or messages. Refusing every retry can lose work. Retry authority comes from method semantics, idempotency keys and application outcome evidence, not from the adjective “graceful.”

QUIC changes the closing mechanism

QUIC carries TLS handshake bytes but does not use TLS record protection for application data. TLS alerts are translated into QUIC connection errors, and QUIC supplies its own CONNECTION_CLOSE, stream FIN, reset, closing and draining states.

TLS close_notify is a warning-level closure mechanism. RFC 9001 says QUIC uses alternative termination mechanisms and treats TLS alerts as fatal connection errors. A common library function name must not erase that protocol difference.

HTTP/3 layers GOAWAY above QUIC precisely because a connection close still does not say which requests were accepted or completed. Stream closure can mark the end of one HTTP message; connection closure can terminate many open streams at once.

Metrics must therefore name the carrier: TLS alert, TCP FIN or RST, QUIC stream FIN, QUIC CONNECTION_CLOSE, idle timeout, HTTP GOAWAY or application acknowledgement. “Secure close” is not a portable protocol state.

Evidence for an ending that can authorize action

Record endpoint role, direction, TLS version, connection correlation and the last complete application message or stream. Preserve the framing rule, expected and received byte counts, request identifier, idempotency key and retry class.

For TLS, record local alert queued and dispatched, peer alert received, library return sequence, read/write shutdown flags, pending plaintext or ciphertext and the exact error classification. Record quiet mode, ignored-EOF policy, library version, kernel TLS and proxy boundary.

For the transport, distinguish FIN, RST, EOF, timeout and half-close. For multiplexed protocols, keep GOAWAY or equivalent bounds and per-stream terminal state. For the business layer, preserve acknowledgement, commit identifier, durable timestamp and the authority that produced them.

Negative tests must sever TCP before the alert, omit a terminal HTTP chunk, close after a complete frame but before commit, return 0 from the first shutdown call, leave peer data pending, enable quiet shutdown, toggle ignored EOF and terminate HTTP/2 or HTTP/3 with a non-idempotent request in flight.

Rollback must restore strict EOF handling without taking unrelated TLS service down. A canary should prove that compliant peers still close, non-compliant peers fail for a named reason, completed application frames remain distinguishable and ambiguous operations are reconciled rather than blindly retried.

Sources