Skip to main content
Restate Virtual Objects give your agents persistent, isolated sessions. Each session is identified by a unique key (like a user ID or conversation ID), maintains its own state, and has built-in concurrency control, so concurrent requests to the same session are automatically queued.

How it works

A Virtual Object is a Restate service type where each instance is identified by a key. State stored in a Virtual Object:
  • Survives crashes and restarts: No external database needed
  • Is isolated per key: Each session has its own state
  • Has concurrency control: Only one write handler runs at a time per key, preventing race conditions
Virtual Objects as sessions

Example: a chat session

To turn your agent into a stateful session, you need two changes compared to a regular durable agent:
  1. Define a Virtual Object: use restate.object() instead of restate.service(). This gives each session key its own isolated state.
  2. Manage conversation history using the object’s K/V store: use ctx.get() and ctx.set() to read and write the message history.
chat-agent.ts
const chatAgent = restate.object({
  name: "Chat",
  handlers: {
    message: restate.createObjectHandler(
      { input: schema(ChatMessageSchema) },
      async (ctx: restate.ObjectContext, { message }: { message: string }) => {
        const model = wrapLanguageModel({
          model: openai("gpt-5.4"),
          middleware: durableCalls(ctx, { maxRetryAttempts: 3 }),
        });

        // Retrieve the state
        const messages =
          (await ctx.get<ModelMessage[]>("messages", superJson)) ?? [];
        messages.push({ role: "user", content: message });

        const res = await generateText({
          model,
          system: "You are a helpful assistant.",
          messages,
        });

        // Update the state
        ctx.set("messages", [...messages, ...res.response.messages], superJson);
        return { answer: res.text };
      },
    ),
    // Shared handler to retrieve the history
    getHistory: shared(async (ctx: restate.ObjectSharedContext) =>
      ctx.get<ModelMessage[]>("messages", superJson),
    ),
  },
});
Install Restate and launch it:
npm install --global @restatedev/restate-server@latest @restatedev/restate@latest
restate-server
Get the example:
restate example typescript-vercel-ai-tour-of-agents && cd typescript-vercel-ai-tour-of-agents
npm install
Export your OpenAI API key and run the agent:
export OPENAI_API_KEY=sk-...
npx tsx ./src/chat-agent.ts
Register the agents with Restate:
restate deployments register http://localhost:9080 --force --yes # dev only: overrides previous registrations
Ask the agent to do some task. Specify the Virtual Object ID in the URL, for example for session123:
curl localhost:8080/restate/call/Chat/session123/message \
--json '{"message": "make a poem about durable execution"}'
Continue the conversation with the same session ID. The agent remembers previous context:
curl localhost:8080/restate/call/Chat/session123/message \
--json '{"message": "shorten it to 2 lines"}'
Send a message to a different session. It starts a completely separate conversation:
curl localhost:8080/restate/call/Chat/session456/message \
--json '{"message": "what are the benefits of durable execution?"}'
The state tab of the Restate UI lets you query the state of each session:
Conversation State Management
This pattern is complementary to AI memory solutions like mem0 or graffiti. You can use Virtual Objects to enforce session concurrency and queueing while storing the agent’s memory in specialized memory systems.
This pattern is implementable with any of our SDKs and any AI SDK. If you need help with a specific SDK, please reach out to us via Discord or Slack.

Built-in concurrency control

Restate queues concurrent requests to the same session key. They are processed sequentially, preventing race conditions on shared state. This works similar to a task queue per session, but without needing to set up any external queue infrastructure. Concurrency control Different session keys run in parallel with no interference.
Send several messages concurrently to different chat sessions:
curl localhost:8080/restate/send/Chat/session123/message --json '{"message": "make a poem about durable execution"}' &
curl localhost:8080/restate/send/Chat/session456/message --json '{"message": "what are the benefits of durable execution?"}' &
curl localhost:8080/restate/send/Chat/session789/message --json '{"message": "how does workflow orchestration work?"}' &
curl localhost:8080/restate/send/Chat/session123/message --json '{"message": "can you make it rhyme better?"}' &
curl localhost:8080/restate/send/Chat/session456/message --json '{"message": "what about fault tolerance in distributed systems?"}' &
curl localhost:8080/restate/send/Chat/session789/message --json '{"message": "give me a practical example"}' &
curl localhost:8080/restate/send/Chat/session101/message --json '{"message": "explain event sourcing in simple terms"}' &
curl localhost:8080/restate/send/Chat/session202/message --json '{"message": "what is the difference between async and sync processing?"}'
The UI shows how Restate queues the requests per session to ensure consistency:
Concurrency control in action

Concurrently retrieving state

The state you store in Virtual Objects lives forever. To resume a session, simply send a new message to the same Virtual Object key.
To retrieve state, view the UI’s state tab or add a handler that reads it. Have a look at the getHistory handler in the example above.Call the handler to get the conversation history:
curl localhost:8080/restate/call/Chat/session123/getHistory
This is a shared handler, meaning it can only read state (not write). This allows it to run concurrently with the message handler.