> ## Documentation Index
> Fetch the complete documentation index at: https://flux-docs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Connections

> Disconnecting listeners, and the cleanup Flux does not do for you.

`Connect` returns a handle with a single method:

```luau theme={null}
local connection = MyEvent:Connect(function(...)
	print("Received event!")
end)

-- Stop listening
connection:Disconnect()
```

<Note>
  The handle is a plain table, not an `RBXScriptConnection`. It has `Disconnect` and
  nothing else: no `Connected` property, no `:Destroy()`. It will not satisfy code or a
  cleanup helper that expects a real Roblox connection object.
</Note>

## Flux does not clean up after you

<Warning>
  Listeners are not removed when the script that created them is destroyed.

  Flux stores listeners in module-level tables keyed by event name. Nothing watches
  script lifetime, `Destroying`, or `AncestryChanged`. A listener registered by a
  destroyed `LocalScript` stays registered for the lifetime of the session, keeps
  firing, and keeps its captured upvalues alive.

  Every `Connect` you make in a script that can be destroyed, or in a module you
  re-initialise, has to be disconnected explicitly.
</Warning>

This matters most in three places.

<AccordionGroup>
  <Accordion title="Per-character or per-round scripts" icon="rotate">
    A `LocalScript` under `StarterCharacterScripts` runs again on every respawn. Without
    a disconnect, each life adds another listener and your callback runs once per life,
    cumulatively.
  </Accordion>

  <Accordion title="UI opened and closed repeatedly" icon="window-restore">
    Connecting inside an `Open()` function and never disconnecting in `Close()` leaks one
    listener per open, along with every table the closure captured.
  </Accordion>

  <Accordion title="Hot-reloaded modules" icon="arrows-rotate">
    Reloading a module that connects at require time stacks listeners on top of the
    previous generation's, and the old ones still hold references to stale state.
  </Accordion>
</AccordionGroup>

## Patterns that hold up

### Disconnect on destroying

```luau theme={null}
local connection = MyEvent:Connect(onMessage)

script.Destroying:Once(function()
	connection:Disconnect()
end)
```

### Track a set of connections

```luau theme={null}
local connections = {}

local function bind()
	table.insert(connections, Health:Connect(onHealth))
	table.insert(connections, Damage:Connect(onDamage))
	table.insert(connections, Respawn:Connect(onRespawn))
end

local function unbind()
	for _, connection in connections do
		connection:Disconnect()
	end
	table.clear(connections)
end
```

### Self-disconnecting listener

Useful when the condition to stop is known only inside the callback. This is also the
workaround for `Once` not returning a handle.

```luau theme={null}
local connection
connection = Countdown:Connect(function(secondsLeft)
	updateLabel(secondsLeft)

	if secondsLeft <= 0 then
		connection:Disconnect()
	end
end)
```

<Tip>
  If you already use a `Trove`, `Janitor` or similar helper, wrap the handle in a
  function so the helper can call it:

  ```luau theme={null}
  trove:Add(function()
      connection:Disconnect()
  end)
  ```

  Passing the handle directly will not work, because those libraries look for
  `:Destroy()` or a real `RBXScriptConnection`.
</Tip>

## Once and Wait

<ResponseField name="Once" type="cleans up automatically">
  A `Once` listener removes itself from the listener list the moment it fires, so it
  cannot leak. It returns no handle, though, so it also cannot be cancelled if the event
  never arrives. A `Once` on an event that is never fired stays registered forever.
</ResponseField>

<ResponseField name="Wait" type="cleans up automatically">
  A `Wait` entry is removed when it resumes the parked thread. There is no timeout: if
  the message never comes, the thread is never resumed and both the thread and its
  listener entry stay alive for the session.
</ResponseField>

Guard a wait that might never be satisfied:

```luau theme={null}
local thread = coroutine.running()
local finished = false

task.spawn(function()
	local result = MyEvent:Wait()
	if not finished then
		finished = true
		task.spawn(thread, result)
	end
end)

task.delay(5, function()
	if not finished then
		finished = true
		task.spawn(thread, nil) -- resume with no result
	end
end)

local result = coroutine.yield()
```

## What Flux does clean up

For completeness, these are the internal resources Flux manages on its own.

| Resource                           | Released when                                            |
| ---------------------------------- | -------------------------------------------------------- |
| Pooled `Writer` / `Reader` objects | Returned to the pool after every flush and every receive |
| Per-player outgoing buffers        | The player leaves (`Players.PlayerRemoving`)             |
| Pending invoke requests            | The response arrives, or the 60s timeout fires           |
| `Once` / `Wait` listener entries   | They fire                                                |
| `Connect` listener entries         | Never. Only your `Disconnect` call removes them          |

<Info>
  Buffers larger than 64 KB are dropped rather than pooled, so a single oversized payload
  does not permanently inflate the pool's memory footprint.
</Info>

## Next steps

<CardGroup cols={2}>
  <Card title="Known limits" icon="triangle-exclamation" href="/reference/limits">
    Every rough edge in one place.
  </Card>

  <Card title="Batching" icon="layer-group" href="/concepts/batching">
    How and when buffers are actually flushed.
  </Card>
</CardGroup>
