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

# Async Safety

> How knowledge search, memory writes, and the in-memory adapter behave under concurrent async tasks

Under async execution, PraisonAI dispatches blocking I/O to worker threads so parallel tasks actually run in parallel, and the built-in in-memory store stays safe under concurrent writes.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import asyncio
from praisonaiagents import Agent

# Two unnamed agents in the same process — skill grants stay scoped per instance.
reviewer = Agent(skills=["./skills/file-reviewer"])
worker   = Agent(instructions="Do a job.")

async def main():
    await reviewer.astart("Review report.pdf")   # knowledge.search runs on a worker thread
    await worker.astart("Summarise the review")  # store_in_memory in the callback also off-loop

asyncio.run(main())
```

Both `knowledge.search(...)` and task-callback memory writes are offloaded automatically — you don't call anything new. What you see is parallel async tasks that share a knowledge base or memory now actually running in parallel.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Async offload"
        L[⚡ Event Loop] --> W[🧵 Worker Thread]
        W --> K[📚 Knowledge search]
        W --> M[🧠 Memory write]
        W --> G[🛡️ Guardrail validate]
        K --> R[✅ Loop stays responsive]
        M --> R
        G --> R
    end

    classDef loop fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef worker fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef ok fill:#10B981,stroke:#7C90A0,color:#fff

    class L loop
    class W,K,M,G worker
    class R ok
```

## Quick Start

**1. Parallel agents sharing knowledge**

An `asyncio.gather(...)` of agents that query the same knowledge base runs in parallel — no lookup blocks the loop.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import asyncio
from praisonaiagents import Agent

agent = Agent(
    name="Research Assistant",
    instructions="Answer questions using the knowledge base.",
    knowledge=["handbook.pdf"],
)

async def main():
    await asyncio.gather(
        agent.astart("Summarise the onboarding policy"),
        agent.astart("What is the refund window?"),
    )

asyncio.run(main())
```

**2. Parallel tasks writing memory**

Task-callback memory writes offload to worker threads, so a fan-out of tasks doesn't stall on one slow write.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import asyncio
from praisonaiagents import Agent, Task, PraisonAIAgents

worker = Agent(name="Worker", instructions="Do the work.", memory=True)

agents = PraisonAIAgents(
    agents=[worker],
    tasks=[
        Task(description="Task A", agent=worker, async_execution=True),
        Task(description="Task B", agent=worker, async_execution=True),
        Task(description="Task C", agent=worker, async_execution=True),
    ],
    # process defaults to "sequential"; async_execution on tasks fans them out.
)

asyncio.run(agents.astart())
```

> [!NOTE]
>
> `async_execution=True` only fans tasks out under the async entry point shown here (`astart()` / `arun()`). The sync `.start()` / `.run()` runs them sequentially and logs a warning — see [PraisonAI PR #5227](https://github.com/MervinPraison/PraisonAI/pull/5227).

**3. Parallel agents with guardrails**

An `asyncio.gather(...)` of agents where one carries a string guardrail runs in parallel — the guardrail's validation LLM call offloads to a worker thread instead of stalling the fan-out.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import asyncio
from praisonaiagents import Agent

agent1 = Agent(
    name="Writer",
    instructions="Write a short blurb.",
    guardrail="Ensure the blurb makes no unverifiable claims.",
)
agent2 = Agent(
    name="Summarizer",
    instructions="Summarise the release notes.",
)

async def main():
    await asyncio.gather(
        agent1.astart("Blurb for the new dashboard"),
        agent2.astart("Summarise v2.0 release notes"),
    )

asyncio.run(main())
```

***

## How It Works

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Loop as Event Loop
    participant Agent
    participant Worker as Worker Thread
    participant Knowledge
    participant Memory

    Agent->>Loop: astart(...)
    Loop->>Worker: to_thread(knowledge.search)
    Worker->>Knowledge: embedding + vector search
    Knowledge-->>Worker: results
    Worker-->>Loop: results (event loop stayed responsive)
    Loop->>Worker: to_thread(store_in_memory)
    Worker->>Memory: embedding + DB write (RLock-guarded)
    Memory-->>Worker: id
    Worker-->>Loop: done
    Loop-->>Agent: continues next coroutine
```

## What runs off the event loop

| Call site | Offloaded via |
| - | - |
| `Knowledge.search` inside `Agent._achat_impl` | `asyncio.to_thread` |
| `Task.store_in_memory` inside `Task.execute_callback` | `asyncio.to_thread` |
| `_validate_with_guardrail` inside `Agent._aapply_guardrail_with_retry` and `_apply_task_guardrail` inside `Agents._aapply_task_guardrail` | `loop.run_in_executor` (with `copy_context_to_callable`) |
| `_goal_gate_async` on the async autonomous loop — see [Goal Loop](https://praison.ai/docs/features/goal-loop.md) | `asyncio.to_thread` (cancellation-safe via `asyncio.shield`) |
| `_auto_save_session` inside `Agent.astart`, `Agent._adelegate_to_backend`, and each iteration of `Agent.run_autonomous_async` ([PR #5258](https://github.com/MervinPraison/PraisonAI/pull/5258)) | `asyncio.to_thread` |
| `_persist_message("user", ...)` inside `Agent._achat_impl` ([PR #5879](https://github.com/MervinPraison/PraisonAI/pull/5879)) | `asyncio.to_thread` (through `_apersist_message`) |
| `RAG.aquery` / `RAG.aretrieve` ([PR #6155](https://github.com/MervinPraison/PraisonAI/pull/6155)) | `asyncio.to_thread` (contextvars copied into the thread) |
| `RAG.astream` retrieval, rerank, and sync-LLM fallback streaming ([PR #6155](https://github.com/MervinPraison/PraisonAI/pull/6155)) | `asyncio.to_thread` |
| `SubQuestionEngine.aquery` ([PR #6155](https://github.com/MervinPraison/PraisonAI/pull/6155)) | `asyncio.to_thread` |
| `agent.chat` inside every `EscalationPipeline.execute()` stage — see [Escalation Pipeline](https://praison.ai/docs/features/escalation-pipeline.md) ([PR #6155](https://github.com/MervinPraison/PraisonAI/pull/6155)) | `asyncio.to_thread` |

Both were synchronous embedding + vector-store / DB calls that previously blocked the event loop for the whole coroutine lifetime. An `asyncio.gather(...)` fan-out would serialise on the slowest lookup or write instead of running concurrently. Offloading them to threads keeps the loop free.

A string / `LLMGuardrail` output guardrail fires a blocking LLM call under the hood to validate the agent's output. On the async path this call is now offloaded via `loop.run_in_executor(...)`, so parallel tasks don't serialise on one task's guardrail round-trip ([PR #4469](https://github.com/MervinPraison/PraisonAI/pull/4469)). Contextvars are preserved across the offload with `copy_context_to_callable`, so trace emission and session context stay intact.

Session auto-save (`Agent(memory=MemoryConfig(auto_save="…"))`) does a `FileLock` + JSON read-modify-write on the process-wide `DefaultSessionStore` singleton. Before [PR #5258](https://github.com/MervinPraison/PraisonAI/pull/5258) that synchronous write ran inline on the async call sites, so one async agent's auto-save blocked every other async agent sharing the loop — a fast agent gathered alongside a slow-writing one would visibly stall. The write is now offloaded via `asyncio.to_thread` at every async call site, and an internal lock serialises the shared `_auto_save_last_index` bookkeeping so two concurrent async runs sharing one `Agent` can't double-persist the same turns.

> [!NOTE]
>
> **Non-blocking user-turn persist (PraisonAI [PR #5879](https://github.com/MervinPraison/PraisonAI/pull/5879)):** The user-turn persist in `Agent._achat_impl` now goes through `_apersist_message` (an `asyncio.to_thread` wrapper over `_persist_message`), like the assistant-turn persists already did. Before the fix, each user turn blocked the event loop for the whole `FileLock` + `json.dump` + `fsync` round-trip — 14–45 ms per turn at 500–2000 messages — so a heartbeat task running beside `achat` stalled on every turn. The write still happens; it just no longer freezes the loop.

> [!NOTE]
>
> **Uncontended `async_lock()` skips the executor (PraisonAI [PR #5879](https://github.com/MervinPraison/PraisonAI/pull/5879)):** `DualLock` now tries a non-blocking acquire of its shared mutex first and only falls back to `loop.run_in_executor(...)` when the mutex is contended. Uncontended async acquisition dropped from \~191 µs to microseconds and no longer uses a default-executor thread. Contended behaviour and cancellation safety are unchanged.

> [!NOTE]
>
> **Request-scoped state survives the offload (PraisonAI [PR #6155](https://github.com/MervinPraison/PraisonAI/pull/6155)):** `RAG.aquery`, `RAG.aretrieve`, `RAG.astream`, `SubQuestionEngine.aquery`, and `EscalationPipeline.execute()` used to block the event loop (or use `run_in_executor(None, ...)`, which drops contextvars). They now use `asyncio.to_thread`, so a user id, trace span, or budget set with `contextvars.ContextVar` on the awaiting task is visible inside the worker thread.
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> import contextvars
>
> user_id = contextvars.ContextVar("user_id", default=None)
>
> async def handler(rag, q):
>     user_id.set("user-123")
>     return await rag.aquery(q)  # retrieval thread sees user_id.get() == "user-123"
> ```

> [!NOTE]
>
> This is transparent — there is no user-facing knob to opt out. You do not call anything new; parallel async tasks simply run in parallel.

## Thread-safe in-memory adapter

The built-in `InMemoryAdapter` guards its store, search, delete, and reset methods with a re-entrant lock (`RLock`). Once writes move off the event loop, parallel async writes from an `asyncio.gather(...)` fan-out cannot produce duplicate ids or lose entries.

If you [write a custom adapter](https://praison.ai/docs/features/custom-memory-adapters.md) that will be shared across async agents, do the same — guard its mutating methods with a lock.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import threading

class MyAdapter:
    def __init__(self):
        self._lock = threading.RLock()
        self._store = {}

    def store_long_term(self, memory_id, content, **kwargs):
        with self._lock:
            self._store[memory_id] = content
```

> [!NOTE]
>
> **See also (PR #5219):** two more concurrency guarantees landed in [PR #5219](https://github.com/MervinPraison/PraisonAI/pull/5219) (fixes [#5162](https://github.com/MervinPraison/PraisonAI/issues/5162)):
>
> * [Image Generation → Non-blocking async](https://praison.ai/docs/features/image-generation.md#non-blocking-async-pr-5219) — `ImageAgent.achat` / `agenerate_image` now offload via `asyncio.to_thread`, so a `gather` of an image agent and a text agent no longer starves the text agent.
> * [Thread Safety → Output singleton lock](https://praison.ai/docs/features/thread-safety.md#output-singleton-tracestatus-lock-pr-5219) — `output="status"` / `output="trace"` metrics no longer corrupt under `asyncio.gather`.

> [!NOTE]
>
> **See also (PR #5370):** [Thread-Safe Agent State → AsyncSafeState reentrancy fix](https://praison.ai/docs/features/thread-safety.md#lock-types) — the sync `with state:` entry point now uses `DualLock.sync()`, closing a same-thread self-deadlock on nested chat-history mutations.

***

## Best Practices

> [accordion] Fan out with asyncio.gather without fear
>
> Knowledge search and memory writes offload to threads, so a `gather(...)` of agents that share a knowledge base or memory runs in parallel instead of serialising on the slowest task.

> [accordion] Make custom adapters thread-safe
>
> Custom memory adapters used from async task callbacks receive writes from multiple worker threads. Guard mutating methods with a lock, as the built-in `InMemoryAdapter` does with an `RLock`.

> [accordion] Guardrails don't stall async fan-outs
>
> String / `LLMGuardrail` output and task guardrails offload their blocking LLM call to a worker thread on the async path, so an `asyncio.gather(...)` of agents that share a guardrail no longer serialises on one task's validation LLM round-trip. Custom guardrail objects that implement synchronous `validate_output` get the same offload for free.

> [accordion] async def guardrails work on the sync path too
>
> Since PraisonAI PR [#5305](https://github.com/MervinPraison/PraisonAI/pull/5305), passing an `async def` guardrail to `Agent(guardrails=...)` or `Task(guardrails=...)` works on both the sync and async execution paths. `_process_guardrail` detects a coroutine function and runs it via `run_coroutine_from_any_context`. Before #5305 the sync path called the coroutine function synchronously — the returned coroutine was never awaited, `GuardrailResult.from_tuple` crashed on it, and the run was reported as a validation failure. Retries were wasted on a guardrail that had never actually executed.

> [accordion] Sync title and crawl tools work inside a loop
>
> Since PraisonAI PR [#6239](https://github.com/MervinPraison/PraisonAI/pull/6239) (Issue [#6238](https://github.com/MervinPraison/PraisonAI/issues/6238)), `generate_title` (session auto-titling) and `crawl4ai_sync` / `crawl4ai_extract_sync` run via `run_coroutine_from_any_context`. Calling them from a FastAPI route or Jupyter cell returns real results instead of the title fallback or a `RuntimeError`.

> [accordion] Nothing to configure
>
> The offload is automatic and transparent. There is no flag to enable or disable it — the sync path is unchanged and the async path always stays responsive.

***

## Related

> [card] [Knowledge](https://praison.ai/docs/features/knowledge.md)
>
> Add document knowledge with async-safe search

> [card] [Memory](https://praison.ai/docs/features/memory.md)
>
> Persistent memory with offloaded, thread-safe writes

> [card] [Async Agents](https://praison.ai/docs/features/async.md)
>
> Run agents and tasks concurrently with asyncio

> [card] [Custom Memory Adapters](https://praison.ai/docs/features/custom-memory-adapters.md)
>
> Build your own adapter — guard it with a lock for async use


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.