# Drain & Shutdown

The last page made `order-svc`'s connection survive a server going away. This page handles the opposite event: your own process going away. When a deploy rolls out, a pod is rescheduled, or an operator sends SIGTERM, your client has a few seconds to disconnect cleanly.

The naive shutdown discards in-flight work. The `warehouse` subscriber has messages sitting in its buffer that its handler hasn't run yet. `order-svc` has a publish that hasn't reached the server. If the process just exits, all of that is gone. This page replaces the naive shutdown with one that finishes its in-flight work first.

Two concepts do that: the difference between **close** and **drain**, and the **drain timeout** that bounds how long drain is allowed to take.

## Close drops in-flight work

A **close** is an abrupt teardown: the client shuts the TCP socket immediately, sends no UNSUB, and abandons anything in flight. In the client libraries this is the `Close()` call (the CLI's equivalent is killing the process with `SIGKILL`).

Two kinds of work are in flight at shutdown, and close abandons both.

An **in-flight message** is one the server has already delivered to the subscriber but the handler hasn't finished; it's sitting in the subscription's buffer waiting its turn. When `warehouse` closes, every buffered `orders.created` event that hasn't run yet is dropped, and the handler never sees it.

A pending publish is the other kind: `order-svc` called publish, but the bytes are still in the client's write buffer, not yet on the wire. Close discards them too. The server never receives that order.

Close is the right call only when you're tearing down a connection you no longer care about, such as a failed health check, a test, or an error path where the work is already lost. For a planned shutdown, use drain instead.

## Drain finishes in-flight work, then closes

A **drain** is a graceful shutdown that flushes before it closes. Instead of dropping in-flight work, the client winds the connection down in order so nothing buffered is lost. In the client libraries this is the `Drain()` call.

Drain runs in two phases, and the connection reports each one as a state. First it enters **DRAINING\_SUBS**: the client sends an UNSUB for every subscription so the server stops delivering new messages, then it lets the handlers finish every in-flight message already in the buffers. Once the subscriptions are quiet, it enters **DRAINING\_PUBS**: it flushes every pending publish to the server. Only then does it close.

The order matters. Drain stops new work from arriving, lets existing work complete, then pushes out anything queued before it closes last, whereas a close goes straight to the close without those steps.

The animation shows these side by side. Close shuts the connection while messages are still buffered; drain delivers those last messages to the handlers, flushes the pending publish, and only then closes.

**Message flow — drainVsCloseAnimated:** Interactive NATS flow diagram.

* server → orders.\* sub
* orders.\* sub → App (Close)
* App (Close) → server
* App (Drain) → server
* server → orders.\* sub
* orders.\* sub → App (Drain)

## Drain on a shutdown signal

The place to call drain is your process's shutdown handler. When the runtime receives SIGTERM, instead of letting the process exit, you call `Drain()` and wait for it to return. The `warehouse` subscriber drains its buffered orders; `order-svc` flushes its pending publish. Both exit having handled everything they had.

The `nats` CLI models this with SIGINT. Press Ctrl-C against a running `nats sub` and it unsubscribes, lets the messages already in its buffer print, and then closes. That's the CLI's stand-in for `Drain()`. The client tabs show the real call wired to a SIGTERM handler.

#### CLI

```
#!/bin/bash

# Shut a subscriber down cleanly: drain in-flight work, then close.

#

# The nats CLI honors SIGINT (Ctrl-C) cleanly. When you press Ctrl-C,

# `nats sub` unsubscribes, lets the messages already in its buffer finish

# printing, and then closes the connection -- the CLI's equivalent of a

# client calling Drain(). It does not abandon in-flight messages the way

# an abrupt kill (SIGKILL) would.

#

# Run this, publish a few orders.created events from another terminal,

# then press Ctrl-C. The last delivered messages are handled before exit.

#

# The client tabs (JS/Go/etc.) show the real Drain() call wired to a

# SIGTERM handler, which the CLI cannot express directly.



nats sub "orders.>" \

  --server nats://n1:4222 \

  --connection-name warehouse
```

The message moving through the drain is the same order event as every other page:

```
{"order_id":"ord_8w2k","customer":"acme-co","total_cents":4200,"ts":"2026-05-22T10:14:22Z"}
```

After `Drain()` is called the connection is in a draining state and refuses new work. A publish attempted at that point doesn't queue and doesn't silently vanish. It returns a draining error (`ErrConnectionDraining` in nats.go, the equivalent in each library) so you can tell the connection is shutting down. Drain is the last thing your shutdown does, after the application has stopped producing.

## The drain timeout bounds how long drain waits

Drain waits for handlers to finish, so a handler that hangs would hang the shutdown forever. The **drain timeout** prevents that. It's the deadline drain is allowed to take; if the in-flight handlers and the pending flush don't complete within it, drain stops waiting, discards whatever is left, and closes anyway. The client surfaces a drain-timeout error (`ErrDrainTimeout` in nats.go) so you know the shutdown was cut short rather than completed.

The default is generous (30 seconds in nats.go), but the right value depends on your slowest handler. If a `warehouse` handler can take five seconds to write an order to a database, a one-second drain timeout will cut it off mid-write every deploy. Size the timeout to your handler latency rather than to a round number.

#### CLI

```
#!/bin/bash

# Drain has a deadline. If in-flight handlers do not finish within the

# drain timeout (default 30s in nats.go), the remaining queued messages

# are discarded and the connection closes anyway, surfacing a

# drain-timeout error.

#

# The CLI does not expose a separate drain-timeout flag -- its global

# --timeout bounds how long a single operation waits. The point this

# snippet stands in for is the client tabs: size the drain timeout to your

# slowest handler so a clean shutdown does not silently drop work.

#

# This publishes the canonical order event with a generous operation

# timeout so a slow handshake is not cut off mid-flush.



nats pub orders.created \

  '{"order_id":"ord_8w2k","customer":"acme-co","total_cents":4200,"ts":"2026-05-22T10:14:22Z"}' \

  --server nats://n1:4222 \

  --connection-name order-svc \

  --timeout 30s
```

The full set of connection options is documented in [Reference](/reference/.md). Here we cover only the ones that change how a connection behaves under fault.

## When the server drains first

Drain is usually something your client initiates. But the server can ask for it. A server entering a graceful shutdown signals **lame duck**: its INFO message carries a flag (`ldm`) telling connected clients it's about to go away. A client that watches for it can stop publishing and reconnect to another server in the pool before the link is cut, rather than waiting to be disconnected.

Detecting lame duck is a callback your client sets, the same shape as the reconnect callbacks from the last page. *Why* a server enters lame duck (a rolling upgrade, a node drain) is a server-side decision covered in [Topologies](/learn/topologies/your-first-cluster.md). Here it's just a hint the client may act on.

## Pitfalls

Three mistakes turn a clean shutdown back into a lossy one. Each comes back to this page's two concepts: drain versus close, and the drain timeout.

**Publishing after you call drain.** Drain can't be reversed: once the connection is draining it refuses new publishes. Code that calls `Drain()` and *then* tries to emit a final "shutting down" event gets a draining error and the event is lost. Drain last, after the application has stopped producing work. Don't interleave a publish with the shutdown.

Handle the draining error instead of letting it look like success:

#### CLI

```
#!/bin/bash

# Publishing after drain is a one-way door. Once Drain() is called the

# connection refuses new publishes and returns a draining error

# (ErrConnectionDraining in nats.go, the equivalent in each library) — the

# message is not queued and is not silently lost.

#

# The nats CLI has no scriptable "publish after drain" path: once it

# unsubscribes on Ctrl-C it exits, so there is no later publish to reject.

# This snippet shows the closest CLI stand-in — a publish issued against a

# server that is no longer accepting it fails fast and prints the error

# rather than succeeding silently. The client tabs (JS/Go/etc.) show the

# real shape: catch the draining error from a publish issued after Drain()

# instead of treating the call as a success.

#

# The teaching point the client tabs carry: drain last, after the

# application has stopped producing. Do not interleave a final "shutting

# down" publish with the shutdown — handle the draining error if you do.



nats pub orders.created \

  '{"order_id":"ord_8w2k","customer":"acme-co","total_cents":4200,"ts":"2026-05-22T10:14:22Z"}' \

  --server nats://n1:4222 \

  --connection-name order-svc
```

**A drain timeout shorter than your slowest handler.** When the timeout fires, in-flight work is discarded rather than finished. Set it below the time a handler actually needs and every deploy silently drops the orders that were mid-handle. Measure your slowest handler and set the drain timeout above it, with margin. Pick a number that covers the work rather than a round one.

**Assuming a core drain acks your JetStream messages.** Drain winds down the *connection*: it unsubscribes, finishes in-flight messages, and flushes publishes. It does not acknowledge a JetStream consumer's messages for you. An in-flight message from the `analytics` consumer that the handler finishes during drain still needs its acknowledgment sent before the connection closes, or JetStream will redeliver it to the next client. Acknowledge JetStream work explicitly; how a consumer's position moves is [JetStream → Acknowledgment](/learn/jetstream/acknowledgment.md), not the connection's job.

## Where you are

`order-svc` and the `warehouse`, `notifications`, and `analytics` subscribers now exit cleanly. A SIGTERM triggers a drain, not a close: buffered orders are handled, pending publishes are flushed, and the drain timeout keeps a stuck handler from hanging the shutdown forever. The connection that survived a server going away on the last page now also survives its own process going away.

## What's next

A connection can be lost from the outside (a server dying), and now it can be torn down cleanly from the inside. The next failure is internal and quieter: a subscriber whose handler can't keep up, so its buffer fills faster than it drains. That's the **slow consumer**, and left unbounded it grows until the process is killed.

Continue to [Slow Consumers](/learn/resilient-clients/slow-consumers.md).

## See also

* [JetStream → Acknowledgment](/learn/jetstream/acknowledgment.md) — what happens to a consumer's position for messages in flight at drain.
* [Topologies → Your first cluster](/learn/topologies/your-first-cluster.md) — why a server enters lame duck and signals its clients to leave.
* [Reference](/reference/.md) — every connection option, including the drain timeout and its default.
