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

> ## Agent Instructions
> Maximem Synap is a managed memory layer for AI agents: it ingests conversations and documents, extracts structured memory, and returns scoped context to the agent at retrieval time.
> The Python and JavaScript/TypeScript SDKs are the supported way to use Synap. Write integration code with SDK calls, not raw HTTP requests.
> Start with the Quickstart: https://docs.maximem.ai/getting-started/quickstart.md. Every SDK method is documented in the SDK reference: https://docs.maximem.ai/sdk-reference/overview.md.

# gRPC vs REST

> The two ways the SDK talks to Synap: one HTTPS request per call, or a long-lived gRPC stream. Which methods use each, what each gives you, and which to choose.

Should your agent hold a connection open to Synap, or make one request per call? This page answers that, and explains what changes for your agent either way.

## The idea

Every SDK method reaches Synap in one of two ways: a single HTTPS request (REST), or a long-lived gRPC stream that [`instance.listen()`](/sdk-reference/instance/listen) opens. REST is complete on its own: you can record turns, add memories and fetch context without ever opening a stream. The stream adds the other direction: Synap follows the conversation as it happens, prepares the context your agent will need next, and pushes it into the SDK before your agent asks.

For a long-running agent, the stream is the recommended default. REST fits serverless functions, short-lived workers, scripts and backfills. Both are SDK methods; you never write HTTP or gRPC code yourself.

## How each path works

### REST: one request per call

A REST method sends one HTTPS request and returns Synap's answer, or raises an error. Nothing stays open between calls. Synap learns about a turn when you record it, and does nothing for your agent until the next call arrives.

### The gRPC stream

`listen()` opens one bidirectional stream and keeps it open until you call [`stop_listening()`](/sdk-reference/instance/stop-listening). Both sides write to it:

* **Your process sends events:** the user's turn, your agent's reply, its tool calls, tool results and reasoning, and the start and end of each session. The SDK opens a session on the first event for a conversation and closes it when you stop listening.
* **Synap sends back:** acknowledgements for the events it has taken, and context bundles it prepared for the next turn.

Each bundle that arrives is stored in the SDK's in-process anticipation cache. The next context fetch looks there before it makes a request.

## Which methods use which

| Method | Path |
| - | - |
| `memories.*`, `conversation.record_message`, `record_messages_batch`, `ingest_transcript` | REST |
| `conversation.context.get_context_for_prompt`, `compact`, `get_compacted`, `get_compaction_status`, `user.get_profile`, `credits.*` | REST |
| `instance.send_message`, `record_thinking`, `record_tool_call`, `record_tool_result`, `end_session` | Stream |
| `instance.listen`, `stop_listening` | Open and close the stream |
| `user.context.fetch`, `customer.context.fetch`, `client.context.fetch`, `conversation.context.fetch`, and `sdk.fetch`, which calls them | Local anticipation cache first, then REST |

The table shows where a request goes when one is needed. Some reads are answered from the SDK's local caches without a request at all.

`conversation.record_message` is REST. It writes the same kind of turn that a streamed `user_message` or `assistant_message` writes, so pick one path per turn: a turn you sent on the stream is already stored.

### How a context fetch uses the cache

The four context fetches check the anticipation cache before anything else, and the result depends on how well a prepared bundle matches the query:

* **A sure match** is answered from the cache. No request leaves your process.
* **A borderline match** waits up to 250 ms for Synap: the SDK sends the request anyway, serves Synap's answer if it arrives in time, and serves the cached match if it does not.
* **No match** goes to Synap as an ordinary REST request.

A fetch that names `query_date` or `max_results_per_query` always goes to Synap, because a prepared bundle was not built for a specific date or per-query share. Without a stream nothing fills the anticipation cache, so every fetch takes the REST path. While the stream is open, the SDK also reports each fetch on it (what was asked and whether the cache answered), which is how Synap learns which predictions were useful.

## What each path gives you, and what it costs

### The stream

**What you get:**

* **Context ready before the next turn.** When your agent's reply arrives as an `assistant_message`, Synap predicts what the user is likely to ask next, searches memory for it, and pushes a bundle. When the next turn fetches, the answer is often already in the process. A session start does the same for the first turn of a conversation.
* **The whole turn, not only its edges.** Tool calls, tool results and reasoning can only be sent on the stream. They let Synap see why your agent did what it did. See [Traces and tool calls](/capture/traces-and-tool-calls).
* **Delivery that survives a dropped connection.** While the stream reconnects, events are buffered (up to 100 events or five minutes, whichever comes first) and replayed when it returns. Events Synap had not yet acknowledged are replayed with their original ids, so Synap drops any it already holds. Both buffers are also written to a journal on disk (`~/.synap/outbox/<instance_id>.jsonl` by default), so events still pending when your process dies go out the next time `listen()` runs on that machine for the same instance. See [Event delivery](/concepts/event-delivery).

**What it costs:**

* **A process that stays up, and owns the stream.** Call `listen()` at startup and `stop_listening()` on your shutdown path, which flushes what is buffered for up to two seconds and closes open sessions. The server closes every stream after one hour; the SDK reconnects with backoff, up to 10 attempts. After that the stream stays down and fetches keep working over REST, with no error in your agent loop, so watch `instance.is_listening` and log from `on_disconnect`.
* **No answer per call.** `send_message()` returns once the event is written to the stream or buffered, not once Synap has stored it. A refusal arrives later and shows up in the SDK's logs, not as an exception. Use REST when a caller needs to know that a specific write landed.
* **One stream per process.** Synap limits concurrent streams to 100 per instance and 200 per account. Open one stream for the whole process and tell users apart by the `user_id` and `customer_id` on each event; calling `listen()` again on the same SDK raises `ListeningAlreadyActiveError`.
* **Send and fetch from the same process.** Synap delivers a conversation's bundles only to the streams that have sent events for that conversation. If one process reports the turns and another fetches, the fetching process never receives them.
* **Size.** Each message on the stream must fit in 4 MB, the largest message Synap accepts.
* **A runtime that can hold a gRPC connection.** In Python, `grpcio` is a required dependency. In JavaScript and TypeScript, `@grpc/grpc-js` and `@grpc/proto-loader` are optional dependencies that npm installs by default; `npm install --omit=optional` leaves them out (along with the local embedding model, so cache matching falls back to word matching), and `listen()` then throws. Edge runtimes and browsers cannot open the stream either: they do not provide the raw TCP and HTTP/2 connection gRPC needs, and `listen()` throws there rather than doing nothing.

### REST

**What you get:**

* **Nothing to manage.** Each call stands alone, so it works in a function that runs for one request and exits.
* **An answer per call.** A write returns its result or raises, so you know whether it landed.
* **The same memory.** Turns recorded with `record_message` are stored, compacted and turned into long-term memory the same way as streamed turns.

**What it costs:**

* **No prepared context.** Each fetch is a request, unless the SDK already holds an identical earlier answer in its local response cache. You pay the network round trip on every new question.
* **No traces.** There is no REST method for tool calls, tool results or reasoning, so Synap never sees them. The SDK can still keep tool results on your machine for local history search with `archive_tool_result()`; see [Traces and tool calls](/capture/traces-and-tool-calls#without-a-stream).

## Which to choose

| Your code runs as | Use | Why |
| - | - | - |
| A long-running agent: a chat server, a voice agent, a worker that lives across many turns | The stream, plus REST for documents and other non-turn content | It can hold the connection, and it gains prepared context and traces |
| A serverless function or request handler that exits after one response | REST | A stream would be opened and torn down on every request, with no time for a bundle to arrive |
| A short-lived worker or job | REST | Same reason: the process is gone before the next turn |
| A script, a backfill, or an import of existing history | REST: `ingest_transcript`, `memories.create` | Nothing is waiting for a next turn. See [Import existing history](/getting-started/bootstrap) |
| An edge runtime or a browser (JavaScript) | Not the stream | `listen()` throws there |

You can mix the two in one process. A long-running agent streams its conversation turns and still calls [`memories.create`](/sdk-reference/memories/create) over REST for a document, or for a fact that must be retrievable at once.

## Opening the stream

```python Python theme={null}
import logging
from maximem_synap import MaximemSynapSDK

log = logging.getLogger(__name__)

sdk = MaximemSynapSDK()  # reads SYNAP_API_KEY
await sdk.initialize()

await sdk.instance.listen(
    on_disconnect=lambda reason: log.warning("synap stream down: %s", reason),
)
try:
    ...  # your agent loop: send_message, record_*, fetch
finally:
    await sdk.instance.stop_listening()
    await sdk.shutdown()
```

```typescript TypeScript theme={null}
import { SynapClient } from '@maximem/synap-js-sdk';

const sdk = new SynapClient(); // reads SYNAP_API_KEY
await sdk.initialize();

await sdk.instance.listen({
  on_disconnect: (reason) => console.warn(`synap stream down: ${reason}`),
});
try {
  // your agent loop: send_message, record_*, fetch
} finally {
  await sdk.instance.stop_listening();
  await sdk.shutdown();
}
```

## Framework integrations

Synap's framework integrations open the stream for you. On the first turn they call `listen()` themselves, then report turns, tool calls, tool results and reasoning on the stream. If the stream cannot open (it gets up to ten seconds, and after a failure they try again no sooner than a minute later), they write each turn with `record_message` over REST instead, never both, and still keep tool results in the local archive. They never raise inside your framework's loop. A stream your own code opened is used as is. To keep an integration on REST, set `SYNAP_AUTO_LISTEN=false`. In Python this needs `maximem-synap-integrations-common` 0.2.3 or later, which the integration packages install. See [Integrations](/integrations/overview).

## Related

* [Real-time anticipation](/concepts/real-time-anticipation): what Synap does with the events the stream carries.
* [Event delivery](/concepts/event-delivery): ids, acknowledgements, buffering and replay.
* [Real-time anticipation in a server](/patterns/real-time-anticipation-server): one shared stream for a multi-tenant process.
* [Traces and tool calls](/capture/traces-and-tool-calls): what to send beyond the turns.
* [instance.listen](/sdk-reference/instance/listen) and [conversation.record\_message](/sdk-reference/conversation/record-message): the two ways to write a turn.


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