Skip to main content
agent.close() and await agent.aclose() shut down every resource the agent owns — LLM clients, memory, MCP tools, runtime MCPs, and per-agent circuit breakers — in one call.
Closing an agent fans out to each owned resource so nothing leaks between runs.

Quick Start

1

Simple

Pass an MCP at construction time; close() shuts down its subprocess and stream.
2

Async

Use the async context manager or call aclose() explicitly — it prefers each tool’s aclose().

How It Works

Closing an agent walks each owned resource and shuts it down best-effort.
close() / aclose() set _closed = True and become no-ops on subsequent calls, so calling them twice is safe. Check agent.is_closed to confirm.
Before PR #5083 the async path (aclose()) skipped LLM-client teardown entirely, so a per-request clone in the HTTP invoke API leaked its client pool even when the caller (correctly) awaited aclose(). If you build long-running services that rely on await agent.aclose(), upgrade to that release.

Garbage-Collection Safety

An agent that is dropped without close() is cleaned up by a finalizer that never blocks.
  • No deadlock. Agent.__del__ reads the approval registry directly, without taking its lock. GC firing on a thread that holds the lock no longer hangs.
  • Silent no-op without a registry. If no approval-gated tool ever ran, there is nothing to release.
  • No leaked approval keys. Agents created and dropped in a long-lived worker thread, event loop, or notebook no longer pile up cached approvals. The next approval write prunes dead agents’ keys.
close() / aclose() and with blocks stay the authoritative teardown. They are deterministic and release resources immediately; GC only runs when Python decides to.
See Approval cache bounds and cleanup and MCP Lifecycle → Garbage-collection safety.

Best Practices

Wrap a multi-agent run in with PraisonAIAgents(...) as workflow: — the team-level context manager fans out to each agent’s close(). See Resource Lifecycle.
For a lone agent, wrap the run in try/finally and call agent.close() (or await agent.aclose()), so cleanup runs even when the run raises.
agent.close() releases the sync HTTP client and drops the cached async client reference; it cannot close the async client (its close() is a coroutine). For deterministic async teardown — a FastAPI lifespan, a worker shutdown hook, or tests run with -W error::ResourceWarning — call await agent.aclose().
Garbage-collection cleanup is best-effort and never blocks — the async client cannot be awaited from a finalizer. Call close() / aclose() (or use with) explicitly for deterministic teardown.
Each step is guarded individually; failures are logged as warnings and never raise, so a bad shutdown never hides the original exception.
The HTTP invoke API (POST /api/v1/agents/{id}/invoke) and invoke_agent_standalone() both wrap the invocation in try/finally and close the per-request clone after every request. You do not need to add extra cleanup around individual invokes; the wrapper handles per-request clones. See n8n API → Per-request cleanup.
If you create one Agent(...) per request or session and never call close(), the process-global approval registry no longer grows without bound — __del__ also releases the agent’s scope when it is garbage-collected. close() / aclose() remain the recommended deterministic path, since finalizer timing is not guaranteed. Only the closing agent’s own grants are dropped; every other live agent keeps its approvals. See Permissions → Approval-scope cleanup.

Runtime MCP

Attach and detach MCP servers at runtime

MCP Lifecycle

Per-MCP context manager and manual shutdown

Resource Lifecycle

Team-level cleanup with PraisonAIAgents

Tool Circuit Breaker

How per-agent breakers work and auto-prune

Permissions

Approval scopes and the cleanup that close() performs

n8n API

Per-request clone cleanup in the HTTP invoke endpoint

Agent Invoke Registry

Templates vs. per-request clones and their teardown