Tearing down circuits

Circuits are torn down when an unrecoverable error occurs along the circuit, or when all streams on a circuit are closed and the circuit’s intended lifetime is over.

Relays SHOULD also tear down circuits which attempt to create:

  • streams with RELAY_BEGIN, or
  • rendezvous points with ESTABLISH_RENDEZVOUS, ending at the first hop. Letting Tor be used as a single hop proxy makes exit and rendezvous nodes a more attractive target for compromise.

Relays MAY use multiple methods to check if they are the first hop:

   * If a relay sees a circuit created with CREATE_FAST, the relay is sure to be
     the first hop of a circuit.
   * If a relay is the responder, and the initiator:
     * did not authenticate the link, or
     * authenticated with a key that is not in the consensus,
     then the relay is probably the first hop of a circuit (or the second hop of
     a circuit via a bridge relay).

   Circuits may be torn down either completely or hop-by-hop.

To tear down a circuit completely, a relay or client sends a DESTROY cell to the adjacent nodes on that circuit, using the appropriate direction’s circID.

Upon receiving an outgoing DESTROY cell, a relay frees resources associated with the corresponding circuit. If it’s not the end of the circuit, it sends a DESTROY cell for that circuit to the next relay in the circuit. If the node is the end of the circuit, then it tears down any associated edge connections (see Calculating the ‘Digest’ field).

After a DESTROY cell has been processed, a relay ignores all data or DESTROY cells for the corresponding circuit.

To tear down part of a circuit, the client may send a RELAY_TRUNCATE message signaling a given relay (Stream ID zero). That relay sends a DESTROY cell to the next node in the circuit, and replies to the client with a RELAY_TRUNCATED message.

As of 2026, current implementations do not support partial circuit tear down, so clients SHOULD NOT send RELAY_TRUNCATE messages.

[Note: If a relay receives a TRUNCATE message and it has any relay cells still queued on the circuit for the next node it will drop them without sending them. This is not considered conformant behavior, but it probably won’t get fixed until a later version of Tor. Thus, clients SHOULD NOT send a TRUNCATE message to a node running any current version of Tor if a) they have sent relay cells through that node, and b) they aren’t sure whether those cells have been sent on yet.]

   When an unrecoverable error occurs along one a circuit, the nodes
   must report it as follows:
     * If possible, send a DESTROY cell to relays _away_ from the client.
     * If possible, send *either* a DESTROY cell towards the client, or
       a RELAY_TRUNCATED cell towards the client.

Current versions of Tor do not reuse truncated RELAY_TRUNCATED circuits: A client, upon receiving a RELAY_TRUNCATED, will send forward a DESTROY cell in order to entirely tear down the circuit. Because of this, we recommend that relays should send DESTROY towards the client, not RELAY_TRUNCATED.

   NOTE:
     In tor versions before 0.4.5.13, 0.4.6.11 and 0.4.7.9, relays would
     handle an inbound DESTROY by sending the client a RELAY_TRUNCATED
     message.  Beginning with those versions, relays now propagate
     DESTROY cells in either direction, in order to tell every
     intermediary relays to stop queuing data on the circuit.  The earlier
     behavior created queuing pressure on the intermediary relays.

The body of a DESTROY cell or RELAY_TRUNCATED message contains a single octet, describing the reason that the circuit was closed. Implementations SHOULD always use the NONE reason to avoid side channels: sending the real DESTROY reason creates a side channel, particularly if the reason is sent in both directions, because it can enable colluding relays to determine that they are on the same circuit.

   NOTE:
   Older implementations include the actual reason
   from the list of error codes below
   in RELAY_TRUNCATED messages and DESTROY cells
   sent \_towards\_ the client.
   This is a remnant of the original relay behavior
   of converting a DESTROY into a RELAY_TRUNCATED
   in the inbound direction, which is no longer the case
   (see the NOTE above).

Reasons from DESTROY cells SHOULD NOT be propagated downward or upward, due to potential side channel risk: A relay receiving a DESTROY command should use the NONE reason for its next cell.

The error codes are:

     0 -- NONE            (No reason given.)
     1 -- PROTOCOL        (Tor protocol violation.)
     2 -- INTERNAL        (Internal error.)
     3 -- REQUESTED       (A client sent a TRUNCATE command.)
     4 -- HIBERNATING     (Not currently operating; trying to save bandwidth.)
     5 -- RESOURCELIMIT   (Out of memory, sockets, or circuit IDs.)
     6 -- CONNECTFAILED   (Unable to reach relay.)
     7 -- OR_IDENTITY     (Connected to relay, but its OR identity was not
                           as expected.)
     8 -- CHANNEL_CLOSED  (The OR connection that was carrying this circuit
                           died.)
     9 -- FINISHED        (The circuit has expired for being dirty or old.)
    10 -- TIMEOUT         (Circuit construction took too long)
    11 -- DESTROYED       (The circuit was destroyed w/o client TRUNCATE)
    12 -- NOSUCHSERVICE   (Request for unknown hidden service)