# Cluster health
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/cluster_health/cluster-health
schemas/openapi-admin.json get /cluster-health
Get the cluster health.
# Create deployment
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/deployment/create-deployment
schemas/openapi-admin.json post /deployments
Create deployment. Restate will invoke the endpoint to gather additional information required for registration, such as the services exposed by the deployment. If the deployment is already registered, this method will fail unless `force` is set to `true`.
# Delete deployment
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/deployment/delete-deployment
schemas/openapi-admin.json delete /deployments/{deployment}
Delete deployment. Currently it's supported to remove a deployment only using the force flag
# Get deployment
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/deployment/get-deployment
schemas/openapi-admin.json get /deployments/{deployment}
Get deployment metadata
# List deployments
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/deployment/list-deployments
schemas/openapi-admin.json get /deployments
List all registered deployments.
# Update deployment
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/deployment/update-deployment
schemas/openapi-admin.json put /deployments/{deployment}
Update deployment. Invokes the endpoint and replaces the existing deployment metadata with the discovered information. This is a dangerous operation that should be used only when there are failing invocations on the deployment that cannot be resolved any other way. Sense checks are applied to test that the new deployment is sufficiently similar to the old one.
# Health check
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/health/health-check
schemas/openapi-admin.json get /health
Check REST API Health.
# Cancel an invocation
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/invocation/cancel-an-invocation
schemas/openapi-admin.json patch /invocations/{invocation_id}/cancel
Cancel the given invocation. Canceling an invocation allows it to free any resources it is holding and roll back any changes it has made so far, running compensation code. For more details, checkout https://docs.restate.dev/guides/sagas
# Delete an invocation
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/invocation/delete-an-invocation
schemas/openapi-admin.json delete /invocations/{invocation_id}
Use kill_invocation/cancel_invocation/purge_invocation instead.
# Kill an invocation
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/invocation/kill-an-invocation
schemas/openapi-admin.json patch /invocations/{invocation_id}/kill
Kill the given invocation. This does not guarantee consistency for virtual object instance state, in-flight invocations to other services, etc.
# Purge an invocation
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/invocation/purge-an-invocation
schemas/openapi-admin.json patch /invocations/{invocation_id}/purge
Purge the given invocation. This cleanups all the state for the given invocation. This command applies only to completed invocations.
# Purge an invocation journal
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/invocation/purge-an-invocation-journal
schemas/openapi-admin.json patch /invocations/{invocation_id}/purge-journal
Purge the given invocation journal. This cleanups only the journal for the given invocation, retaining the metadata. This command applies only to completed invocations.
# Restart as new invocation
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/invocation/restart-as-new-invocation
schemas/openapi-admin.json patch /invocations/{invocation_id}/restart-as-new
Restart the given invocation as new. This will restart the invocation as a new invocation with a different invocation id. By using the 'from' query parameter, some of the partial progress can be copied over to the new invocation.
# Resume an invocation
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/invocation/resume-an-invocation
schemas/openapi-admin.json patch /invocations/{invocation_id}/resume
Resume the given invocation. In case the invocation is backing-off, this will immediately trigger the retry timer. If the invocation is suspended or paused, this will resume it.
# OpenAPI specification
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/openapi/openapi-specification
schemas/openapi-admin.json get /openapi
# Get service
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/service/get-service
schemas/openapi-admin.json get /services/{service}
Get a registered service.
# Get service OpenAPI
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/service/get-service-openapi
schemas/openapi-admin.json get /services/{service}/openapi
Get the service OpenAPI 3.1 contract.
# List services
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/service/list-services
schemas/openapi-admin.json get /services
List all registered services.
# Modify a service
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/service/modify-a-service
schemas/openapi-admin.json patch /services/{service}
Modify a registered service configuration. NOTE: Service re-discovery will update the settings based on the service endpoint configuration.
# Modify a service state
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/service/modify-a-service-state
schemas/openapi-admin.json post /services/{service}/state
Modify service state
# Get service handler
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/service_handler/get-service-handler
schemas/openapi-admin.json get /services/{service}/handlers/{handler}
Get the handler of a service
# List service handlers
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/service_handler/list-service-handlers
schemas/openapi-admin.json get /services/{service}/handlers
List all the handlers of the given service.
# Create subscription
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/subscription/create-subscription
schemas/openapi-admin.json post /subscriptions
Create subscription.
# Delete subscription
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/subscription/delete-subscription
schemas/openapi-admin.json delete /subscriptions/{subscription}
Delete subscription.
# Get subscription
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/subscription/get-subscription
schemas/openapi-admin.json get /subscriptions/{subscription}
Get subscription
# List subscriptions
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/subscription/list-subscriptions
schemas/openapi-admin.json get /subscriptions
List all subscriptions.
# Admin version information
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/admin-api/version/admin-version-information
schemas/openapi-admin.json get /version
Obtain admin version information.
# AI Agent Quickstart
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/ai-quickstart
Build and run your first AI agent with Restate and popular AI SDKs
export const GitHubLink = ({url}) =>
;
This guide takes you through building your first AI agent with Restate and popular AI SDKs.
We will run a simple weather agent that can answer questions about the weather using durable execution to ensure reliability.
Select your AI SDK:
**Prerequisites**:
* [Node.js](https://nodejs.org/en/) >= v20
* OpenAI API key (get one at [OpenAI](https://platform.openai.com/))
Restate is a single self-contained binary. No external dependencies needed.
```shell theme={null}
brew install restatedev/tap/restate-server restatedev/tap/restate
```
Start the server:
```shell theme={null}
restate-server
```
Download prebuilt binaries from the [releases page](https://github.com/restatedev/restate/releases/latest):
```shell MacOS-x64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=x86_64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell MacOS-arm64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=aarch64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell Linux-x64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=x86_64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
```shell Linux-arm64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=aarch64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
Start the server:
```shell theme={null}
restate-server
```
```shell theme={null}
npm install --global @restatedev/restate-server@latest @restatedev/restate@latest
```
Start the server:
```shell theme={null}
restate-server
```
Run the Restate Server:
```shell theme={null}
docker run --name restate_dev --rm \
-p 8080:8080 -p 9070:9070 -p 9071:9071 \
--add-host=host.docker.internal:host-gateway \
docker.restate.dev/restatedev/restate:latest
```
Run CLI commands:
```shell theme={null}
docker run -it --network=host \
docker.restate.dev/restatedev/restate-cli:latest \
invocations ls
```
Replace `invocations ls` with any CLI subcommand.
You can find the Restate UI running on port 9070 (`http://localhost:9070`) after starting the Restate Server.
Get the weather agent template for the [Vercel AI SDK](https://ai-sdk.dev/docs/foundations/overview) and Restate:
```shell theme={null}
git clone https://github.com/restatedev/ai-examples.git &&
cd ai-examples/vercel-ai/template &&
npm install
```
Export your OpenAI key and run the agent:
```shell theme={null}
export OPENAI_API_KEY=your_openai_api_key_here
npm run dev
```
The weather agent is now listening on port 9080.
Tell Restate where the service is running (`http://localhost:9080`), so Restate can discover and register the services and handlers behind this endpoint.
You can do this via the UI (`http://localhost:9070`) or via:
```shell CLI theme={null}
restate deployments register http://localhost:9080
```
```shell curl theme={null}
curl localhost:9070/deployments --json '{"uri": "http://localhost:9080"}'
```
```shell CLI theme={null}
❯ SERVICES THAT WILL BE ADDED:
- agent
Type: Service
HANDLER INPUT OUTPUT
run value of content-type 'application/json' value of content-type 'application/json'
✔ Are you sure you want to apply those changes? · yes
✅ DEPLOYMENT:
SERVICE REV
agent 1
```
```shell curl theme={null}
{
"id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"services": [
{
"name": "Agent",
"handlers": [
{
"name": "run",
"ty": "Shared",
"input_description": "one of [\"none\", \"value of content-type 'application/json'\"]",
"output_description": "value of content-type 'application/json'"
}
],
"ty": "Service",
"deployment_id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"revision": 1,
"public": true,
"idempotency_retention": "1day"
}
]
}
```
If you run Restate with Docker, register `http://host.docker.internal:9080` instead of `http://localhost:9080`.
When using [Restate Cloud](https://restate.dev/cloud), your service must be accessible over the public internet so Restate can invoke it.
If you want to develop with a local service, you can expose it using our [tunnel](/deploy/server/cloud/#registering-restate-services-with-your-environment) feature.
Invoke the agent via the Restate UI playground: go to `http://localhost:9070`, click on your service and then on playground.
Or invoke via `curl`:
```shell theme={null}
curl localhost:8080/agent/run --json '"What is the weather in Detroit?"'
```
Output: `The weather in Detroit is currently 17°C with misty conditions.`.
The agent you just invoked uses Durable Execution to make agents resilient to failures. Restate persisted all LLM calls and tool execution steps, so if anything fails, the agent can resume exactly where it left off.
We did this by using Restate's `durableCalls` middleware to persist LLM responses and using [Restate Context actions](/foundations/actions) (e.g. `ctx.run`) to make the tool executions resilient:
```ts expandable {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/vercel-ai/template/src/app.ts?collapse_imports"} theme={null}
async function weatherAgent(restate: restate.Context, prompt: string) {
// The durableCalls middleware persists each LLM response in Restate,
// so they can be restored on retries without re-calling the LLM
const model = wrapLanguageModel({
model: openai("gpt-4o"),
middleware: durableCalls(restate, { maxRetryAttempts: 3 }),
});
const { text } = await generateText({
model,
system: "You are a helpful agent that provides weather updates.",
prompt,
tools: {
getWeather: tool({
description: "Get the current weather for a given city.",
inputSchema: z.object({ city: z.string() }),
execute: async ({ city }) => {
// call tool wrapped as Restate durable step
return await restate.run("get weather", () => fetchWeather(city));
},
}),
},
stopWhen: [stepCountIs(5)],
providerOptions: { openai: { parallelToolCalls: false } },
});
return text;
}
// create a Restate Service as the callable entrypoint
// for our durable agent function
const agent = restate.service({
name: "agent",
handlers: {
run: async (ctx: restate.Context, prompt: string) => {
return weatherAgent(ctx, prompt);
},
},
});
// Serve the entry-point via an HTTP/2 server
restate.serve({
services: [agent],
});
```
The Invocations tab of the Restate UI shows us how Restate captured each LLM call and tool step in a journal:
Ask about the weather in Denver:
```shell theme={null}
curl localhost:8080/agent/run --json '"What is the weather in Denver?"'
```
You can see in the service logs and in the Restate UI how each LLM call and tool step gets durably executed.
We can see how the weather tool is currently stuck, because the weather API is down.
This was a mimicked failure. To fix the problem, remove the line `failOnDenver` from the `fetchWeather` function in the `utils.ts` file:
```ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/vercel-ai/template/src/utils/weather.ts#weather"} theme={null}
export async function fetchWeather(city: string) {
failOnDenver(city);
const output = await fetchWeatherFromAPI(city);
return parseWeatherResponse(output);
}
```
Once you restart the service, the agent resumes at the weather tool call and successfully completes the request.
**Next step:**
Follow the [Tour of Agents](/tour/vercel-ai-agents) to learn how to build agents with Restate and Vercel AI SDK, OpenAI Agents SDK, etc.
**Prerequisites**:
* Python >= v3.12
* [uv](https://docs.astral.sh/uv/getting-started/installation/)
* OpenAI API key (get one at [OpenAI](https://platform.openai.com/))
Restate is a single self-contained binary. No external dependencies needed.
```shell theme={null}
brew install restatedev/tap/restate-server restatedev/tap/restate
```
Start the server:
```shell theme={null}
restate-server
```
Download prebuilt binaries from the [releases page](https://github.com/restatedev/restate/releases/latest):
```shell MacOS-x64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=x86_64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell MacOS-arm64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=aarch64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell Linux-x64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=x86_64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
```shell Linux-arm64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=aarch64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
Start the server:
```shell theme={null}
restate-server
```
```shell theme={null}
npm install --global @restatedev/restate-server@latest @restatedev/restate@latest
```
Start the server:
```shell theme={null}
restate-server
```
Run the Restate Server:
```shell theme={null}
docker run --name restate_dev --rm \
-p 8080:8080 -p 9070:9070 -p 9071:9071 \
--add-host=host.docker.internal:host-gateway \
docker.restate.dev/restatedev/restate:latest
```
Run CLI commands:
```shell theme={null}
docker run -it --network=host \
docker.restate.dev/restatedev/restate-cli:latest \
invocations ls
```
Replace `invocations ls` with any CLI subcommand.
You can find the Restate UI running on port 9070 (`http://localhost:9070`) after starting the Restate Server.
Get the weather agent template for the [OpenAI Agents SDK](https://openai.github.io/openai-agents-python/) and Restate:
```shell theme={null}
git clone https://github.com/restatedev/ai-examples.git &&
cd ai-examples/openai-agents/template
```
Export your OpenAI key and run the agent:
```shell theme={null}
export OPENAI_API_KEY=your_openai_api_key_here
uv run .
```
The weather agent is now listening on port 9080.
Tell Restate where the service is running (`http://localhost:9080`), so Restate can discover and register the services and handlers behind this endpoint.
You can do this via the UI (`http://localhost:9070`) or via:
```shell CLI theme={null}
restate deployments register http://localhost:9080
```
```shell curl theme={null}
curl localhost:9070/deployments --json '{"uri": "http://localhost:9080"}'
```
```shell CLI theme={null}
❯ SERVICES THAT WILL BE ADDED:
- Agent
Type: Service
HANDLER INPUT OUTPUT
run value of content-type 'application/json' value of content-type 'application/json'
✔ Are you sure you want to apply those changes? · yes
✅ DEPLOYMENT:
SERVICE REV
Agent 1
```
```shell curl theme={null}
{
"id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"services": [
{
"name": "Agent",
"handlers": [
{
"name": "run",
"ty": "Shared",
"input_description": "one of [\"none\", \"value of content-type 'application/json'\"]",
"output_description": "value of content-type 'application/json'"
}
],
"ty": "Service",
"deployment_id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"revision": 1,
"public": true,
"idempotency_retention": "1day"
}
]
}
```
If you run Restate with Docker, register `http://host.docker.internal:9080` instead of `http://localhost:9080`.
When using [Restate Cloud](https://restate.dev/cloud), your service must be accessible over the public internet so Restate can invoke it.
If you want to develop with a local service, you can expose it using our [tunnel](/deploy/server/cloud/#registering-restate-services-with-your-environment) feature.
Invoke the agent via the Restate UI playground: go to `http://localhost:9070`, click on your service and then on playground.
Or invoke via `curl`:
```shell theme={null}
curl localhost:8080/agent/run --json '"What is the weather in Detroit?"'
```
Output: `The weather in Detroit is currently 17°C with misty conditions.`
The agent you just invoked uses Durable Execution to make agents resilient to failures. Restate persisted all LLM calls and tool execution steps, so if anything fails, the agent can resume exactly where it left off.
We did this by using the `DurableModelCalls` model provider to persist LLM responses and using [Restate Context actions](/foundations/actions) (e.g. `ctx.run`) to make the tool executions resilient:
```python expandable {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/openai-agents/template/agent.py?collapse_imports"} theme={null}
@function_tool(failure_error_function=raise_restate_errors)
async def get_weather(
wrapper: RunContextWrapper[restate.Context], req: WeatherRequest
) -> WeatherResponse:
"""Get the current weather for a given city."""
# Do durable steps using the Restate context
restate_context = wrapper.context
return await restate_context.run_typed("Get weather", fetch_weather, city=req.city)
weather_agent = Agent[restate.Context](
name="WeatherAgent",
instructions="You are a helpful agent that provides weather updates.",
tools=[get_weather],
)
agent_service = restate.Service("agent")
@agent_service.handler()
async def run(restate_context: restate.Context, message: str) -> str:
result = await Runner.run(
weather_agent,
input=message,
# Pass the Restate context to tools to make tool execution steps durable
context=restate_context,
# Choose any model and let Restate persist your calls
run_config=RunConfig(
model="gpt-4o", model_provider=DurableModelCalls(restate_context)
),
)
return result.final_output
```
The Invocations tab of the Restate UI shows us how Restate captured each LLM call and tool step in a journal:
Ask about the weather in Denver:
```shell theme={null}
curl localhost:8080/agent/run --json '"What is the weather in Denver?"'
```
You can see in the service logs and in the Restate UI how each LLM call and tool step gets durably executed.
We can see how the weather tool is currently stuck, because the weather API is down.
This was a mimicked failure. To fix the problem, remove the line `fail_on_denver` from the `fetch_weather` function in the `utils.py` file:
```python {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/openai-agents/template/utils/utils.py#weather"} theme={null}
async def fetch_weather(city: str) -> WeatherResponse:
fail_on_denver(city)
weather_data = await call_weather_api(city)
return parse_weather_data(weather_data)
```
Once you restart the service, the agent resumes at the weather tool call and successfully completes the request.
# Connecting services to Restate Cloud
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/cloud/connecting-services
Learn how to connect your services to Restate Cloud.
You can connect your services to Restate Cloud in several ways, depending on where they run:
* If your [services run in private environments](#connecting-services-in-private-environments), by setting up a tunnel.
* If your [services run on AWS Lambda](#connecting-aws-lambda-services), by giving Restate Cloud permission to invoke them.
* By making your [services accessible over the public internet](#public-endpoint-with-request-signing), and using request signing.
## Connecting services in private environments
You can connect services that run in a locked-down private environment to Restate Cloud using a secure tunnel.
This allows you to expose your services to Restate Cloud without opening up ports to the public internet.
The tunnel will also act as an authenticating proxy to Restate Cloud and make it appear as if your Restate Cloud environment is inside your private network.
This allows you to use native access control mechanisms, like VPC Security Groups and Kubernetes network policies, to manage access to your environment.
### 1. Create a Restate Cloud environment
Open the [Restate Cloud UI](https://cloud.restate.dev) and create a new environment.
Note the environment id (`env_...`) and request signing key (under security -> HTTP services: `publickeyv1_...`), as you will need them later.
Create a `"Full"` scope API key with a descriptive name for the tunnel client and copy the key for later.
### 2. Setup the CLI
Next, we need to set up a CLI profile to connect to our Restate Cloud environment.
Log in to Restate Cloud using the CLI:
```bash theme={null}
restate cloud login
```
Set up a new CLI profile for your environment:
```bash theme={null}
restate cloud env configure
```
Tell the CLI to use the new environment:
```bash theme={null}
restate config use-env
```
### 3. Run the tunnel
The tunnel can be hosted as a cloud VM in your VPC, a sidecar to your service, or a dedicated pod in your container orchestrator.
You can deploy multiple copies for redundancy.
To run the tunnel:
```bash theme={null}
# export RESTATE_ENVIRONMENT_ID=env_...
# export RESTATE_BEARER_TOKEN=key_...
# export RESTATE_TUNNEL_NAME=test-tunnel
# export RESTATE_SIGNING_PUBLIC_KEY=publickeyv1_...
# export RESTATE_CLOUD_REGION=eu
docker run \
-e RESTATE_ENVIRONMENT_ID \
-e RESTATE_BEARER_TOKEN \
-e RESTATE_TUNNEL_NAME \
-e RESTATE_SIGNING_PUBLIC_KEY \
-e RESTATE_CLOUD_REGION \
-p 8080:8080 \
-p 9090:9090 \
-p 9070:9070
-it ghcr.io/restatedev/restate-cloud-tunnel-client:latest
```
* `RESTATE_ENVIRONMENT_ID` is the environment id (including the `env_` prefix).
* `RESTATE_BEARER_TOKEN` is the API key you created in step 1.
* `RESTATE_TUNNEL_NAME` is a name for the tunnel. Choose a unique DNS-friendly tunnel name, e.g. `prod-tunnel`.
* `RESTATE_SIGNING_PUBLIC_KEY` is the public key you copied from the Cloud UI in step 1.
* `RESTATE_CLOUD_REGION` is the region of your Restate Cloud environment, e.g. `eu` or `us`.
* You can run `latest` or pin the current version, e.g. `0.4.0`
* The health check URL is at `:9090/health`
The tunnel client exposes the following ports
* `9090` tunnel's own health status
* `8080` Restate Ingress (authenticating proxy)
* `9070` Restate Admin API (authenticating proxy)
### 4. Run a Restate service
Follow the quickstart to [run a Restate service locally](/quickstart).
```bash TypeScript theme={null}
restate example typescript-hello-world
cd typescript-hello-world
npm install
npm run dev
```
```bash Java theme={null}
restate example java-hello-world-maven
cd java-hello-world-maven
mvn compile exec:java
```
```bash Kotlin theme={null}
restate example kotlin-hello-world
cd kotlin-hello-world
./gradlew run
```
```bash Python theme={null}
restate example python-hello-world
cd python-hello-world
uv run .
```
```Bash Go theme={null}
restate example go-hello-world &&
cd go-hello-world
go run .
```
```bash Rust theme={null}
restate example rust-hello-world
cd rust-hello-world
cargo run
```
### 5. Register your service
If your setup is correct, you can now register your service with the Restate Cloud environment:
```bash theme={null}
restate deployments register https://tunnel..restate.cloud:9080///http//
```
* Use just the numeric environment id here (without the `env_` prefix).
* The tunnel name must match the name provided to the tunnel client
* The remote fqdn and port must resolve to the Restate service endpoint (e.g. `localhost:9080`) from the perspective of the tunnel client
For example, if your service is running on `localhost:9080` and your environment id is `env_20d1231jyphzkm8` in region `eu`, you can register it like this:
```bash theme={null}
restate deployments register https://tunnel.eu.restate.cloud:9080/20d1231jyphzkm8/test-tunnel/http/localhost/9080
```
### 6. Test your service
You can now test your service by invoking it via Restate Cloud:
```bash TypeScript theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Sarah"}'
```
```bash Java theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Sarah"}'
```
```bash Kotlin theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Sarah"}'
```
```bash Python theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Sarah"}'
```
```Bash Go theme={null}
curl localhost:8080/Greeter/Greet --json '"Sarah"'
```
```bash Rust theme={null}
curl localhost:8080/Greeter/greet --json '"Sarah"'
```
### How does it work?
* Restate CLI is communicating with your Restate environment at `https://*.eu.restate.cloud:9070` using your user ID token and asks the admin API to perform a discovery at the special tunnel URL.
* The Restate Cloud end of the tunnel receives the request from your environment and forwards the traffic to the tunnel container.
* The tunnel container is forwarding the traffic to the Restate service endpoint.
## Connecting AWS Lambda services
To invoke services running on AWS Lambda, Restate Cloud needs to assume an AWS
identity in the same account that the Lambda is deployed to. Create a new role
that has permission to invoke your Lambda handlers, and give it the following
trust policy.
The Restate Cloud role is distinct from the Lambda function's execution role.
The execution role is assumed by your function to perform its work. A dedicated
invoker role is needed to grant Restate Cloud permission to invoke service handlers
functions in your account, and nothing more.
```json IAM JSON Policy expandable theme={null}
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::654654156625:root"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"aws:PrincipalArn": "arn:aws:iam::654654156625:role/RestateCloud",
"sts:ExternalId": "${ENVIRONMENT_ID}"
}
}
},
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::654654156625:root"
},
"Action": "sts:TagSession"
}
]
}
```
Replace the `${ENVIRONMENT_ID}` placeholder with the environment ID can be found in the UI and in the output of `restate whoami`.
This trust policy allows the Restate Cloud `us.restate.cloud` region principal to assume the role, but only on behalf of the specified environment ID.
```ts AWS CDK theme={null}
const restateCloudRole = new iam.Role(this, "RestateCloudRole", {
assumedBy: new iam.AccountPrincipal("654654156625")
.withConditions({
"StringEquals": {
"sts:ExternalId": environmentId,
"aws:PrincipalArn": "arn:aws:iam::654654156625:role/RestateCloud",
},
}),
});
restateCloudRole.assumeRolePolicy!.addStatements(
new iam.PolicyStatement({
principals: [new iam.AccountPrincipal("654654156625")],
actions: ["sts:TagSession"],
}),
);
```
When you use the [Restate CDK construct library](https://github.com/restatedev/cdk) to deploy
Lambda handlers, the provided invoker role will automatically be granted access
to invoke the corresponding functions. If you manage Restate service deployments
some other way, you should ensure that the Restate Cloud invoker role is permitted
to call the appropriate Lambda handler functions by allowing it to perform `lambda:InvokeFunction`.
Use the `environmentId` variable to pass the environment ID can be found in the UI and in the output of `restate whoami`.
This trust policy allows the Restate Cloud `us.restate.cloud` region principal to assume the role, but only on behalf of the specified environment.
You can now register your Lambda through the new role:
```shell theme={null}
restate deployments register --assume-role-arn
```
If something isn't working, the environment logs in the cloud UI may help
you find the issue.
**Securing your services:**
If your Lambda has an appropriate trust policy as described above, you do not
need to secure incoming requests any further. If you choose to however, the
identity verification checks will work on Lambda endpoints as well.
## Public endpoint with request signing
Restate can invoke your services over the public internet. For production use cases
HTTPS must be used between Restate and your services. You can terminate TLS at
the service endpoint directly, but it's likely easier to use a fronting
load balancer like an AWS NLB.
You must secure access to your service so that only Restate can call it.
The easiest way to do this is with our native request identity feature.
All requests to your service will be signed with a unique environment-specific private
key. You can find the corresponding public key in the environment settings UI, under HTTP Services.
It is safe to include this public key directly in your service code.
Have a look at the SDK serving documentation to learn how for [TS](/develop/ts/serving#validating-request-identity), [Java, Kotlin](/develop/java/serving#validating-request-identity), [Python](/develop/python/serving#validating-request-identity), [Go](/develop/go/serving#validating-request-identity), and [Rust](https://docs.rs/restate-sdk/latest/restate_sdk/http_server/index.html#validating-request-identity).
# Getting Started with Restate Cloud
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/cloud/getting-started
Learn how to use Restate Cloud, the fully managed version of Restate.
Learn more and sign up.
Restate Cloud is a fully managed serverless version of Restate, with a suite of tools supporting a stellar local development experience, and security features like [request signing](#securing-your-services) and [IAM roles for AWS Lambda](#aws-lambda-services).
This allows you to focus on your services, which you can deploy wherever you normally do, while Restate Cloud handles all aspects of availability and durability for your invocations, workflows, and state.
## Creating your first environment
You can sign in to Cloud using an email address and password, or using Google or GitHub SSO.
After signing up, you'll be prompted to create an account, and an environment. An environment is an instance of the Restate server that is unique to you, and managed by Restate.
Accounts and environments must have a name comprising of lowercase alphanumeric
characters or hyphens. For example, you can choose `dev` for your account and
`my-environment` for your environment.
You can start using your new environment straight away using the [CLI](/installation#running-restate-server--cli-locally):
```bash theme={null}
# authenticate to Cloud
restate cloud login
# set up your CLI to talk to your Cloud environment
restate cloud env configure
# check that everything is working!
restate whoami
```
At any time, you can switch your CLI back to point to a Restate server running
on your machine with `restate config use-environment local` (see the
[CLI config docs](/references/cli-config#)).
## Registering Restate services with your environment
You can use your Cloud environment exactly as a self-hosted one; register
services with `restate deployments register `.
However, currently your services must be accessible over the public internet for
Restate to be able to invoke them. If you want to develop using a
service running on your local machine, you can expose it using our tunnel
feature:
```bash theme={null}
# expose localhost:9080 to Restate Cloud
restate cloud env tunnel --local-port 9080
# copy the tunnel url from the output and register it to your environment
restate deployments register tunnel://example:9081
```
## Invoking your services
In the UI you can find the URL of your Restate environment. All requests must be
authenticated using a Bearer token. We suggest that humans use their SSO token
which is obtained by `restate cloud login`, and will be used automatically by
the CLI. If you want to test invocations using `curl` as described in the [invoke docs](/services/invocation/http), you can use the tunnel command to expose
the ingress port of the Restate server as if it was running on your machine:
```bash theme={null}
restate cloud env tunnel --remote-port 8080
curl localhost:8080/MyService/myHandler
```
### API tokens
For programmatic invocation, awakeable completion, or admin API usage from
scripts, CI pipelines, API gateways, or services that exist outside Restate,
you can create an API key in the UI, selecting an appropriate role for your use
case.
```bash theme={null}
curl -H "Authorization: Bearer $MY_API_KEY" https://201hy10cd3h6426jy80tb32n6en.env.us.restate.cloud:8080/MyService/MyHandler
curl -H "Authorization: Bearer $MY_API_KEY" https://201hy10cd3h6426jy80tb32n6en.env.us.restate.cloud:9070/deployments
```
To use the CLI programmatically with this token:
```bash theme={null}
export RESTATE_HOST_SCHEME=https RESTATE_HOST=201hy10cd3h6426jy80tb32n6en.env.us.restate.cloud RESTATE_AUTH_TOKEN=$MY_API_KEY
restate whoami
```
# AI-Assisted Development
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/ai-assistant
Developing Restate apps with AI coding agents.
The Restate documentation site includes several features to help you leverage AI coding agents like Cursor and Claude Code in your development workflow.
## Contextual menu
Each page of the Restate documentation has a contextual menu in the top right corner that provides quick access to various AI resources:
You can use this to quickly hydrate your AI chat with the content of the current page, or to add the Restate docs MCP server to Cursor, or VSCode.
## Restate Docs as MCP server
The Restate documentation is available as an MCP server, which you can add to Cursor or other AI coding agents that support MCP.
You can either use the shortcuts in the [contextual menu](/develop/ai-assistant#contextual-menu) or follow the following guidelines:
1. Navigate to the `Connectors` page in the Claude settings.
2. Select `Add custom connector`.
3. Add the MCP server name and URL: `restate-docs`, `https://docs.restate.dev/mcp`.
4. Select `Add`.
5. When using Claude, select the attachments button (the plus icon).
6. Select your MCP server.
To add the Restate documentation MCP server to Claude Code, run the following command in your terminal:
```shell theme={null}
claude mcp add --transport http restate-docs https://docs.restate.dev/mcp
```
1. Use `Command` + `Shift` + `P` (`Ctrl` + `Shift` + `P` on Windows) to open the command palette.
2. Search for `“Open MCP settings”`.
3. Select `Add custom MCP`. This will open the `mcp.json` file.
4. In `mcp.json`, configure your server:
```json theme={null}
{
"mcpServers": {
"restate-docs": {
"url": "https://docs.restate.dev/mcp"
}
}
}
```
1. Create a `.vscode/mcp.json` file.
2. In `mcp.json`, configure your server:
```json theme={null}
{
"servers": {
"restate-docs": {
"type": "http",
"url": "https://docs.restate.dev/mcp"
}
}
}
```
More information in the [Mintlify documentation](https://www.mintlify.com/docs/ai/model-context-protocol#using-your-mcp-server).
## AGENTS.md: Coding agent rules
AGENTS.md is a simple markdown file for defining agent instructions.
To improve the performance of coding agents when implementing Restate applications, add the following rules to your agent's context.
* [TypeScript SDK AGENTS.md](/develop/ts/agents.md)
* [Java SDK AGENTS.md](/develop/java/agents.md)
* [Python SDK AGENTS.md](/develop/python/agents.md)
* [Go SDK AGENTS.md](/develop/go/agents.md)
### Cursor
Download the AGENTS.md file and put it [in the `.cursor/rules` folder](https://cursor.com/docs/context/rules#agentsmd) at the root of your project:
```shell Typescript theme={null}
mkdir -p ./.cursor/rules && wget -O ./.cursor/rules/AGENTS.md https://docs.restate.dev/develop/ts/agents.md
```
```shell Java theme={null}
mkdir -p ./.cursor/rules && wget -O ./.cursor/rules/AGENTS.md https://docs.restate.dev/develop/java/agents.md
```
```shell Python theme={null}
mkdir -p ./.cursor/rules && wget -O ./.cursor/rules/AGENTS.md https://docs.restate.dev/develop/python/agents.md
```
```shell Go theme={null}
mkdir -p ./.cursor/rules && wget -O ./.cursor/rules/AGENTS.md https://docs.restate.dev/develop/go/agents.md
```
### Claude Code
Download the CLAUDE.md file and put it [in the `.claude` folder](https://www.anthropic.com/engineering/claude-code-best-practices) at the root of your project:
```shell Typescript theme={null}
mkdir -p ./.claude && wget -O ./.claude/CLAUDE.md https://docs.restate.dev/develop/ts/agents.md
```
```shell Java theme={null}
mkdir -p ./.claude && wget -O ./.claude/CLAUDE.md https://docs.restate.dev/develop/java/agents.md
```
```shell Python theme={null}
mkdir -p ./.claude && wget -O ./.claude/CLAUDE.md https://docs.restate.dev/develop/python/agents.md
```
```shell Go theme={null}
mkdir -p ./.claude && wget -O ./.claude/CLAUDE.md https://docs.restate.dev/develop/go/agents.md
```
## llms.txt and llms-full.txt
The Restate documentation is available in markdown, for easy ingestion by LLMs.
Add `.md` to any page’s URL to view a Markdown version.
The documentation also includes [`llms.txt`](/llms.txt) (navigation structure) and [`llms-full.txt`](/llms-full.txt) (full documentation) files.
# Code Generation
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/go/code-generation
Use proto for type-safe servers and clients.
The Go SDK supports generating typed clients and server interfaces from
[Protocol Buffer service definitions](https://protobuf.dev/programming-guides/proto3/#services).
A full example can be found in the [SDK repository](https://github.com/restatedev/sdk-go/tree/main/examples/codegen).
Code generation relies on the tool `protoc-gen-go-restate` which you can install using
`go install github.com/restatedev/sdk-go/protoc-gen-go-restate@latest`, in addition to `protoc`
which you can install from the [Protobuf compiler repo](https://github.com/protocolbuffers/protobuf#protobuf-compiler-installation).
## Defining a service
To get started, create a Protocol Buffer definition file defining a service:
```proto service.proto {"CODE_LOAD::go/develop/codegen/proto/service.proto#service"} theme={null}
package greeter;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloResponse) {}
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string message = 1;
}
```
You can compile this file into Go types and your Restate generated code with:
```shell theme={null}
protoc --go_out=. --go_opt=paths=source_relative --go-restate_out=. --go-restate_opt=paths=source_relative proto/service.proto
```
This creates two files: `service.pb.go` containing Go types, and `service_restate.pb.go` which contains several useful objects:
1. `NewGreeterClient`, which returns an interface for making type-safe calls to the Greeter service. The default serialization
for request and response data is the [canonical Protobuf JSON encoding](https://protobuf.dev/programming-guides/proto3/#json),
but this is configurable.
2. `GreeterServer`, an interface for server code to implement. An implementor can be converted into a service definition with
`NewGreeterServer`.
3. `UnimplementedGreeterServer`, an empty struct that implements `GreeterServer` by just returning 'not implemented' terminal
errors for each method. You can choose to embed this struct into your own implementation struct, in which case new methods
in the Protobuf definition will not cause compilation errors in your Go code, which can help with backwards compatibility.
Both `NewGreeterClient` and `NewGreeterServer` accept option parameters which can be used to set a different serialization codec.
For example, you can use Protobuf by providing `restate.WithProto` to both functions.
## Implementing the server
Next, you can implement the `GreeterServer` interface and bind it to your Restate server.
```go {"CODE_LOAD::go/develop/codegen/codegen.go#server"} theme={null}
type greeter struct {
proto.UnimplementedGreeterServer
}
func (greeter) SayHello(ctx restate.Context, req *proto.HelloRequest) (*proto.HelloResponse, error) {
return &proto.HelloResponse{
Message: fmt.Sprintf("%s!", req.Name),
}, nil
}
func main() {
if err := server.NewRestate().
Bind(proto.NewGreeterServer(greeter{})).
Start(context.Background(), ":9080"); err != nil {
log.Fatal(err)
}
}
```
## Using the client
`NewGreeterClient` will return a similar client to what you'd get from `restate.Service`, with additional
type safety for method names and request/response types.
```go {"CODE_LOAD::go/develop/codegen/codegen.go#client"} theme={null}
client := proto.NewGreeterClient(ctx)
resp, err := client.SayHello().Request(&proto.HelloRequest{Name: "world"})
if err != nil {
return err
}
```
## Defining Virtual Objects and Workflows
To define a Virtual Object or Workflow, you will need to provide the option `dev.restate.sdk.go.service_type` when defining the service.
This option is defined by the Go SDK's proto file
[`dev/restate/sdk/go.proto`](https://github.com/restatedev/sdk-go/blob/main/proto/dev/restate/sdk/go.proto) which must be imported in your own file,
and a directory containing it must be provided to protoc eg with `-I $GOPATH/src/github.com/restatedev/sdk-go/proto`
You may additionally provide the option `dev.restate.sdk.go.handler_type` to denote that the handler is a shared handler.
```proto service.proto {"CODE_LOAD::go/develop/codegen/proto/service.proto#virtual_object"} theme={null}
import "dev/restate/sdk/go.proto";
service Counter {
option (dev.restate.sdk.go.service_type) = VIRTUAL_OBJECT;
rpc Add (AddRequest) returns (AddResponse) {}
rpc Get (GetRequest) returns (GetResponse) {
option (dev.restate.sdk.go.handler_type) = SHARED;
}
}
service Workflow {
option (dev.restate.sdk.go.service_type) = WORKFLOW;
rpc Run (RunRequest) returns (RunResponse) {}
rpc GetStatus (GetStatusRequest) returns (GetStatusResponse) {}
}
```
Clients are instantiated with a key:
```go {"CODE_LOAD::go/develop/codegen/codegen.go#virtual_object_client"} theme={null}
client := proto.NewCounterClient(ctx, "key-1")
resp, err := client.Add().Request(&proto.AddRequest{Delta: 1})
if err != nil {
return err
}
```
Instead of manually wrangling `protoc` to handle generation and proto imports,
it can be a lot easier to use [Buf](https://buf.build/).
Buf determines what code to generate based on a `buf.gen.yaml` file:
```yaml theme={null}
version: v2
managed:
enabled: true
plugins:
- remote: buf.build/protocolbuffers/go:v1.34.2
out: .
opt: paths=source_relative
- local: protoc-gen-go-restate
out: .
opt: paths=source_relative
inputs:
- directory: .
```
And to allow the import of `dev/restate/sdk/go.proto`, you can specify the dependency in your `buf.yaml`:
```yaml theme={null}
version: v2
lint:
use:
- DEFAULT
breaking:
use:
- FILE
deps:
- buf.build/restatedev/sdk-go
```
Then you can run `buf generate` to generate the code.
# Concurrent Tasks
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/go/concurrent-tasks
Execute multiple tasks concurrently and gather results.
When building resilient applications, you often need to perform multiple operations in parallel to improve performance and user experience. Restate provides durable concurrency primitives that allow you to run tasks concurrently while maintaining deterministic execution during replays.
## When to use concurrent tasks
Use concurrent tasks when you need to:
* Call multiple external services simultaneously (e.g., fetching data from different APIs)
* Race multiple operations and use the first result (e.g., trying multiple LLM providers)
* Implement timeouts by racing an operation against a timer
* Perform batch operations where individual tasks can run in parallel
## Key benefits
* **Deterministic replay**: Restate logs the order of completion, ensuring consistent behavior during failures
* **Fault tolerance**: If your handler fails, tasks that were already completed will be replayed with their results, while pending tasks will be retried
## Parallelizing tasks
Start multiple durable operations concurrently by calling them without immediately awaiting:
```go {"CODE_LOAD::go/develop/journalingresults.go#parallel"} theme={null}
call1 := restate.RunAsync(ctx, func(ctx restate.RunContext) (UserData, error) {
return fetchUserData(123)
})
call2 := restate.RunAsync(ctx, func(ctx restate.RunContext) (OrderHistory, error) {
return fetchOrderHistory(123)
})
call3 := restate.Service[int](ctx, "AnalyticsService", "CalculateMetrics").RequestFuture(123)
user, _ := call1.Result()
orders, _ := call2.Result()
metrics, _ := call3.Response()
```
Check out the guide on [parallelizing work](guides/parallelizing-work).
## Retrieving results
Operations such as calls, awakeables, and `restate.After` return futures which can
be safely selected over using `restate.Select`. Restate will log the order in which they complete, to make this deterministic on replay.
**Don't use Go routines**:
Do not try to combine blocking Restate operations using goroutines, channels or select statements. These cannot be used deterministically, and will likely lead to non-determinism errors upon replay. The only place it is safe to use these types is inside of a restate.Run function.
### Select the first successful completion
```go {"CODE_LOAD::go/develop/journalingresults.go#race"} theme={null}
sleepFuture := restate.After(ctx, 30*time.Second)
callFuture := restate.Service[string](ctx, "MyService", "MyHandler").RequestFuture("hi")
selector := restate.Select(ctx, sleepFuture, callFuture)
switch selector.Select() {
case sleepFuture:
if err := sleepFuture.Done(); err != nil {
return "", err
}
return "sleep won", nil
case callFuture:
result, err := callFuture.Response()
if err != nil {
return "", err
}
return fmt.Sprintf("call won with result: %s", result), nil
}
```
Select blocks on the next completed operation or returns `nil` if there are none left.
### Wait for all tasks to complete
```go {"CODE_LOAD::go/develop/journalingresults.go#all"} theme={null}
callFuture1 := restate.Service[string](ctx, "MyService", "MyHandler").RequestFuture("hi")
callFuture2 := restate.Service[string](ctx, "MyService", "MyHandler").RequestFuture("hi again")
selector := restate.Select(ctx, callFuture1, callFuture2)
// Collect all results
var subResults []string
for selector.Remaining() {
response, err := selector.Select().(restate.ResponseFuture[string]).Response()
if err != nil {
return "", err
}
subResults = append(subResults, response)
}
```
# Durable Steps
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/go/durable-steps
Persist results of operations.
Restate uses an execution log to replay operations after failures and suspensions. Non-deterministic operations (database calls, HTTP requests, UUID generation) must be wrapped to ensure deterministic replay.
## Run
Use `Run` to safely wrap any non-deterministic operation, like HTTP calls or database responses, and have Restate store its result in the execution log.
```go {"CODE_LOAD::go/develop/journalingresults.go#side_effect"} theme={null}
result, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
return doDbRequest()
})
if err != nil {
return err
}
```
Note that inside `Run`, you cannot use the Restate context (e.g., `get`, `sleep`, or nested `Run`).
You should only use methods available on the [`RunContext`](https://pkg.go.dev/github.com/restatedev/sdk-go#RunContext) provided to your function.
You can return any payload that can be serialized. By default, serialization is
done with [`JSONCodec`](https://pkg.go.dev/github.com/restatedev/sdk-go/encoding#PayloadCodec)
which uses `encoding/json`. If you don't need to return anything, you can use
`restate.Void{}` which serialises to a nil byte slice.
Failures in `Run` are treated the same as any other handler error. Restate will retry it unless configured otherwise or unless a [`TerminalError`](/develop/go/error-handling) is thrown.
You can customize how `Run` retries via:
```go {"CODE_LOAD::go/develop/journalingresults.go#side_effect_retry"} theme={null}
result, err := restate.Run(ctx,
func(ctx restate.RunContext) (string, error) {
return doDbRequest()
},
// After 10 seconds, give up retrying
restate.WithMaxRetryDuration(time.Second*10),
// On the first retry, wait 100 milliseconds before next attempt
restate.WithInitialRetryInterval(time.Millisecond*100),
// Grow retry interval with factor 2
restate.WithRetryIntervalFactor(2.0),
// Optional: provide a name for the operation to be visible in the
// observability tools.
restate.WithName("my_db_request"),
)
if err != nil {
return err
}
```
* You can limit retries by time or count
* When the policy is exhausted, a `TerminalError` is thrown
* See the [Error Handling Guide](/guides/error-handling) and the [Sagas Guide](/guides/sagas) for patterns like compensation
If Restate doesn't receive new journal entries from a service for more than one minute (by default), it will automatically abort the invocation and retry it.
However, some business logic can take longer to complete—for example, an LLM call that takes up to 3 minutes to respond.
In such cases, you can adjust the service’s [abort timeout and inactivity timeout](/services/configuration) settings to accommodate longer execution times.
For more information, see the [error handling guide](/guides/error-handling).
## Deterministic randoms
The SDK provides deterministic helpers for random values — seeded by the invocation ID — so they return the **same result on retries**.
### UUIDs
To generate stable UUIDs for things like idempotency keys:
```go {"CODE_LOAD::go/develop/journalingresults.go#uuid"} theme={null}
uuid := restate.Rand(ctx).UUID()
```
Do not use this in cryptographic contexts.
### Random numbers
Methods exist on `restate.Rand(ctx)` for generating `float64` and `uint64`, or
otherwise `restate.Rand(ctx).Source()` can be provided to `math/rand/v2` as a
source for any random operation:
```go {"CODE_LOAD::go/develop/journalingresults.go#random_nb"} theme={null}
randomInt := restate.Rand(ctx).Uint64()
randomFloat := restate.Rand(ctx).Float64()
randomSource := rand.New(restate.Rand(ctx).Source())
```
# Scheduling & Timers
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/go/durable-timers
Durable timers, scheduled actions, and sleep, backed by Restate.
Restate provides durable, fault-tolerant timers that allow you to:
* [Sleep](#durable-sleep): Pause a handler for a given duration
* [Send delayed messages](#scheduling-async-tasks): Schedule handler invocations in the future
* [Set timeouts](#timers-and-timeouts) for async operations
* Implement patterns like [cron jobs](#cron-jobs)
## How it works
Restate tracks and manages timers, ensuring they survive failures and restarts.
For example, if a service is sleeping for 12 hours and fails after 8 hours, then Restate will make sure it only sleeps 4 hours more.
If your handler runs on function-as-a-service platforms like AWS Lambda, Restate suspends the handler while it is sleeping, to free up resources.
Since these platforms charge on execution time, this saves costs.
## Durable sleep
To pause a handler for a set duration:
```go {"CODE_LOAD::go/develop/durabletimers.go#here"} theme={null}
if err := restate.Sleep(ctx, 10*time.Second); err != nil {
return "", err
}
```
There is no hard limit on how long you can sleep. You can sleep for months or even years, but keep in mind:
* If you sleep in an [exclusive handler](/foundations/handlers#handler-behavior) in a Virtual Object, all other calls to this object will be queued.
* You need to keep the deployment version until the invocation completes (see [versioning](/services/versioning)).
So for long sleeps, we recommend breaking this up into multiple handlers that call each other with [delayed messages](/develop/go/service-communication#delayed-messages).
The Restate SDK calculates the wake-up time based on the delay you specify.
The Restate Server then uses this calculated time to wake up the handler.
If the Restate Server and the SDK have different system clocks, the sleep duration might not be accurate.
So make sure that the system clock of the Restate Server and the SDK have the same timezone and are synchronized.
A mismatch can cause timers to fire earlier or later than expected.
## Scheduling async tasks
To invoke a handler at a later time, use [delayed messages](/develop/go/service-communication#delayed-messages).
In theory, you can schedule future work in Restate in two ways:
1. **Delayed messages** (recommended)
2. **Sleep + send** - sleeping in the current handler, then sending a message
At first sight, both approaches might seem to achieve similar results.
However, we recommend to use delayed messages for the following reasons:
* **Handler completes immediately**: The calling handler can finish execution and complete the invocation without waiting for the delay to finish
* **No Virtual Object blocking**: Other requests to the same Virtual Object can be processed during the delay period
* **Better for service versioning**: No long-running invocations that require keeping old service deployments around for a long time (see [service versioning](/services/versioning))
## Timers and timeouts
Most context actions are async actions that return a future.
You can use [Restate's combinators](/develop/go/concurrent-tasks) to race an async operation against a timeout:
```go {"CODE_LOAD::go/develop/durabletimers.go#timer"} theme={null}
sleepFuture := restate.After(ctx, 30*time.Second)
callFuture := restate.Service[string](ctx, "MyService", "MyHandler").RequestFuture("hi")
selector := restate.Select(ctx, sleepFuture, callFuture)
switch selector.Select() {
case sleepFuture:
if err := sleepFuture.Done(); err != nil {
return "", err
}
return "sleep won", nil
case callFuture:
result, err := callFuture.Response()
if err != nil {
return "", err
}
return fmt.Sprintf("call won with result: %s", result), nil
}
```
## Cron jobs
Restate does not yet include native cron support, but you can implement your own cron scheduler using:
* Durable timers
* Virtual Objects
* A repeat loop or sleep-schedule pattern
Check out the guide on [implementing cron jobs](/guides/cron).
# Error Handling
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/go/error-handling
Stop infinite retries with Terminal Errors.
Restate handles retries for failed invocations.
Check out the [Error Handling guide](/guides/error-handling) to learn more about how Restate handles transient errors, terminal errors, retries, and timeouts.
## Retry strategies
By default, Restate does infinite retries with an exponential backoff strategy.
Check out the [error handling guide](/guides/error-handling) to learn how to customize this.
## Terminal Errors
For failures for which you do not want retries, but instead want the invocation to end and the error message
to be propagated back to the caller, you can throw a **terminal error**.
You can throw a `TerminalError` with an optional HTTP status code and a message anywhere in your handler, as follows:
```go {"CODE_LOAD::go/develop/errorhandling.go#here"} theme={null}
return restate.TerminalError(fmt.Errorf("Something went wrong."), 500)
```
You can catch terminal errors, and build your control flow around it.
When you throw a terminal error, you might need to undo the actions you did earlier in your handler to make sure that your system remains in a consistent state.
Have a look at our [sagas guide](/guides/sagas) to learn more.
# External Events
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/go/external-events
Handle external events and human-in-the-loop patterns with durable waiting primitives.
Sometimes your handlers need to pause and wait for external processes to complete. This is common in:
* **Human-in-the-loop workflows** (approvals, reviews, manual steps)
* **External system integration** (waiting for webhooks, async APIs)
* **AI agent patterns** (tool execution, human oversight)
This pattern is also known as the **callback** or **task token** pattern.
## Two Approaches
Restate provides two primitives for handling external events:
| Primitive | Use Case | Key Feature |
| -------------------- | -------------------------- | ------------------------------------ |
| **Awakeables** | Services & Virtual Objects | Unique ID-based completion |
| **Durable Promises** | Workflows only | Named promises for simpler signaling |
## How it works
Implementing this pattern in a distributed system is tricky, since you need to ensure that the handler can recover from failures and resume waiting for the external event.
Restate makes promises are durable and distributed. They survive crashes and can be resolved or rejected by any handler in the workflow.
To save costs on FaaS deployments, Restate lets the handler [suspend](/foundations/key-concepts#suspensions-on-faas) while awaiting the promise, and invokes it again when the result is available.
## Awakeables
**Best for:** Services and Virtual Objects where you need to coordinate with external systems.
### Creating and waiting for awakeables
1. **Create an awakeable** - Get a unique ID and promise
2. **Send the ID externally** - Pass the awakeable ID to your external system
3. **Wait for result** - Your handler [suspends](/foundations/key-concepts#suspensions-on-faas) until the external system responds
```go {"CODE_LOAD::go/develop/awakeable.go#here"} theme={null}
awakeable := restate.Awakeable[string](ctx)
awakeableId := awakeable.Id()
if _, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
return requestHumanReview(awakeableId)
}); err != nil {
return err
}
review, err := awakeable.Result()
if err != nil {
return err
}
```
You can resolve an awakeable with any payload that can be serialized. By default,
serialization is done with [`JSONCodec`](https://pkg.go.dev/github.com/restatedev/sdk-go/encoding#PayloadCodec)
which uses `encoding/json`. If you don't need to provide anything, you can
use `restate.Void{}` which serializes to a nil byte slice.
Note that if you wait for an awakeable in an [exclusive handler](/foundations/handlers#handler-behavior) in a Virtual Object, all other calls to this object will be queued.
### Resolving/rejecting Awakeables
External processes complete awakeables in two ways:
* **Resolve** with success data → handler continues normally
* **Reject** with error reason → throws a [terminal error](/develop/go/error-handling) in the waiting handler
#### Via SDK (from other handlers)
**Resolve:**
```go {"CODE_LOAD::go/develop/awakeable.go#resolve"} theme={null}
restate.ResolveAwakeable(ctx, awakeableId, "Looks good!")
```
**Reject:**
```go {"CODE_LOAD::go/develop/awakeable.go#reject"} theme={null}
restate.RejectAwakeable(ctx, awakeableId, fmt.Errorf("Cannot do review"))
```
#### Via HTTP API
External systems can complete awakeables using Restate's HTTP API:
**Resolve with data:**
```shell theme={null}
curl localhost:8080/restate/awakeables/sign_1PePOqp/resolve \
--json '"Looks good!"'
```
**Reject with error:**
```shell theme={null}
curl localhost:8080/restate/awakeables/sign_1PePOqp/reject \
-H 'content-type: text/plain' \
-d 'Review rejected: insufficient documentation'
```
## Durable Promises
**Best for:** Workflows where you need to signal between different workflow handlers.
**Key differences from awakeables:**
* No ID management - use logical names instead
* Scoped to workflow execution lifetime
Use this for:
* Sending data to the run handler
* Have handlers wait for events emitted by the run handler
After a workflow's run handler completes, other handlers can still be called for up to 24 hours (default).
The results of resolved Durable Promises remain available during this time.
Update the retention time via the [service configuration](/services/configuration).
### Creating and waiting for promises
Wait for a promise by name:
```go {"CODE_LOAD::go/develop/durablepromise.go#promise"} theme={null}
review, err := restate.Promise[string](ctx, "review").Result()
```
### Resolving/rejecting promises
Resolve/reject from any workflow handler:
```go {"CODE_LOAD::go/develop/durablepromise.go#resolve_promise"} theme={null}
err := restate.Promise[string](ctx, "review").Resolve(review)
if err != nil {
return err
}
```
### Complete workflow example
```go expandable {"CODE_LOAD::go/develop/durablepromise.go#review"} theme={null}
type ReviewWorkflow struct{}
func (ReviewWorkflow) Run(ctx restate.WorkflowContext, documentId string) (string, error) {
// Send document for review
if _, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return restate.Void{}, askReview(documentId)
}); err != nil {
return "", err
}
// Wait for external review submission
review, err := restate.Promise[string](ctx, "review").Result()
if err != nil {
return "", err
}
// Process the review result
return processReview(documentId, review)
}
func (ReviewWorkflow) SubmitReview(ctx restate.WorkflowSharedContext, review string) error {
// Signal the waiting run handler
return restate.Promise[string](ctx, "review").Resolve(review)
}
```
### Two signaling patterns
**External → Workflow** (shown above): External handlers signal the run handler
* Use for human approvals, external API responses, manual interventions
* External handlers call the handler which resolves the promise
**Workflow → External**: Run handler signals other handlers waiting for workflow events
* Use for step completion notifications, status updates, result broadcasting
* Run handler resolves promises that external handlers are awaiting
## Best Practices
* **Use awakeables** for services/objects coordinating with external systems
* **Use durable promises** for workflow signaling
* **Always handle rejections** to gracefully manage failures
* **Include timeouts** for long-running external processes
# Logging
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/go/logging
Configure the log level of your services.
The Go SDK uses the standard library [`log/slog`](https://pkg.go.dev/log/slog)
package for logging.
By default logs will go to the slog default handler. However, you may provide
any slog handler upon creating the Restate server instance, which allows you to
direct the logs to other log libraries:
```go {"CODE_LOAD::go/develop/logging.go#handler"} theme={null}
myHandler := slog.NewJSONHandler(os.Stdout, nil)
server.NewRestate().
WithLogger(myHandler, true).
Bind(restate.Reflect(Monitoring{})).
Start(context.Background(), "0.0.0.0:9080")
```
**Avoiding duplicate logging**
If you use the default slog logger or another logger, log statements will be
printed over and over again during replays.
To avoid this, you can use the Restate context logger, which is a
`*slog.Logger` that suppresses duplicate log statements during replays:
```go {"CODE_LOAD::go/develop/logging.go#logger"} theme={null}
ctx.Log().Info("This will not be printed again during replays")
ctx.Log().Debug("This will not be printed again during replays")
```
When providing a custom log handler using
[`WithLogger`](https://pkg.go.dev/github.com/restatedev/sdk-go/server#Restate.WithLogger),
you can provide `false` as the second argument, in which case logs will not be
dropped during replay allowing you to handle them as you prefer. You can still
determine whether they would have been dropped using
[`rcontext.LogContextFrom`](https://pkg.go.dev/github.com/restatedev/sdk-go/rcontext#LogContextFrom)
on the context passed to your log handler.
# Service Communication
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/go/service-communication
Call other services from your handler.
Your Restate handler can call other handlers in three ways:
* **[Request-response calls](#request-response-calls)**: Call and wait for a response
* **[One-way messages](#sending-messages)**: Send a message and continue
* **[Delayed messages](#delayed-messages)**: Send a message after a delay
To call a service from an external application, see the [HTTP](/services/invocation/http), [Kafka](/services/invocation/kafka), or [SDK Clients](/services/invocation/clients/typescript-sdk) documentation.
[Learn how Restate how it works](/foundations/key-concepts#resilient-communication)
## Request-response calls
To call a Restate handler and wait for its result:
```go {"CODE_LOAD::go/develop/servicecommunication.go#request_response"} theme={null}
// To call a Service:
svcResponse, err := restate.Service[string](ctx, "MyService", "MyHandler").
Request("Hi")
if err != nil {
return err
}
// To call a Virtual Object:
objResponse, err := restate.Object[string](ctx, "MyObject", "Mary", "MyHandler").
Request("Hi")
if err != nil {
return err
}
// To call a Workflow:
// `run` handler — can only be called once per workflow ID
wfResponse, err := restate.Workflow[bool](ctx, "MyWorkflow", "my-workflow-id", "Run").
Request("Hi")
if err != nil {
return err
}
// Other handlers can be called anytime within workflow retention
status, err := restate.Workflow[restate.Void](ctx, "MyWorkflow", "my-workflow-id", "GetStatus").
Request("Hi again")
if err != nil {
return err
}
```
After a workflow's run handler completes, other handlers can still be called for up to 24 hours (default).
Update this via the [service configuration](/services/configuration).
Request-response calls between [exclusive handlers](/foundations/handlers#handler-behavior) of Virtual Objects may lead to deadlocks:
* Cross deadlock: A → B and B → A (same keys).
* Cycle deadlock: A → B → C → A.
Use the UI or CLI to [cancel](/services/invocation/managing-invocations#cancel) and unblock deadlocked invocations.
## Sending messages
To send a message to another Restate handler without waiting for a response:
```go {"CODE_LOAD::go/develop/servicecommunication.go#one_way"} theme={null}
// To message a Service:
restate.ServiceSend(ctx, "MyService", "MyHandler").Send("Hi")
// To message a Virtual Object:
restate.ObjectSend(ctx, "MyObject", "Mary", "MyHandler").Send("Hi")
// To message a Workflow:
// `run` handler — can only be called once per workflow ID
restate.WorkflowSend(ctx, "MyWorkflow", "my-workflow-id", "Run").
Send("Hi")
// Other handlers can be called anytime within workflow retention
restate.WorkflowSend(ctx, "MyWorkflow", "my-workflow-id", "InteractWithWorkflow").
Send("Hi again")
```
Restate handles message delivery and retries, so the handler can complete and return without waiting for the message to be processed.
Calls to a Virtual Object execute in order of arrival, serially.
Example:
```go {"CODE_LOAD::go/develop/servicecommunication.go#ordering"} theme={null}
restate.ObjectSend(ctx, "MyService", "Mary", "MyHandler").Send("I'm call A")
restate.ObjectSend(ctx, "MyService", "Mary", "MyHandler").Send("I'm call B")
```
Call A is guaranteed to execute before B. However, other invocations may interleave between A and B.
## Delayed messages
To send a message after a delay:
```go {"CODE_LOAD::go/develop/servicecommunication.go#delayed"} theme={null}
// To message a Service with a delay:
restate.ServiceSend(ctx, "MyService", "MyHandler").
Send("Hi", restate.WithDelay(5*time.Hour))
// To message a Virtual Object with a delay:
restate.ObjectSend(ctx, "MyObject", "Mary", "MyHandler").
Send("Hi", restate.WithDelay(5*time.Hour))
// To message a Workflow with a delay:
restate.WorkflowSend(ctx, "MyWorkflow", "my-workflow-id", "Run").
Send("Hi", restate.WithDelay(5*time.Hour))
```
Learn [how this is different](/develop/ts/durable-timers#scheduling-async-tasks) from sleeping and then sending a message.
## Using an idempotency key
To prevent duplicate executions of the same call, add an idempotency key:
```go {"CODE_LOAD::go/develop/servicecommunication.go#idempotency_key"} theme={null}
restate.
ServiceSend(ctx, "MyService", "MyHandler").
// Send attaching idempotency key
Send("Hi", restate.WithIdempotencyKey("my-idempotency-key"))
```
Restate automatically deduplicates calls made during the same handler execution, so there's no need to provide an idempotency key in that case.
However, if multiple handlers might call the same service independently, you can use an idempotency key to ensure deduplication across those calls.
## Attach to an invocation
To wait for or get the result of a previously sent message:
```go {"CODE_LOAD::go/develop/servicecommunication.go#attach"} theme={null}
// Execute the request and retrieve the invocation id
invocationId := restate.
ServiceSend(ctx, "MyService", "MyHandler").
// Optional: send attaching idempotency key
Send("Hi", restate.WithIdempotencyKey("my-idempotency-key")).
GetInvocationId()
// Later re-attach to the request
response, err := restate.AttachInvocation[string](ctx, invocationId).Response()
```
* With an idempotency key: Wait for completion and retrieve the result.
* Without an idempotency key: Can only wait, not retrieve the result.
## Cancel an invocation
To cancel a running handler:
```go {"CODE_LOAD::go/develop/servicecommunication.go#cancel"} theme={null}
// Execute the request and retrieve the invocation id
invocationId := restate.
ServiceSend(ctx, "MyService", "MyHandler").
Send("Hi").
GetInvocationId()
// I don't need this invocation anymore, let me just cancel it
restate.CancelInvocation(ctx, invocationId)
```
## See also
* **[SDK Clients](/develop/go/service-communication)**: Call Restate services from external applications
* **[Error Handling](/develop/go/error-handling)**: Handle failures and terminal errors in service calls
* **[Durable Timers](/develop/go/durable-timers)**: Implement timeouts for your service calls
* **[Sagas](/guides/sagas)**: Roll back or compensate for canceled service calls.
# Services
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/go/services
Implementing Restate services with the Go SDK.
The Restate Go SDK is open source and available on [GitHub](https://github.com/restatedev/sdk-go).
The Restate SDK lets you implement **handlers**. Handlers can be part of a **[Basic Service](/foundations/services#basic-service)**, a **[Virtual Object](/foundations/services#virtual-object)**, or a **[Workflow](/foundations/services#workflow)**. This page shows how to define them with the Go SDK.
## Prerequisites
* Go: >= 1.21.0
## Getting started
Get started quickly with the [Go Quickstart](/quickstart#go).
Add the `github.com/restatedev/sdk-go` dependency to your project to start developing Restate services.
## Basic Services
[Basic Services](/foundations/services) group related **handlers** and expose them as callable endpoints:
```go {"CODE_LOAD::go/develop/myservice/main.go?collapse_prequel"} theme={null}
type MyService struct{}
func (MyService) MyHandler(ctx restate.Context, greeting string) (string, error) {
return fmt.Sprintf("%s!", greeting), nil
}
func main() {
if err := server.NewRestate().
Bind(restate.Reflect(MyService{})).
Start(context.Background(), "0.0.0.0:9080"); err != nil {
log.Fatal(err)
}
}
```
* Define a Service by implementing exported handlers on any struct.
* Handlers have the `Context` as the first argument.
Within the handler, you use the `Context` to interact with Restate.
The SDK stores the actions you do on the context in the Restate journal to make them durable.
* The handler input parameters and return type can be of any type, as long as
they can be serialized. By default, serialization is done with [`JSONCodec`](https://pkg.go.dev/github.com/restatedev/sdk-go/encoding#PayloadCodec)
which uses `encoding/json`. Input types, output types, and even errors are not mandatory
and can be omitted if your handler doesn't need them.
* The service will be reachable under the struct name `MyService`, so the service can be
called at `/MyService/MyHandler`.
* Pass the `MyService` struct to `restate.Reflect` which uses reflection to turn
the methods into handlers. It will skip unexported methods and those that
don't have the expected signature - see the [package documentation](https://pkg.go.dev/github.com/restatedev/sdk-go#Reflect)
for a list of allowed signatures.
* Finally, create a server listening on the specified address and bind the
service(s) to it.
The Go SDK also supports defining handlers and input/output types using
code generation from [Protocol Buffers](https://protobuf.dev/). See
[Code Generation](/develop/go/code-generation) for more details.
## Virtual Objects
[Virtual Objects](/foundations/services) are services that are stateful and key-addressable — each object instance has a unique ID and persistent state.
```go {"CODE_LOAD::go/develop/myvirtualobject/main.go?collapse_prequel"} theme={null}
type MyObject struct{}
func (MyObject) MyHandler(ctx restate.ObjectContext, greeting string) (string, error) {
return fmt.Sprintf("%s %s!", greeting, restate.Key(ctx)), nil
}
func (MyObject) MyConcurrentHandler(ctx restate.ObjectSharedContext, greeting string) (string, error) {
return fmt.Sprintf("%s %s!", greeting, restate.Key(ctx)), nil
}
func main() {
if err := server.NewRestate().
Bind(restate.Reflect(MyObject{})).
Start(context.Background(), "0.0.0.0:9080"); err != nil {
log.Fatal(err)
}
}
```
* Define a Virtual Object by implementing exported handlers on any struct.
* You can retrieve the key of the object you are in via `restate.Key(ctx)`.
* Virtual Objects can have [exclusive and shared handlers](/foundations/handlers#handler-behavior).
- Exclusive handlers receive an `ObjectContext`, allowing read/write access to object state.
- Shared handlers are wrapped in `handlers.object.shared(...)` and use the `ObjectSharedContext`
## Workflows
[Workflows](/foundations/services) are long-lived processes with a defined lifecycle. They run once per key and are ideal for orchestrating multi-step operations, which require external interaction via signals and queries.
```go {"CODE_LOAD::go/develop/myworkflow/main.go?collapse_prequel"} theme={null}
type MyWorkflow struct{}
func (MyWorkflow) Run(ctx restate.WorkflowContext, req string) (string, error) {
// implement the workflow logic here
return "success", nil
}
func (MyWorkflow) InteractWithWorkflow(ctx restate.WorkflowSharedContext) error {
// implement interaction logic here
// e.g. resolve a promise that the workflow is waiting on
return nil
}
func main() {
server := server.NewRestate().
Bind(restate.Reflect(MyWorkflow{}))
if err := server.Start(context.Background(), ":9080"); err != nil {
slog.Error("application exited unexpectedly", "err", err.Error())
os.Exit(1)
}
}
```
* Define a Workflow by implementing exported handlers on any struct.
- Every workflow **must** include a `Run` handler:
* This is the main orchestration entry point
* It runs exactly once per workflow execution and uses the `WorkflowContext`
* Resubmission of the same workflow will fail with "Previously accepted". The invocation ID can be found in the request header `x-restate-id`.
* Use `restate.Key(ctx)` to access the workflow's unique ID
- Additional handlers must use the `WorkflowSharedContext` and can signal or query the workflow. They can run concurrently with the `Run` handler and until the retention time expires.
## Configuring services
Check out the [service configuration docs](/services/configuration) to learn how to configure service behavior, including timeouts and retention policies.
# Serving
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/go/serving
Create an endpoint to serve your services.
The Restate SDK is served as a HTTP handler. You can choose to run this in a
standalone HTTP2 server, or connect the handler into a FaaS system like Lambda.
## Creating a HTTP2 server
1. Create the Restate Server
2. Bind one or multiple services to it.
3. Listen on the specified port for connections and requests.
```go {"CODE_LOAD::go/develop/serving.go#endpoint"} theme={null}
if err := server.NewRestate().
Bind(restate.Reflect(MyService{})).
Bind(restate.Reflect(MyObject{})).
Bind(restate.Reflect(MyWorkflow{})).
Start(context.Background(), ":9080"); err != nil {
log.Fatal(err)
}
```
If you need to customize the HTTP2 server, or serve over HTTP1.1
you can call `.Handler()` instead of `Start()`, and then use the
handler as normal. To discover services over HTTP1.1 you must
provide the `--use-http1.1` CLI flag.
```go {"CODE_LOAD::go/develop/serving.go#custom_endpoint"} theme={null}
handler, err := server.NewRestate().
Bind(restate.Reflect(MyService{})).
Bind(restate.Reflect(MyObject{})).
Bind(restate.Reflect(MyWorkflow{})).
Handler()
if err != nil {
log.Fatal(err)
}
```
By default, this handler will advertise itself as working
bidirectionally; the SDK will try to get completions from the runtime
during execution.
However, you can use the method `.Bidirectional(false)` on the endpoint
builder to change this on platforms that do not support bidirectional
communication, such as Lambda. If you don't do this your handler may get
stuck.
## Creating a Lambda handler
To register your service as a Lambda function change the endpoint into a Lambda handler
with `.LambdaHandler()`, and pass this handler to [`lambda.Start`](https://pkg.go.dev/github.com/aws/aws-lambda-go/lambda#Start).
```go {"CODE_LOAD::go/develop/servinglambda.go#lambda"} theme={null}
handler, err := server.NewRestate().
Bind(restate.Reflect(MyService{})).
Bind(restate.Reflect(MyObject{})).
Bind(restate.Reflect(MyWorkflow{})).
Bidirectional(false).
LambdaHandler()
if err != nil {
log.Fatal(err)
}
lambda.Start(handler)
```
Have a look at the [deployment section](/services/deploy/lambda) for guidance on how to deploy your services on AWS Lambda.
The implementation of your services and handlers remains the same for both deployment options.
## Validating request identity
SDKs can validate that incoming requests come from a particular Restate
instance. You can find out more about request identity in the [Security docs](/services/security#locking-down-service-access)
```go {"CODE_LOAD::go/develop/serving.go#identity"} theme={null}
if err := server.NewRestate().
Bind(restate.Reflect(MyService{})).
WithIdentityV1("publickeyv1_w7YHemBctH5Ck2nQRQ47iBBqhNHy4FV7t2Usbye2A6f").
Start(context.Background(), ":9080"); err != nil {
log.Fatal(err)
}
```
# State
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/go/state
Store key-value state in Restate.
Restate lets you persist key-value (K/V) state using its embedded K/V store.
## Key characteristics
[Learn about Restate's embedded K/V store](/foundations/key-concepts#consistent-state).
**State is only available for Virtual Objects and Workflows.**
Scope & retention:
* For Virtual Objects: State is scoped per object key and retained indefinitely. It is persisted and shared across all invocations for that object until explicitly cleared.
* For Workflows: State is scoped per workflow execution (workflow ID) and retained only for the duration of the workflow’s configured retention time.
Access Rules:
* [Exclusive handlers](/foundations/handlers#handler-behavior) (e.g., `run()` in workflows) can read and write state.
* [Shared handlers](/foundations/handlers#handler-behavior) can only read state and cannot mutate it.
You can inspect and edit the K/V state via the UI and the [CLI](/services/introspection#inspecting-application-state).
## List all state keys
To retrieve all keys for which the current Virtual Object has stored state:
```go {"CODE_LOAD::go/develop/state.go#statekeys"} theme={null}
stateKeys, err := restate.Keys(ctx)
if err != nil {
return err
}
```
## Get state value
To read a value by key:
```go {"CODE_LOAD::go/develop/state.go#get"} theme={null}
myString := "my-default"
if s, err := restate.Get[*string](ctx, "my-string-key"); err != nil {
return err
} else if s != nil {
myString = *s
}
myNumber, err := restate.Get[int](ctx, "my-number-key")
if err != nil {
return err
}
```
Returns null if the key doesn't exist.
## Set state value
To write or update a value:
```go {"CODE_LOAD::go/develop/state.go#set"} theme={null}
restate.Set(ctx, "my-key", "my-new-value")
```
## Clear state key
To delete a specific key:
```go {"CODE_LOAD::go/develop/state.go#clear"} theme={null}
restate.Clear(ctx, "my-key")
```
## Clear all state keys
To remove all stored state for the current Virtual Object:
```go {"CODE_LOAD::go/develop/state.go#clear_all"} theme={null}
restate.ClearAll(ctx)
```
## Advanced: Eager vs. lazy state loading
Restate supports two modes for loading state in handlers:
### Eager state (default)
* **How it works**: State is automatically sent with the request when invoking a handler
* **Benefits**: State is available immediately when the handler starts executing
* **Behavior**: All reads and writes to state are local to the handler execution
* **Best for**: Small to medium state objects that are frequently accessed
### Lazy state
* **How it works**: State is fetched on-demand on `get` calls from the Restate Server
* **Benefits**: Reduces initial request size and memory usage
* **Setup**: Enable lazy state in the [service or handler configuration](/services/configuration)
* **Best for**: Large state objects that aren't needed in every handler execution
# Concurrent Tasks
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/java/concurrent-tasks
Execute multiple tasks concurrently and gather results.
When building resilient applications, you often need to perform multiple operations in parallel to improve performance and user experience. Restate provides durable concurrency primitives that allow you to run tasks concurrently while maintaining deterministic execution during replays.
## When to use concurrent tasks
Use concurrent tasks when you need to:
* Call multiple external services simultaneously (e.g., fetching data from different APIs)
* Race multiple operations and use the first result (e.g., trying multiple LLM providers)
* Implement timeouts by racing an operation against a timer
* Perform batch operations where individual tasks can run in parallel
## Key benefits
* **Deterministic replay**: Restate logs the order of completion, ensuring consistent behavior during failures
* **Fault tolerance**: If your handler fails, tasks that were already completed will be replayed with their results, while pending tasks will be retried
## Parallelizing tasks
Start multiple durable operations concurrently by calling them without immediately awaiting:
```java Java {"CODE_LOAD::java/src/main/java/develop/JournalingResults.java#parallel"} theme={null}
// Start operations concurrently using DurableFuture
var call1 = ctx.runAsync(UserData.class, () -> fetchUserData(123));
var call2 = ctx.runAsync(OrderHistory.class, () -> fetchOrderHistory(123));
var call3 = AnalyticsServiceClient.fromContext(ctx).calculateMetric(123);
// Now wait for results as needed
UserData user = call1.await();
OrderHistory orders = call2.await();
Integer metric = call3.await();
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/JournalingResults.kt#parallel"} theme={null}
val call1 = ctx.runAsync { fetchUserData(123) }
val call2 = ctx.runAsync { fetchOrderHistory(123) }
val call3 = AnalyticsServiceClient.fromContext(ctx).calculateMetric(123)
// Now wait for results as needed
val user: UserData = call1.await()
val orders: OrderHistory = call2.await()
val metric: Int = call3.await()
```
Check out the guide on [parallelizing work](guides/parallelizing-work).
## Retrieving results
Restate provides several patterns for coordinating concurrent tasks. All patterns use `DurableFuture` combinators that log the order of completion, ensuring deterministic behavior during replays.
### Wait for the first completion
**Select** creates a `DurableFuture` that returns on the first completed `DurableFuture` of the provided ones.
The semantics are similar to [`CompleteableFuture.anyOf()`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/CompletableFuture.html#anyOf\(java.util.concurrent.CompletableFuture...\)), but the outcome is stored in the Restate journal to be deterministically replayable.
```java Java {"CODE_LOAD::java/src/main/java/develop/JournalingResults.java#combine_any"} theme={null}
boolean res = Select.select().or(a1).or(a2).or(a3).await();
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/JournalingResults.kt#combine_any"} theme={null}
val resSelect =
select {
a1.onAwait { it }
a2.onAwait { it }
a3.onAwait { it }
}
.await()
```
### Wait for all tasks to complete
**Await all** creates a `DurableFuture` that awaits for all the provided DurableFutures to resolve.
The semantics are similar to [`CompleteableFuture.allOf()`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/CompletableFuture.html#allOf\(java.util.concurrent.CompletableFuture...\)), but the outcome is stored in the Restate journal to be deterministically replayable.
```java Java {"CODE_LOAD::java/src/main/java/develop/JournalingResults.java#combine_all"} theme={null}
DurableFuture.all(a1, a2, a3).await();
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/JournalingResults.kt#combine_all"} theme={null}
listOf(a1, a2, a3).awaitAll()
```
# Durable Steps
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/java/durable-steps
Persist results of operations.
Restate uses an execution log to replay operations after failures and suspensions. Non-deterministic operations (database calls, HTTP requests, UUID generation) must be wrapped to ensure deterministic replay.
## Run
Use `ctx.run` to safely wrap any non-deterministic operation, like HTTP calls or database responses, and have Restate store its result in the execution log.
```java Java {"CODE_LOAD::java/src/main/java/develop/JournalingResults.java#side_effect"} theme={null}
String output = ctx.run(String.class, () -> doDbRequest());
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/JournalingResults.kt#side_effect"} theme={null}
val output = ctx.runBlock { doDbRequest() }
```
Note that inside `ctx.run`, you cannot use the Restate context (e.g., `ctx.get`, `ctx.sleep`, or nested `ctx.run`).
To run an action asynchronously, use `ctx.runAsync`:
```java Java {"CODE_LOAD::java/src/main/java/develop/JournalingResults.java#async_side_effect"} theme={null}
DurableFuture myRunFuture = ctx.runAsync(String.class, () -> doSomethingSlow());
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/JournalingResults.kt#async_side_effect"} theme={null}
val myRunFuture = ctx.runAsync { doSomethingSlow() }
```
You can use this to combine your run actions with other asynchronous operations, like waiting for a timer or an awakeable.
Check out [Concurrent tasks](/develop/java/concurrent-tasks).
Have a look at the [serialization docs](/develop/java/serialization) to learn how to serialize and deserialize your objects.
Failures in `ctx.run` are treated the same as any other handler error. Restate will retry it unless configured otherwise or unless a [`TerminalException`](/develop/java/error-handling) is thrown.
You can customize how `ctx.run` retries via:
```java Java {"CODE_LOAD::java/src/main/java/develop/RetryRunService.java#here"} theme={null}
try {
RetryPolicy myRunRetryPolicy =
RetryPolicy.defaultPolicy()
.setInitialDelay(Duration.ofMillis(500))
.setExponentiationFactor(2)
.setMaxDelay(Duration.ofSeconds(10))
.setMaxAttempts(10)
.setMaxDuration(Duration.ofMinutes(5));
ctx.run("my-run", myRunRetryPolicy, () -> writeToOtherSystem());
} catch (TerminalException e) {
// Handle the terminal error after retries exhausted
// For example, undo previous actions (see sagas guide) and
// propagate the error back to the caller
}
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/RetryRunService.kt#here"} theme={null}
try {
val myRunRetryPolicy =
RetryPolicy(
initialDelay = 5.seconds,
exponentiationFactor = 2.0f,
maxDelay = 60.seconds,
maxAttempts = 10,
maxDuration = 5.minutes)
ctx.runBlock("write", myRunRetryPolicy) { writeToOtherSystem() }
} catch (e: TerminalException) {
// Handle the terminal error after retries exhausted
// For example, undo previous actions (see sagas guide) and
// propagate the error back to the caller
throw e
}
```
* You can limit retries by time or count
* When the policy is exhausted, a `TerminalException` is thrown
* See the [Error Handling Guide](/guides/error-handling) and the [Sagas Guide](/guides/sagas) for patterns like compensation
If Restate doesn't receive new journal entries from a service for more than one minute (by default), it will automatically abort the invocation and retry it.
However, some business logic can take longer to complete—for example, an LLM call that takes up to 3 minutes to respond.
In such cases, you can adjust the service’s [abort timeout and inactivity timeout](/services/configuration) accommodate longer execution times.
For more information, see the [error handling guide](/guides/error-handling).
## Deterministic randoms
The SDK provides deterministic helpers for random values — seeded by the invocation ID — so they return the **same result on retries**.
### UUIDs
To generate stable UUIDs for things like idempotency keys:
```java Java {"CODE_LOAD::java/src/main/java/develop/JournalingResults.java#uuid"} theme={null}
UUID uuid = ctx.random().nextUUID();
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/JournalingResults.kt#uuid"} theme={null}
val uuid = ctx.random().nextUUID()
```
Do not use this in cryptographic contexts.
### Random numbers
To generate a deterministic float between `0` and `1`:
```java Java {"CODE_LOAD::java/src/main/java/develop/JournalingResults.java#random_nb"} theme={null}
int value = ctx.random().nextInt();
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/JournalingResults.kt#random_nb"} theme={null}
val value = ctx.random().nextInt()
```
This behaves like the standard library `Random` class but is deterministically replayable.
You can use any of the methods of [`java.util.Random`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Random.html) to generate random numbers: for example, `nextBoolean()`, `nextLong()`, `nextFloat()`, etc.
# Scheduling & Timers
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/java/durable-timers
Durable timers, scheduled actions, and sleep, backed by Restate.
Restate provides durable, fault-tolerant timers that allow you to:
* [Sleep](#durable-sleep): Pause a handler for a given duration
* [Send delayed messages](#scheduling-async-tasks): Schedule handler invocations in the future
* [Set timeouts](#timers-and-timeouts) for async operations
* Implement patterns like [cron jobs](#cron-jobs)
## How it works
Restate tracks and manages timers, ensuring they survive failures and restarts.
For example, if a service is sleeping for 12 hours and fails after 8 hours, then Restate will make sure it only sleeps 4 hours more.
If your handler runs on function-as-a-service platforms like AWS Lambda, Restate suspends the handler while it is sleeping, to free up resources.
Since these platforms charge on execution time, this saves costs.
## Durable sleep
To pause a handler for a set duration:
```java Java {"CODE_LOAD::java/src/main/java/develop/DurableTimers.java#sleep"} theme={null}
ctx.sleep(Duration.ofSeconds(10));
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/DurableTimers.kt#sleep"} theme={null}
ctx.sleep(10.seconds)
```
There is no hard limit on how long you can sleep. You can sleep for months or even years, but keep in mind:
* If you sleep in an [exclusive handler](/foundations/handlers#handler-behavior) in a Virtual Object, all other calls to this object will be queued.
* You need to keep the deployment version until the invocation completes (see [versioning](/services/versioning)).
So for long sleeps, we recommend breaking this up into multiple handlers that call each other with [delayed messages](/develop/java/service-communication#delayed-messages).
The Restate SDK calculates the wake-up time based on the delay you specify.
The Restate Server then uses this calculated time to wake up the handler.
If the Restate Server and the SDK have different system clocks, the sleep duration might not be accurate.
So make sure that the system clock of the Restate Server and the SDK have the same timezone and are synchronized.
A mismatch can cause timers to fire earlier or later than expected.
## Scheduling async tasks
To invoke a handler at a later time, use [delayed messages](/develop/java/service-communication#delayed-messages).
In theory, you can schedule future work in Restate in two ways:
1. **Delayed messages** (recommended)
2. **Sleep + send** - sleeping in the current handler, then sending a message
At first sight, both approaches might seem to achieve similar results.
However, we recommend to use delayed messages for the following reasons:
* **Handler completes immediately**: The calling handler can finish execution and complete the invocation without waiting for the delay to finish
* **No Virtual Object blocking**: Other requests to the same Virtual Object can be processed during the delay period
* **Better for service versioning**: No long-running invocations that require keeping old service deployments around for a long time (see [service versioning](/services/versioning))
## Timers and timeouts
Most context actions are async actions that return a `DurableFuture`.
Restate's `DurableFuture` supports setting timeouts, allowing you to bound how long your code waits for an operation.
When an operation times out, it throws a Restate `TimeoutException`.
```java Java {"CODE_LOAD::java/src/main/java/develop/DurableTimers.java#timer"} theme={null}
try {
MyServiceClient.fromContext(ctx).myHandler("Hi!").await(Duration.ofSeconds(5));
} catch (TimeoutException e) {
// Handle the timeout error
}
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/DurableTimers.kt#timer"} theme={null}
try {
MyServiceClient.fromContext(ctx).myHandler("Hi!").await(5.seconds)
} catch (e: TimeoutException) {
// Handle the timeout
}
```
For alternative ways of racing async operations, have a look at the [Durable Future combinators docs](/develop/java/concurrent-tasks).
## Cron jobs
Restate does not yet include native cron support, but you can implement your own cron scheduler using:
* Durable timers
* Virtual Objects
* A repeat loop or sleep-schedule pattern
Check out the guide on [implementing cron jobs](/guides/cron).
# Error Handling
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/java/error-handling
Stop infinite retries with Terminal Errors.
Restate handles retries for failed invocations.
Check out the [Error Handling guide](/guides/error-handling) to learn more about how Restate handles transient errors, terminal errors, retries, and timeouts.
## Retry strategies
By default, Restate does infinite retries with an exponential backoff strategy.
Check out the [error handling guide](/guides/error-handling) to learn how to customize this.
## Terminal Errors
For failures for which you do not want retries, but instead want the invocation to end and the error message
to be propagated back to the caller, you can throw a **terminal error**.
You can throw a `TerminalException` with an optional HTTP status code and a message anywhere in your handler, as follows:
```java Java {"CODE_LOAD::java/src/main/java/develop/ErrorHandling.java#here"} theme={null}
throw new TerminalException(500, "Something went wrong");
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/ErrorHandling.kt#here"} theme={null}
throw TerminalException(500, "Something went wrong")
```
You can catch terminal errors, and build your control flow around it.
When throwing a terminal error, undo earlier handler actions to keep the system consistent. See our [sagas guide](/guides/sagas) for details.
# External Events
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/java/external-events
Handle external events and human-in-the-loop patterns with durable waiting primitives.
Sometimes your handlers need to pause and wait for external processes to complete. This is common in:
* **Human-in-the-loop workflows** (approvals, reviews, manual steps)
* **External system integration** (waiting for webhooks, async APIs)
* **AI agent patterns** (tool execution, human oversight)
This pattern is also known as the **callback** or **task token** pattern.
## Two Approaches
Restate provides two primitives for handling external events:
| Primitive | Use Case | Key Feature |
| -------------------- | -------------------------- | ------------------------------------ |
| **Awakeables** | Services & Virtual Objects | Unique ID-based completion |
| **Durable Promises** | Workflows only | Named promises for simpler signaling |
## How it works
Implementing this pattern in a distributed system is tricky, since you need to ensure that the handler can recover from failures and resume waiting for the external event.
Restate makes promises are durable and distributed. They survive crashes and can be resolved or rejected by any handler in the workflow.
To save costs on FaaS deployments, Restate lets the handler [suspend](/foundations/key-concepts#suspensions-on-faas) while awaiting the promise, and invokes it again when the result is available.
## Awakeables
**Best for:** Services and Virtual Objects where you need to coordinate with external systems.
### Creating and waiting for awakeables
1. **Create an awakeable** - Get a unique ID and Future
2. **Send the ID externally** - Pass the awakeable ID to your external system
3. **Wait for result** - Your handler [suspends](/foundations/key-concepts#suspensions-on-faas) until the external system responds
```java Java {"CODE_LOAD::java/src/main/java/develop/Awakeables.java#here"} theme={null}
// Create awakeable and get unique ID
Awakeable awakeable = ctx.awakeable(String.class);
String awakeableId = awakeable.id();
// Send ID to external system (email, queue, webhook, etc.)
ctx.run(() -> requestHumanReview(name, awakeableId));
// Handler suspends here until external completion
String review = awakeable.await();
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/Awakeables.kt#here"} theme={null}
// Create awakeable and get unique ID
val awakeable = ctx.awakeable()
val awakeableId = awakeable.id
// Send ID to external system (email, queue, webhook, etc.)
ctx.runBlock { requestHumanReview(awakeableId) }
// Handler suspends here until external completion
val review = awakeable.await()
```
To customize serialization, visit the [docs](/develop/java/serialization).
Note that if you wait for an awakeable in an [exclusive handler](/foundations/handlers#handler-behavior) in a Virtual Object, all other calls to this object will be queued.
### Resolving/rejecting Awakeables
External processes complete awakeables in two ways:
* **Resolve** with success data → handler continues normally
* **Reject** with error reason → throws a [terminal error](/develop/java/error-handling) in the waiting handler
#### Via SDK (from other handlers)
**Resolve:**
```java Java {"CODE_LOAD::java/src/main/java/develop/Awakeables.java#resolve"} theme={null}
// Complete with success data
ctx.awakeableHandle(awakeableId).resolve(String.class, "Looks good!");
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/Awakeables.kt#resolve"} theme={null}
// Complete with success data
ctx.awakeableHandle(awakeableId).resolve("Looks good!")
```
**Reject:**
```java Java {"CODE_LOAD::java/src/main/java/develop/Awakeables.java#reject"} theme={null}
// Complete with error
ctx.awakeableHandle(awakeableId).reject("This cannot be reviewed.");
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/Awakeables.kt#reject"} theme={null}
// Complete with error
ctx.awakeableHandle(awakeableId).reject("This cannot be reviewed.")
```
#### Via HTTP API
External systems can complete awakeables using Restate's HTTP API:
**Resolve with data:**
```shell theme={null}
curl localhost:8080/restate/awakeables/sign_1PePOqp/resolve \
--json '"Looks good!"'
```
**Reject with error:**
```shell theme={null}
curl localhost:8080/restate/awakeables/sign_1PePOqp/reject \
-H 'content-type: text/plain' \
-d 'Review rejected: insufficient documentation'
```
## Durable Promises
**Best for:** Workflows where you need to signal between different workflow handlers.
**Key differences from awakeables:**
* No ID management - use logical names instead
* Scoped to workflow execution lifetime
Use this for:
* Sending data to the run handler
* Have handlers wait for events emitted by the run handler
After a workflow's run handler completes, other handlers can still be called for up to 24 hours (default).
The results of resolved Durable Promises remain available during this time.
Update the retention time via the [service configuration](/services/configuration).
### Creating and waiting for promises
Create a promise key:
```java Java {"CODE_LOAD::java/src/main/java/develop/ReviewWorkflow.java#promise_key"} theme={null}
private static final DurablePromiseKey REVIEW_PROMISE =
DurablePromiseKey.of("review", String.class);
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/ReviewWorkflow.kt#promise_key"} theme={null}
val REVIEW_PROMISE = durablePromiseKey("review")
```
Wait for a promise by name:
```java Java {"CODE_LOAD::java/src/main/java/develop/ReviewWorkflow.java#promise"} theme={null}
String review = ctx.promise(REVIEW_PROMISE).future().await();
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/ReviewWorkflow.kt#promise"} theme={null}
val review: String = ctx.promise(REVIEW_PROMISE).future().await()
```
Your workflow can [suspend](/foundations/key-concepts#suspensions-on-faas) until the promise is resolved or rejected.
### Resolving/rejecting promises
Resolve/reject from any workflow handler:
```java Java {"CODE_LOAD::java/src/main/java/develop/ReviewWorkflow.java#resolve_promise"} theme={null}
ctx.promiseHandle(REVIEW_PROMISE).resolve(review);
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/ReviewWorkflow.kt#resolve_promise"} theme={null}
ctx.promiseHandle(REVIEW_PROMISE).resolve(review)
```
### Complete workflow example
```java Java expandable {"CODE_LOAD::java/src/main/java/develop/ReviewWorkflow.java#here"} theme={null}
@Workflow
public class ReviewWorkflow {
private static final DurablePromiseKey REVIEW_PROMISE =
DurablePromiseKey.of("review", String.class);
@Workflow
public String run(WorkflowContext ctx, String documentId) {
// Send document for review
ctx.run(() -> askReview(documentId));
// Wait for external review submission
String review = ctx.promise(REVIEW_PROMISE).future().await();
// Process the review result
return processReview(documentId, review);
}
@Shared
public void submitReview(SharedWorkflowContext ctx, String review) {
// Signal the waiting run handler
ctx.promiseHandle(REVIEW_PROMISE).resolve(review);
}
}
```
```kotlin Kotlin expandable {"CODE_LOAD::kotlin/src/main/kotlin/develop/ReviewWorkflow.kt#here"} theme={null}
@Workflow
class ReviewWorkflow {
companion object {
val REVIEW_PROMISE = durablePromiseKey("review")
}
@Workflow
suspend fun run(ctx: WorkflowContext, documentId: String): String {
// Send document for review
ctx.runBlock { askReview(documentId) }
// Wait for external review submission
val review: String = ctx.promise(REVIEW_PROMISE).future().await()
// Process the review result
return processReview(documentId, review)
}
@Shared
suspend fun submitReview(ctx: SharedWorkflowContext, review: String) {
// Signal the waiting run handler
ctx.promiseHandle(REVIEW_PROMISE).resolve(review)
}
}
```
### Two signaling patterns
**External → Workflow** (shown above): External handlers signal the run handler
* Use for human approvals, external API responses, manual interventions
* External handlers call the handler which resolves the promise
**Workflow → External**: Run handler signals other handlers waiting for workflow events
* Use for step completion notifications, status updates, result broadcasting
* Run handler resolves promises that external handlers are awaiting
## Best Practices
* **Use awakeables** for services/objects coordinating with external systems
* **Use durable promises** for workflow signaling
* **Always handle rejections** to gracefully manage failures
* **Include timeouts** for long-running external processes
# Logging
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/java/logging
Configure the log level of your services.
The Java SDK uses [log4j2](https://logging.apache.org/log4j/2.x/manual/configuration.html) as logging facade.
To configure the logging, add the file `resources/log4j2.properties`:
```properties theme={null}
# Set to debug or trace if log4j initialization is failing
status = warn
# Console appender configuration
appender.console.type = Console
appender.console.name = consoleLogger
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %notEmpty{[%X{restateInvocationTarget}]}%notEmpty{[%X{restateInvocationId}]} %c - %m%n
# Filter out logging during replay
appender.console.filter.replay.type = ContextMapFilter
appender.console.filter.replay.onMatch = DENY
appender.console.filter.replay.onMismatch = NEUTRAL
appender.console.filter.replay.0.type = KeyValuePair
appender.console.filter.replay.0.key = restateInvocationStatus
appender.console.filter.replay.0.value = REPLAYING
# Restate logs to debug level
logger.app.name = dev.restate
logger.app.level = info
logger.app.additivity = false
logger.app.appenderRef.console.ref = consoleLogger
# Root logger
rootLogger.level = info
rootLogger.appenderRef.stdout.ref = consoleLogger
```
If you want to do filtering of the logs, you can use `log4j2` filters.
Logging on the `INFO` level is enough for most use cases, but you can set the log level of
the `dev.restate` classes to `DEBUG` and `TRACE` if you want more info about the internal SDK operations.
The SDK injects the following additional metadata to the logging context that can be used for filtering as well:
* `restateInvocationTarget`: Invocation Target, e.g. `counter.Counter/Add`.
* `restateInvocationId`: [Invocation ID](/services/invocation/http#invocation-identifier).
* `restateInvocationStatus`: Invocation status, can be `WAITING_START`, `REPLAYING`, `PROCESSING`, `CLOSED`.
# Serialization
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/java/serialization
Customize serialization for SDK actions.
Restate sends data over the network for storing state, journaling actions, awakeables, etc.
Therefore, Restate needs to serialize and deserialize the journal entries.
## Default serialization
The SDK uses by default [Jackson Databind](https://github.com/FasterXML/jackson) for the Java API, and [Kotlinx serialization](https://kotlinlang.org/docs/serialization.html) for the Kotlin API.
For example, for state keys:
```java Java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#state_keys"} theme={null}
// Primitive types
var myString = StateKey.of("myString", String.class);
// Generic types need TypeRef (similar to Jackson's TypeReference)
var myMap = StateKey.of("myMap", TypeTag.of(new TypeRef
The same applies to journaling actions, awakeables, etc.
## Customizing serialization
There are different entrypoints in the SDK to customize (de)serialization, depending on your needs.
### Custom Jackson `ObjectMapper`
You can customize the Jackson's [`ObjectMapper`](https://javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/latest/com/fasterxml/jackson/databind/ObjectMapper.html) by subclassing the [`JacksonSerdeFactory`](https://restatedev.github.io/sdk-java/javadocs/dev/restate/sdk/serde/jackson/JacksonSerdeFactory) class:
```java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#custom_jackson"} theme={null}
class MyJacksonSerdeFactory extends JacksonSerdeFactory {
public MyJacksonSerdeFactory() {
super(new ObjectMapper().configure(SerializationFeature.INDENT_OUTPUT, true));
}
}
```
And annotating your services with [`@CustomSerdeFactory`](https://restatedev.github.io/sdk-java/javadocs/dev/restate/sdk/annotation/CustomSerdeFactory):
```java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#custom_jackson_service"} theme={null}
@CustomSerdeFactory(MyJacksonSerdeFactory.class)
@Service
class ServiceWithCustomJacksonObjectMapper {
@Handler
public String greet(Context context) {
return "Hello world";
}
}
```
### Custom Kotlinx Serialization `Json`
You can customize the Kotlinx Serialization [`Json`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json/) by subclassing the [`KotlinSerializationSerdeFactory`](https://restatedev.github.io/sdk-java/ktdocs/sdk-api-kotlin/dev.restate.sdk.kotlin.serialization/-kotlin-serialization-serde-factory/) class:
```kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/CustomSerializationExample.kt#custom"} theme={null}
class MyJsonSerdeFactory : KotlinSerializationSerdeFactory(json = Json { prettyPrint = true })
```
And annotating your services with [`@CustomSerdeFactory`](https://restatedev.github.io/sdk-java/ktdocs/sdk-common/dev.restate.sdk.annotation/-custom-serde-factory/):
```kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/CustomSerializationExample.kt#custom_service"} theme={null}
@CustomSerdeFactory(MyJsonSerdeFactory::class)
@Service
class ServiceWithCustomSerdeFactory {
@Handler suspend fun greet(ctx: Context) = "Hello world!"
}
```
### Use custom serializer for a specific operation
If you need for a specific operation (e.g. for `Context.run` or `Context.Awakeable`) a custom (de)serializer, you can implement one using the interface [`Serde`](https://restatedev.github.io/sdk-java/javadocs/dev/restate/serde/Serde):
```java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#customserdes"} theme={null}
class MyPersonSerde implements Serde {
@Override
public Slice serialize(Person person) {
// convert value to a byte array, then wrap in a Slice
return Slice.wrap(person.toBytes());
}
@Override
public Person deserialize(Slice slice) {
// convert value to Person
return Person.fromBytes(slice.toByteArray());
}
}
```
And then use it, for example, in combination with `ctx.run`:
```java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#use_person_serde"} theme={null}
ctx.run(new MyPersonSerde(), () -> new Person());
```
### Use another serialization library
If you want to use a different serialization library throughout your service, we suggest implementing [`SerdeFactory`](https://restatedev.github.io/sdk-java/javadocs/dev/restate/serde/SerdeFactory) and annotating the service class with [`@CustomSerdeFactory`](https://restatedev.github.io/sdk-java/javadocs/dev/restate/sdk/annotation/CustomSerdeFactory). Refer to the Javadocs for more details.
# Service Communication
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/java/service-communication
Call other services from your handler.
Your Restate handler can call other handlers in three ways:
* **[Request-response calls](#request-response-calls)**: Call and wait for a response
* **[One-way messages](#sending-messages)**: Send a message and continue
* **[Delayed messages](#delayed-messages)**: Send a message after a delay
To call a service from an external application, see the [HTTP](/services/invocation/http), [Kafka](/services/invocation/kafka), or [SDK Clients](/services/invocation/clients/java-sdk) documentation.
[Why use Restate for service communication?](/foundations/key-concepts#resilient-communication)
## Generating service clients
The Restate Java SDK automatically generates clients for each of your services when you build the project.
If you don't see the generated clients, make sure you [added the code generator](develop/java/overview#getting-started) and have built the project with `./gradlew build` or `mvn compile exec:java`.
## Request-response calls
To call a Restate handler, use the generated clients and wait for its result:
```java Java {"CODE_LOAD::java/src/main/java/develop/ServiceCommunication.java#request_response"} theme={null}
// To call a Service:
String svcResponse = MyServiceClient.fromContext(ctx).myHandler(request).await();
// To call a Virtual Object:
String objResponse = MyObjectClient.fromContext(ctx, objectKey).myHandler(request).await();
// To call a Workflow:
// `run` handler — can only be called once per workflow ID
String wfResponse = MyWorkflowClient.fromContext(ctx, workflowId).run(request).await();
// Other handlers can be called anytime within workflow retention
String status =
MyWorkflowClient.fromContext(ctx, workflowId).interactWithWorkflow(request).await();
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/ServiceCommunication.kt#request_response"} theme={null}
// To call a Service:
val svcResponse = MyServiceClient.fromContext(ctx).myHandler(request).await()
// To call a Virtual Object:
val objResponse = MyObjectClient.fromContext(ctx, objectKey).myHandler(request).await()
// To call a Workflow:
// `run` handler — can only be called once per workflow ID
val wfResponse = MyWorkflowClient.fromContext(ctx, workflowId).run(request).await()
// Other handlers can be called anytime within workflow retention
MyWorkflowClient.fromContext(ctx, workflowId).interactWithWorkflow(request).await()
```
Use the generic clients when you don't have access to typed clients or need dynamic service names:
```java Java {"CODE_LOAD::java/src/main/java/develop/ServiceCommunication.java#request_response_generic"} theme={null}
Target target = Target.service("MyService", "myHandler"); // or virtualObject or workflow
String response =
ctx.call(Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request))
.await();
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/ServiceCommunication.kt#request_response_generic"} theme={null}
val target = Target.service("MyService", "myHandler")
val response =
ctx.call(Request.of(target, typeTag(), typeTag(), request)).await()
```
After a workflow's run handler completes, other handlers can still be called for up to 24 hours (default).
Update this via the [service configuration](/services/configuration).
Request-response calls between [exclusive handlers](/foundations/handlers#handler-behavior) of Virtual Objects may lead to deadlocks:
* Cross deadlock: A → B and B → A (same keys).
* Cycle deadlock: A → B → C → A.
Use the UI or CLI to [cancel](/services/invocation/managing-invocations#cancel) and unblock deadlocked invocations.
## Sending messages
To send a message to another Restate handler without waiting for a response:
```java Java {"CODE_LOAD::java/src/main/java/develop/ServiceCommunication.java#one_way"} theme={null}
MyServiceClient.fromContext(ctx).send().myHandler(request);
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/ServiceCommunication.kt#one_way"} theme={null}
MyServiceClient.fromContext(ctx).send().myHandler(request)
```
Restate handles message delivery and retries, so the handler can complete and return without waiting for the message to be processed.
Use generic clients when you don't have the service definition:
```java Java {"CODE_LOAD::java/src/main/java/develop/ServiceCommunication.java#one_way_generic"} theme={null}
Target target = Target.service("MyService", "myHandler"); // or virtualObject or workflow
ctx.send(Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request));
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/ServiceCommunication.kt#one_way_generic"} theme={null}
val target = Target.service("MyService", "myHandler")
ctx.send(Request.of(target, typeTag(), typeTag(), request))
```
Calls to a Virtual Object execute in order of arrival, serially.
Example:
```java {"CODE_LOAD::java/src/main/java/develop/ServiceCommunication.java#ordering"} theme={null}
MyObjectClient.fromContext(ctx, objectKey).send().myHandler("I'm call A");
MyObjectClient.fromContext(ctx, objectKey).send().myHandler("I'm call B");
```
Call A is guaranteed to execute before B. However, other invocations may interleave between A and B.
## Delayed messages
To send a message after a delay, use the generated clients with `.send()` and the `Duration` as second parameter:
```java Java {"CODE_LOAD::java/src/main/java/develop/ServiceCommunication.java#delayed"} theme={null}
MyServiceClient.fromContext(ctx).send().myHandler(request, Duration.ofDays(5));
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/ServiceCommunication.kt#delayed"} theme={null}
MyServiceClient.fromContext(ctx).send().myHandler(request, 5.days)
```
Or with the generic clients:
```java Java {"CODE_LOAD::java/src/main/java/develop/ServiceCommunication.java#delayed_generic"} theme={null}
Target target = Target.service("MyService", "myHandler"); // or virtualObject or workflow
ctx.send(
Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request),
Duration.ofDays(5));
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/ServiceCommunication.kt#delayed_generic"} theme={null}
val target = Target.service("MyService", "myHandler")
Request.of(target, typeTag(), typeTag(), request).send(ctx, 5.days)
```
Learn [how this is different](/develop/java/durable-timers#scheduling-async-tasks) from sleeping and then sending a message.
## Using an idempotency key
To prevent duplicate executions of the same call, add an idempotency key:
```java Java {"CODE_LOAD::java/src/main/java/develop/ServiceCommunication.java#idempotency_key"} theme={null}
// For a request-response call
MyServiceClient.fromContext(ctx).myHandler(request, req -> req.idempotencyKey("abc123"));
// For a message
MyServiceClient.fromContext(ctx).send().myHandler(request, req -> req.idempotencyKey("abc123"));
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/ServiceCommunication.kt#idempotency_key"} theme={null}
// For a regular call
MyServiceClient.fromContext(ctx).myHandler(request) { idempotencyKey = "abc123" }
// For a one way call
MyServiceClient.fromContext(ctx).send().myHandler(request) { idempotencyKey = "abc123" }
```
Restate automatically deduplicates calls made during the same handler execution, so there's no need to provide an idempotency key in that case.
However, if multiple handlers might call the same service independently, you can use an idempotency key to ensure deduplication across those calls.
## Attach to an invocation
To wait for or get the result of a previously sent message:
```java Java {"CODE_LOAD::java/src/main/java/develop/ServiceCommunication.java#attach"} theme={null}
var handle =
MyServiceClient.fromContext(ctx)
.send()
.myHandler(request, req -> req.idempotencyKey("abc123"));
var response = handle.attach().await();
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/ServiceCommunication.kt#attach"} theme={null}
val handle =
MyServiceClient.fromContext(ctx).send().myHandler(request) { idempotencyKey = "abc123" }
val response = handle.attach().await()
```
* With an idempotency key: Wait for completion and retrieve the result.
* Without an idempotency key: Can only wait, not retrieve the result.
## Cancel an invocation
To cancel a running handler:
```java Java {"CODE_LOAD::java/src/main/java/develop/ServiceCommunication.java#cancel"} theme={null}
var handle = MyServiceClient.fromContext(ctx).send().myHandler(request);
handle.cancel();
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/ServiceCommunication.kt#cancel"} theme={null}
val handle = MyServiceClient.fromContext(ctx).send().myHandler(request)
handle.cancel()
```
## See also
* **[SDK Clients](/develop/java/service-communication)**: Call Restate services from external applications
* **[Error Handling](/develop/java/error-handling)**: Handle failures and terminal errors in service calls
* **[Durable Timers](/develop/java/durable-timers)**: Implement timeouts for your service calls
* **[Serialization](/develop/java/serialization)**: Customize how data is serialized between services
* **[Sagas](/guides/sagas)**: Roll back or compensate for canceled service calls.
# Services
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/java/services
Implementing Restate services with the Java/Kotlin SDK.
The Restate Java/Kotlin SDK is open source and available on [GitHub](https://github.com/restatedev/sdk-java).
The Restate SDK lets you implement **handlers**. Handlers can be part of a **[Basic Service](/foundations/services#basic-service)**, a **[Virtual Object](/foundations/services#virtual-object)**, or a **[Workflow](/foundations/services#workflow)**. This page shows how to define them with the Java/Kotlin SDK.
## Prerequisites
* [JDK](https://whichjdk.com/) >= 17
Contact us on [Discord](https://discord.com/invite/skW3AZ6uGd)/[Slack](https://join.slack.com/t/restatecommunity/shared_invite/zt-2v9gl005c-WBpr167o5XJZI1l7HWKImA) if you need earlier Java versions.
## Getting started
Get started quickly with the [Java](/quickstart#java) or [Kotlin](/quickstart#kotlin) Quickstart.
Use the following dependencies:
```kt Java theme={null}
// Annotation processor
annotationProcessor("dev.restate:sdk-api-gen:2.4.0")
// For HTTP services
implementation("dev.restate:sdk-java-http:2.4.0")
// Or for Lambda services
implementation("dev.restate:sdk-java-lambda:2.4.0")
```
```kt Kotlin theme={null}
// Annotation processor
ksp("dev.restate:sdk-api-kotlin-gen:2.4.0")
// For HTTP services
implementation("dev.restate:sdk-kotlin-http:2.4.0")
// Or for Lambda services
// implementation("dev.restate:sdk-kotlin-lambda:2.4.0")
```
## Basic Services
[Basic Services](/foundations/services) group related **handlers** and expose them as callable endpoints:
```java Java {"CODE_LOAD::java/src/main/java/develop/MyService.java?collapse_prequel"} theme={null}
@Service
public class MyService {
@Handler
public String myHandler(Context ctx, String greeting) {
return greeting + "!";
}
public static void main(String[] args) {
RestateHttpServer.listen(Endpoint.bind(new MyService()));
}
}
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/MyService.kt?collapse_prequel"} theme={null}
@Service
class MyService {
@Handler suspend fun myHandler(ctx: Context, greeting: String) = "$greeting!"
}
fun main() {
RestateHttpServer.listen(endpoint { bind(MyService()) })
}
```
* Define a service using the [`@Service`](http://docs.restate.dev/javadocs/dev/restate/sdk/annotation/Service.html) and [`@Handler`](http://docs.restate.dev/javadocs/dev/restate/sdk/annotation/Handler.html) annotations
* Each handler can be called at `/MyService/myHandler`. To override the service name (default is simple class name), use the annotation [`@Name`](http://docs.restate.dev/javadocs/dev/restate/sdk/annotation/Name.html).
* Handlers take the `Context` ([JavaDocs](https://restatedev.github.io/sdk-java/javadocs/dev/restate/sdk/ObjectContext)/[KotlinDocs](https://restatedev.github.io/sdk-java/ktdocs/sdk-api-kotlin/dev.restate.sdk.kotlin/-context/)) as the first argument.
* The input parameter (at most one) and return type are optional and can be of any type. See [serialization](/develop/java/serialization) for more details.
* Create an endpoint to expose the service over HTTP (port `9080` by default).
## Virtual Objects
[Virtual Objects](/foundations/services) are services that are stateful and key-addressable — each object instance has a unique ID and persistent state.
```java Java {"CODE_LOAD::java/src/main/java/develop/MyObject.java?collapse_prequel"} theme={null}
@VirtualObject
public class MyObject {
@Handler
public String myHandler(ObjectContext ctx, String greeting) {
String objectId = ctx.key();
return greeting + " " + objectId + "!";
}
@Shared
public String myConcurrentHandler(SharedObjectContext ctx, String input) {
return "my-output";
}
public static void main(String[] args) {
RestateHttpServer.listen(Endpoint.bind(new MyObject()));
}
}
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/MyObject.kt?collapse_prequel"} theme={null}
@VirtualObject
class MyObject {
@Handler
suspend fun myHandler(ctx: ObjectContext, greeting: String): String {
val objectKey = ctx.key()
return "$greeting $objectKey!"
}
@Shared suspend fun myConcurrentHandler(ctx: SharedObjectContext, input: String) = "my-output"
}
fun main() {
RestateHttpServer.listen(endpoint { bind(MyObject()) })
}
```
* Use the `@VirtualObject` annotation.
* The first argument of the handler must be the `ObjectContext` parameter ([JavaDocs](https://restatedev.github.io/sdk-java/javadocs/dev/restate/sdk/ObjectContext)/[KotlinDocs](https://restatedev.github.io/sdk-java/ktdocs/sdk-api-kotlin/dev.restate.sdk.kotlin/-object-context/)).
* Each instance is identified by a key (accessible via `ctx.key()`).
* Virtual Objects can have [exclusive and shared handlers](/foundations/handlers#handler-behavior).
* Exclusive handlers receive an `ObjectContext`, allowing read/write access to object state.
* Shared handlers use the [`@Shared`](http://docs.restate.dev/javadocs/dev/restate/sdk/annotation/Shared.html) annotation and the `SharedObjectContext` ([JavaDocs](https://restatedev.github.io/sdk-java/javadocs/dev/restate/sdk/SharedObjectContext)/[KotlinDocs](https://restatedev.github.io/sdk-java/ktdocs/sdk-api-kotlin/dev.restate.sdk.kotlin/-shared-object-context/)).
## Workflows
[Workflows](/foundations/services) are long-lived processes with a defined lifecycle. They run once per key and are ideal for orchestrating multi-step operations, which require external interaction via signals and queries.
```java Java {"CODE_LOAD::java/src/main/java/develop/MyWorkflow.java?collapse_prequel"} theme={null}
@Workflow
public class MyWorkflow {
@Workflow
public String run(WorkflowContext ctx, String input) {
// implement workflow logic here
return "success";
}
@Shared
public String interactWithWorkflow(SharedWorkflowContext ctx, String input) {
// implement interaction logic here
return "my result";
}
public static void main(String[] args) {
RestateHttpServer.listen(Endpoint.bind(new MyWorkflow()));
}
}
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/MyWorkflow.kt?collapse_prequel"} theme={null}
@Workflow
class MyWorkflow {
@Workflow
suspend fun run(ctx: WorkflowContext, input: String): String {
// implement workflow logic here
return "success"
}
@Handler
suspend fun interactWithWorkflow(ctx: SharedWorkflowContext, input: String): String {
// implement interaction logic here
return "my result"
}
}
fun main() {
RestateHttpServer.listen(endpoint { bind(MyWorkflow()) })
}
```
* Create the workflow by using the [`@Workflow`](http://docs.restate.dev/javadocs/dev/restate/sdk/annotation/Workflow.html) annotation.
* Every workflow **must** include a `run` handler:
* This is the main orchestration entry point
* It runs exactly once per workflow execution and uses the `WorkflowContext` ([JavaDocs](https://restatedev.github.io/sdk-java/javadocs/dev/restate/sdk/WorkflowContext)/[KotlinDocs](https://restatedev.github.io/sdk-java/ktdocs/sdk-api-kotlin/dev.restate.sdk.kotlin/-workflow-context/))
* Resubmission of the same workflow will fail with "Previously accepted". The invocation ID can be found in the request header `x-restate-id`.
* Use `ctx.key()` to access the workflow's unique ID
* Additional handlers must use the `SharedWorkflowContext` ([JavaDocs](https://restatedev.github.io/sdk-java/javadocs/dev/restate/sdk/SharedWorkflowContext)/[KotlinDocs](https://restatedev.github.io/sdk-java/ktdocs/sdk-api-kotlin/dev.restate.sdk.kotlin/-shared-workflow-context/)) and can signal or query the workflow. They can run concurrently with the `run` handler and until the retention time expires.
## Configuring services
Check out the [service configuration docs](/services/configuration) to learn how to configure service behavior, including timeouts and retention policies.
# Serving
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/java/serving
Create an endpoint to serve your services.
export const CustomVars = ({name}) => {
const RESTATE_VERSION = "1.4";
const TYPESCRIPT_SDK_VERSION = "1.7.2";
const JAVA_SDK_VERSION = "2.4.0";
const GO_SDK_VERSION = "0.18.0";
const PYTHON_SDK_VERSION = "0.8.0";
const RUST_SDK_VERSION = "0.6.0";
const mapping = {
RESTATE_VERSION,
TYPESCRIPT_SDK_VERSION,
JAVA_SDK_VERSION,
GO_SDK_VERSION,
PYTHON_SDK_VERSION,
RUST_SDK_VERSION,
JAVA_HTTP_DEPENDENCY: `dev.restate:sdk-java-http:${JAVA_SDK_VERSION}`,
KOTLIN_HTTP_DEPENDENCY: `dev.restate:sdk-kotlin-http:${JAVA_SDK_VERSION}`,
JAVA_LAMBDA_DEPENDENCY: `dev.restate:sdk-java-lambda:${JAVA_SDK_VERSION}`,
KOTLIN_LAMBDA_DEPENDENCY: `dev.restate:sdk-kotlin-lambda:${JAVA_SDK_VERSION}`,
JAVA_SDK_REQUEST_IDENTITY: `dev.restate:sdk-request-identity:${JAVA_SDK_VERSION}`,
JAVA_TESTING: `dev.restate:sdk-testing:${JAVA_SDK_VERSION}`,
JAVA_CLIENT: `dev.restate:client:${JAVA_SDK_VERSION}`,
KOTLIN_CLIENT: `dev.restate:client-kotlin:${JAVA_SDK_VERSION}`,
DENO_FETCH: `npm:@restatedev/restate-sdk@^${TYPESCRIPT_SDK_VERSION}/fetch`
};
return mapping[name];
};
Restate services can run in two ways: as an HTTP endpoint or as AWS Lambda functions.
## Creating an HTTP endpoint
1. Use either {} or {} as SDK dependency.
2. Create an endpoint
3. Bind one or multiple services to it
4. Listen on the specified port (default `9080`) for connections and requests.
```java Java {"CODE_LOAD::java/src/main/java/develop/ServingHttp.java#here"} theme={null}
import dev.restate.sdk.endpoint.Endpoint;
import dev.restate.sdk.http.vertx.RestateHttpServer;
class MyApp {
public static void main(String[] args) {
RestateHttpServer.listen(
Endpoint.bind(new MyService()).bind(new MyObject()).bind(new MyWorkflow()), 8080);
}
}
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/ServingHttp.kt#here"} theme={null}
fun main() {
RestateHttpServer.listen(
endpoint {
bind(MyService())
bind(MyObject())
bind(MyWorkflow())
})
}
```
## Creating a Lambda handler
1. Use either {} or {} as SDK dependency.
2. Extend the class `BaseRestateLambdaHandler`
3. Override the register method
4. Bind one or multiple services to the builder
```java Java {"CODE_LOAD::java/src/main/java/develop/ServingLambda.java#here"} theme={null}
import dev.restate.sdk.endpoint.Endpoint;
import dev.restate.sdk.lambda.BaseRestateLambdaHandler;
class MyLambdaHandler extends BaseRestateLambdaHandler {
@Override
public void register(Endpoint.Builder builder) {
builder.bind(new MyService()).bind(new MyObject());
}
}
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/MyLambdaHandler.kt#here"} theme={null}
import dev.restate.sdk.endpoint.Endpoint
import dev.restate.sdk.lambda.BaseRestateLambdaHandler
class MyLambdaHandler : BaseRestateLambdaHandler() {
override fun register(builder: Endpoint.Builder) {
builder.bind(MyService()).bind(MyObject())
}
}
```
The implementation of your services and handlers remains the same for both deployment options.
Have a look at the [deployment section](/services/deploy/lambda) for guidance on how to deploy your services on AWS Lambda.
If you use a JVM >= 21, you can use virtual threads to run your services:
```java Java {"CODE_LOAD::java/src/main/java/develop/ServingVirtualThreads.java#here"} theme={null}
builder.bind(
new Greeter(),
HandlerRunner.Options.withExecutor(Executors.newVirtualThreadPerTaskExecutor()));
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/ServingVirtualThreads.kt#here"} theme={null}
builder.bind(
Greeter(),
HandlerRunner.Options(
coroutineContext = Executors.newVirtualThreadPerTaskExecutor().asCoroutineDispatcher(),
),
)
```
## Validating request identity
SDKs can validate that incoming requests come from a particular Restate
instance. You can find out more about request identity in the
[Security docs](/services/security#locking-down-service-access). You will need to use the request identity dependency {}.
```java Java {"CODE_LOAD::java/src/main/java/develop/ServingIdentity.java#here"} theme={null}
import dev.restate.sdk.auth.signing.RestateRequestIdentityVerifier;
import dev.restate.sdk.endpoint.Endpoint;
import dev.restate.sdk.http.vertx.RestateHttpServer;
class MySecureApp {
public static void main(String[] args) {
var endpoint =
Endpoint.bind(new MyService())
.withRequestIdentityVerifier(
RestateRequestIdentityVerifier.fromKeys(
"publickeyv1_w7YHemBctH5Ck2nQRQ47iBBqhNHy4FV7t2Usbye2A6f"));
RestateHttpServer.listen(endpoint);
}
}
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/ServingIdentity.kt#here"} theme={null}
import dev.restate.sdk.auth.signing.RestateRequestIdentityVerifier
import dev.restate.sdk.http.vertx.RestateHttpServer
import dev.restate.sdk.kotlin.endpoint.endpoint
fun main() {
RestateHttpServer.listen(
endpoint {
bind(MyService())
// !mark(1:3)
requestIdentityVerifier =
RestateRequestIdentityVerifier.fromKeys(
"publickeyv1_w7YHemBctH5Ck2nQRQ47iBBqhNHy4FV7t2Usbye2A6f",
)
})
}
```
# State
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/java/state
Store key-value state in Restate.
Restate lets you persist key-value (K/V) state using its embedded K/V store.
## Key characteristics
[Learn about Restate's embedded K/V store](/foundations/key-concepts#consistent-state).
**State is only available for Virtual Objects and Workflows.**
Scope & retention:
* For Virtual Objects: State is scoped per object key and retained indefinitely. It is persisted and shared across all invocations for that object until explicitly cleared.
* For Workflows: State is scoped per workflow execution (workflow ID) and retained only for the duration of the workflow’s configured retention time.
Access Rules:
* [Exclusive handlers](/foundations/handlers#handler-behavior) (e.g., `run()` in workflows) can read and write state.
* [Shared handlers](/foundations/handlers#handler-behavior) can only read state and cannot mutate it.
You can inspect and edit the K/V state via the UI and the [CLI](/services/introspection#inspecting-application-state).
## List all state keys
To retrieve all keys for which the current Virtual Object has stored state:
```java Java {"CODE_LOAD::java/src/main/java/develop/State.java#statekeys"} theme={null}
Collection keys = ctx.stateKeys();
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/State.kt#statekeys"} theme={null}
val keys = ctx.stateKeys()
```
## Get state value
To read a value by key.
```java Java {"CODE_LOAD::java/src/main/java/develop/State.java#get"} theme={null}
// Getting String value
StateKey STRING_STATE_KEY = StateKey.of("my-key", String.class);
String stringState = ctx.get(STRING_STATE_KEY).orElse("my-default");
// Getting integer value
StateKey INT_STATE_KEY = StateKey.of("my-key", Integer.class);
int intState = ctx.get(INT_STATE_KEY).orElse(0);
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/State.kt#get"} theme={null}
// Getting String value
val STRING_STATE_KEY = stateKey("my-key")
val stringState: String? = ctx.get(STRING_STATE_KEY)
// Getting integer value
val INT_STATE_KEY = stateKey("my-key")
val intState: Int? = ctx.get(INT_STATE_KEY)
```
See the [serialization docs](/develop/java/serialization) to customize the state serializer.
## Set state value
To write or update a value:
```java Java {"CODE_LOAD::java/src/main/java/develop/State.java#set"} theme={null}
StateKey STRING_STATE_KEY = StateKey.of("my-key", String.class);
ctx.set(STRING_STATE_KEY, "my-new-value");
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/State.kt#set"} theme={null}
val STRING_STATE_KEY = stateKey("my-key")
ctx.set(STRING_STATE_KEY, "my-new-value")
```
## Clear state key
To delete a specific key:
```java Java {"CODE_LOAD::java/src/main/java/develop/State.java#clear"} theme={null}
StateKey STRING_STATE_KEY = StateKey.of("my-key", String.class);
ctx.clear(STRING_STATE_KEY);
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/State.kt#clear"} theme={null}
val STRING_STATE_KEY = stateKey("my-key")
ctx.clear(STRING_STATE_KEY)
```
## Clear all state keys
To remove all stored state for the current Virtual Object:
```java Java {"CODE_LOAD::java/src/main/java/develop/State.java#clear_all"} theme={null}
ctx.clearAll();
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/State.kt#clear_all"} theme={null}
ctx.clearAll()
```
## Advanced: Eager vs. lazy state loading
Restate supports two modes for loading state in handlers:
### Eager state (default)
* **How it works**: State is automatically sent with the request when invoking a handler
* **Benefits**: State is available immediately when the handler starts executing
* **Behavior**: All reads and writes to state are local to the handler execution
* **Best for**: Small to medium state objects that are frequently accessed
### Lazy state
* **How it works**: State is fetched on-demand on `get` calls from the Restate Server
* **Benefits**: Reduces initial request size and memory usage
* **Setup**: Enable lazy state in the [service or handler configuration](/services/configuration)
* **Best for**: Large state objects that aren't needed in every handler execution
# Testing
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/java/testing
Utilities to test your handler logic.
export const CustomVars = ({name}) => {
const RESTATE_VERSION = "1.4";
const TYPESCRIPT_SDK_VERSION = "1.7.2";
const JAVA_SDK_VERSION = "2.4.0";
const GO_SDK_VERSION = "0.18.0";
const PYTHON_SDK_VERSION = "0.8.0";
const RUST_SDK_VERSION = "0.6.0";
const mapping = {
RESTATE_VERSION,
TYPESCRIPT_SDK_VERSION,
JAVA_SDK_VERSION,
GO_SDK_VERSION,
PYTHON_SDK_VERSION,
RUST_SDK_VERSION,
JAVA_HTTP_DEPENDENCY: `dev.restate:sdk-java-http:${JAVA_SDK_VERSION}`,
KOTLIN_HTTP_DEPENDENCY: `dev.restate:sdk-kotlin-http:${JAVA_SDK_VERSION}`,
JAVA_LAMBDA_DEPENDENCY: `dev.restate:sdk-java-lambda:${JAVA_SDK_VERSION}`,
KOTLIN_LAMBDA_DEPENDENCY: `dev.restate:sdk-kotlin-lambda:${JAVA_SDK_VERSION}`,
JAVA_SDK_REQUEST_IDENTITY: `dev.restate:sdk-request-identity:${JAVA_SDK_VERSION}`,
JAVA_TESTING: `dev.restate:sdk-testing:${JAVA_SDK_VERSION}`,
JAVA_CLIENT: `dev.restate:client:${JAVA_SDK_VERSION}`,
KOTLIN_CLIENT: `dev.restate:client-kotlin:${JAVA_SDK_VERSION}`,
DENO_FETCH: `npm:@restatedev/restate-sdk@^${TYPESCRIPT_SDK_VERSION}/fetch`
};
return mapping[name];
};
The Java SDK comes with the module `sdk-testing` that integrates with JUnit 5 and [Testcontainers](https://testcontainers.com/) to start up a Restate container together with your services code and automatically register them.
Add the following dependency to your project: {}.
## Using the JUnit 5 Extension
Given the service to test `MyService`, annotate the test class with `@RestateTest` and annotate the services to bind with `@BindService`:
```java Java {"CODE_LOAD::java/src/main/java/develop/MyServiceTest.java#extension"} theme={null}
@RestateTest
class MyServiceTest {
@BindService MyService service = new MyService();
// Your tests
}
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/MyServiceTest.kt#extension"} theme={null}
@RestateTest
class MyServiceTest {
@BindService val service = MyService()
// Your tests
}
```
Note that the extension will start one Restate server for the whole test class. For more details, checkout [`RestateTest` Javadocs](https://restatedev.github.io/sdk-java/javadocs/dev/restate/sdk/testing/RestateTest.html).
Once the extension is set, you can implement your test methods as usual, and inject a [`Client`](https://restatedev.github.io/sdk-java/javadocs/dev/restate/client/Client.html) using [`@RestateClient`](https://restatedev.github.io/sdk-java/javadocs/dev/restate/sdk/testing/RestateClient.html) to interact with Restate and the registered services:
```java Java {"CODE_LOAD::java/src/main/java/develop/MyServiceTestMethod.java#test"} theme={null}
@Test
void testMyHandler(@RestateClient Client ingressClient) {
// Create the service client from the injected ingress client
var client = MyServiceClient.fromClient(ingressClient);
// Send request to service and assert the response
var response = client.myHandler("Hi");
assertEquals(response, "Hi!");
}
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/MyServiceTestMethod.kt#test"} theme={null}
@Test
fun testMyHandler(@RestateClient ingressClient: Client) = runTest {
// Create the service client from the injected ingress client
val client = MyServiceClient.fromClient(ingressClient)
// Send request to service and assert the response
val response = client.myHandler("Hi")
assertEquals(response, "Hi!")
}
```
## Usage without JUnit 5
You can use the testing tools without JUnit 5 by using [`RestateRunner`](https://restatedev.github.io/sdk-java/javadocs/dev/restate/sdk/testing/RestateRunner.html) directly.
For more details, refer to the Javadocs.
# Concurrent Tasks
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/python/concurrent-tasks
Execute multiple tasks concurrently and gather results.
When building resilient applications, you often need to perform multiple operations in parallel to improve performance and user experience. Restate provides durable concurrency primitives that allow you to run tasks concurrently while maintaining deterministic execution during replays.
## When to use concurrent tasks
Use concurrent tasks when you need to:
* Call multiple external services simultaneously (e.g., fetching data from different APIs)
* Race multiple operations and use the first result (e.g., trying multiple LLM providers)
* Implement timeouts by racing an operation against a timer
* Perform batch operations where individual tasks can run in parallel
## Key benefits
* **Deterministic replay**: Restate logs the order of completion, ensuring consistent behavior during failures
* **Fault tolerance**: If your handler fails, tasks that were already completed will be replayed with their results, while pending tasks will be retried
## Parallelizing tasks
Start multiple durable operations concurrently by calling them without immediately awaiting:
```python {"CODE_LOAD::python/src/develop/journaling_results.py#parallel"} theme={null}
# Start operations concurrently
call1 = ctx.run_typed("fetch_user", fetch_user_data, user_id=123)
call2 = ctx.run_typed("fetch_orders", fetch_order_history, user_id=123)
call3 = ctx.service_call(calculate_metrics, arg=123)
# Now wait for results as needed
user = await call1
orders = await call2
metrics = await call3
```
Check out the guide on [parallelizing work](guides/parallelizing-work).
## Retrieving results
Restate provides several patterns for coordinating concurrent tasks. All patterns use `RestateDurableFuture` combinators that log the order of completion, ensuring deterministic behavior during replays.
### Waiting for first completion
There are two ways to do this.
#### Select
Use `restate.select()` to race multiple operations and handle the first one that completes. This is ideal for implementing timeouts or waiting for external confirmations:
```python {"CODE_LOAD::python/src/develop/journaling_results.py#select"} theme={null}
_, confirmation_future = ctx.awakeable(type_hint=str)
match await restate.select(
confirmation=confirmation_future, timeout=ctx.sleep(timedelta(days=1))
):
case ["confirmation", "ok"]:
return "success!"
case ["confirmation", "deny"]:
raise TerminalError("Confirmation was denied!")
case _:
raise TerminalError("Verification timer expired!")
```
#### Wait completed
Use `restate.wait_completed()` when you want to wait for at least one task to complete.
This returns a tuple of two lists: the first list contains the futures that are completed,
the second list contains the futures that are not completed.
This gives you the option to, for example, cancel the pending futures:
```python {"CODE_LOAD::python/src/develop/journaling_results.py#wait_completed"} theme={null}
claude = ctx.service_call(claude_sonnet, arg=f"What is the weather?")
openai = ctx.service_call(open_ai, arg=f"What is the weather?")
pending, done = await restate.wait_completed(claude, openai)
# collect the completed results
results = [await f for f in done]
# cancel the pending calls
for f in pending:
await f.cancel_invocation()
```
#### Key Differences
* **Return behavior**: `select` returns as soon as the first future completes, while `wait_completed` waits for at least one to complete and returns both completed and pending futures.
* **Use cases**:
* Use `select` when you want to race multiple operations and act on whichever completes first.
* Use `wait_completed` when you want to handle completed futures immediately while potentially canceling or managing pending ones,
* **Pattern matching**: `select` uses pattern matching to determine which future completed, while `wait_completed` separates futures into completed and pending collections.
### Waiting for all tasks to complete
```python {"CODE_LOAD::python/src/develop/journaling_results.py#all"} theme={null}
claude = ctx.service_call(claude_sonnet, arg=f"What is the weather?")
openai = ctx.service_call(open_ai, arg=f"What is the weather?")
results_done = await restate.gather(claude, openai)
results = [await result for result in results_done]
```
### Processing results as they complete
Use `restate.as_completed()` to process results in the order they finish:
```python {"CODE_LOAD::python/src/develop/journaling_results.py#as_completed"} theme={null}
call1 = ctx.run_typed(
"LLM call", call_llm, prompt="What is the weather?", model="gpt-4"
)
call2 = ctx.run_typed(
"LLM call", call_llm, prompt="What is the weather?", model="gpt-3.5-turbo"
)
async for future in restate.as_completed(call1, call2):
# do something with the completed future
print(await future)
```
# Durable Steps
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/python/durable-steps
Persist results of operations.
Restate uses an execution log to replay operations after failures and suspensions. Non-deterministic operations (database calls, HTTP requests, UUID generation) must be wrapped to ensure deterministic replay.
## Run
Use `ctx.run` to safely wrap any non-deterministic operation, like HTTP calls or database responses, and have Restate store its result in the execution log.
```python {"CODE_LOAD::python/src/develop/journaling_results.py#side_effect"} theme={null}
async def call_llm(prompt: str) -> str:
# ... implement ...
return "llm response"
# specify the (async) function to call and its arguments
result = await ctx.run_typed("LLM call", call_llm, prompt="What is the weather?")
# or use a lambda to capture a single value
my_number = await ctx.run_typed("generate number", lambda: random.randint(0, 10))
```
Note that inside `ctx.run`, you cannot use the Restate context (e.g., `ctx.get`, `ctx.sleep`, or nested `ctx.run`).
By default, the SDK serializes the journal entry with the [`json`](https://docs.python.org/3/library/json.html#) library.
Alternatively, you can specify a [Pydantic model](/develop/python/serialization#pydantic) or [custom serializer](/develop/python/serialization#custom-serialization).
Failures in `ctx.run` are treated the same as any other handler error. Restate will retry it unless configured otherwise or unless a [`TerminalError`](/develop/python/error-handling) is thrown.
You can customize how `ctx.run` retries via:
```py {"CODE_LOAD::python/src/develop/retries.py#here"} theme={null}
try:
retry_opts = restate.RunOptions(
max_attempts=10, max_retry_duration=timedelta(seconds=30)
)
await ctx.run_typed("write", write_to_other_system, retry_opts)
except TerminalError as err:
# Handle the terminal error after retries exhausted
# For example, undo previous actions (see sagas guide) and
# propagate the error back to the caller
raise err
```
* You can limit retries by time or count
* When the policy is exhausted, a `TerminalError` is thrown
* See the [Error Handling Guide](/guides/error-handling) and the [Sagas Guide](/guides/sagas) for patterns like compensation
If Restate doesn't receive new journal entries from a service for more than one minute (by default), it will automatically abort the invocation and retry it.
However, some business logic can take longer to complete—for example, an LLM call that takes up to 3 minutes to respond.
In such cases, you can adjust the service’s [abort timeout and inactivity timeout](/services/configuration) settings to accommodate longer execution times.
For more information, see the [error handling guide](/guides/error-handling).
## Deterministic randoms
When you do non-deterministic operations, like generating UUIDs or random numbers, you must ensure that the results are deterministic on replay.
For example, to generate stable UUIDs for things like idempotency keys:
```python {"CODE_LOAD::python/src/develop/journaling_results.py#uuid"} theme={null}
my_uuid = ctx.uuid()
```
The SDK provides deterministic helpers for random values — seeded by the invocation ID — so they return the **same result on retries**.
### UUIDs
To generate stable UUIDs for things like idempotency keys:
```python {"CODE_LOAD::python/src/develop/journaling_results.py#uuid"} theme={null}
my_uuid = ctx.uuid()
```
Do not use this in cryptographic contexts.
### Random numbers
To generate a deterministic float between `0` and `1`:
```python {"CODE_LOAD::python/src/develop/journaling_results.py#random_nb"} theme={null}
ctx.random().random()
```
This behaves like `Math.random()` but is deterministically replayable.
### Deterministic time
To get the current millis since midnight, January 1, 1970, that is consistent across retries:
```python {"CODE_LOAD::python/src/develop/journaling_results.py#time"} theme={null}
current_time = await ctx.time()
```
# Scheduling & Timers
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/python/durable-timers
Durable timers, scheduled actions, and sleep, backed by Restate.
Restate provides durable, fault-tolerant timers that allow you to:
* [Sleep](#durable-sleep): Pause a handler for a given duration
* [Send delayed messages](#scheduling-async-tasks): Schedule handler invocations in the future
* [Set timeouts](#timers-and-timeouts) for async operations
* Implement patterns like [cron jobs](#cron-jobs)
## How it works
Restate tracks and manages timers, ensuring they survive failures and restarts.
For example, if a service is sleeping for 12 hours and fails after 8 hours, then Restate will make sure it only sleeps 4 hours more.
If your handler runs on function-as-a-service platforms like AWS Lambda, Restate suspends the handler while it is sleeping, to free up resources.
Since these platforms charge on execution time, this saves costs.
## Durable sleep
To pause a handler for a set duration:
```python {"CODE_LOAD::python/src/develop/durable_timers.py#here"} theme={null}
await ctx.sleep(delta=timedelta(seconds=10))
```
There is no hard limit on how long you can sleep. You can sleep for months or even years, but keep in mind:
* If you sleep in an [exclusive handler](/foundations/handlers#handler-behavior) in a Virtual Object, all other calls to this object will be queued.
* You need to keep the deployment version until the invocation completes (see [versioning](/services/versioning)).
So for long sleeps, we recommend breaking this up into multiple handlers that call each other with [delayed messages](/develop/python/service-communication#delayed-messages).
The Restate SDK calculates the wake-up time based on the delay you specify.
The Restate Server then uses this calculated time to wake up the handler.
If the Restate Server and the SDK have different system clocks, the sleep duration might not be accurate.
So make sure that the system clock of the Restate Server and the SDK have the same timezone and are synchronized.
A mismatch can cause timers to fire earlier or later than expected.
## Scheduling async tasks
To invoke a handler at a later time, use [delayed messages](/develop/python/service-communication#delayed-messages).
In theory, you can schedule future work in Restate in two ways:
1. **Delayed messages** (recommended)
2. **Sleep + send** - sleeping in the current handler, then sending a message
At first sight, both approaches might seem to achieve similar results.
However, we recommend to use delayed messages for the following reasons:
* **Handler completes immediately**: The calling handler can finish execution and complete the invocation without waiting for the delay to finish
* **No Virtual Object blocking**: Other requests to the same Virtual Object can be processed during the delay period
* **Better for service versioning**: No long-running invocations that require keeping old service deployments around for a long time (see [service versioning](/services/versioning))
## Timers and timeouts
Most context actions are async actions that return a `RestateDurableFuture`.
You can race these against a timer to bound how long your code waits for an operation.
For example, to either wait for the greeting or for the timeout:
```py {"CODE_LOAD::python/src/develop/durable_timers.py#timer"} theme={null}
match await restate.select(
greeting=ctx.service_call(my_service.my_handler, "Hi"),
timeout=ctx.sleep(timedelta(seconds=5)),
):
case ["greeting", greeting]:
print("Greeting:", greeting)
case _:
print("Timeout occurred")
```
For alternative ways of racing async operations, have a look at the [`RestateDurableFuture` Combinators](/develop/python/concurrent-tasks).
## Cron jobs
Restate does not yet include native cron support, but you can implement your own cron scheduler using:
* Durable timers
* Virtual Objects
* A repeat loop or sleep-schedule pattern
Check out the guide on [implementing cron jobs](/guides/cron).
# Error Handling
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/python/error-handling
Stop infinite retries with Terminal Errors.
Restate handles retries for failed invocations.
Check out the [Error Handling guide](/guides/error-handling) to learn more about how Restate handles transient errors, terminal errors, retries, and timeouts.
## Retry strategies
By default, Restate does infinite retries with an exponential backoff strategy.
Check out the [error handling guide](/guides/error-handling) to learn how to customize this.
## Terminal Errors
For failures for which you do not want retries, but instead want the invocation to end and the error message
to be propagated back to the caller, you can throw a **terminal error**.
You can throw a `TerminalError` with an optional HTTP status code and a message anywhere in your handler, as follows:
```python {"CODE_LOAD::python/src/develop/error_handling.py#terminal"} theme={null}
from restate.exceptions import TerminalError
raise TerminalError("Something went wrong.")
```
You can catch terminal errors, and build your control flow around it.
When you throw a terminal error, you might need to undo the actions you did earlier in your handler to make sure that your system remains in a consistent state.
Have a look at our [sagas guide](/guides/sagas) to learn more.
# External Events
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/python/external-events
Handle external events and human-in-the-loop patterns with durable waiting primitives.
Sometimes your handlers need to pause and wait for external processes to complete. This is common in:
* **Human-in-the-loop workflows** (approvals, reviews, manual steps)
* **External system integration** (waiting for webhooks, async APIs)
* **AI agent patterns** (tool execution, human oversight)
This pattern is also known as the **callback** or **task token** pattern.
## Two Approaches
Restate provides two primitives for handling external events:
| Primitive | Use Case | Key Feature |
| -------------------- | -------------------------- | ------------------------------------ |
| **Awakeables** | Services & Virtual Objects | Unique ID-based completion |
| **Durable Promises** | Workflows only | Named promises for simpler signaling |
## How it works
Implementing this pattern in a distributed system is tricky, since you need to ensure that the handler can recover from failures and resume waiting for the external event.
Restate makes promises are durable and distributed. They survive crashes and can be resolved or rejected by any handler in the workflow.
To save costs on FaaS deployments, Restate lets the handler [suspend](/foundations/key-concepts#suspensions-on-faas) while awaiting the promise, and invokes it again when the result is available.
## Awakeables
**Best for:** Services and Virtual Objects where you need to coordinate with external systems.
### Creating and waiting for awakeables
1. **Create an awakeable** - Get a unique ID and promise
2. **Send the ID externally** - Pass the awakeable ID to your external system
3. **Wait for result** - Your handler [suspends](/foundations/key-concepts#suspensions-on-faas) until the external system responds
```py {"CODE_LOAD::python/src/develop/awakeables.py#here"} theme={null}
id, promise = ctx.awakeable(type_hint=str)
await ctx.run_typed("trigger task", request_human_review, name=name, id=id)
review = await promise
```
By default, the SDK serializes the journal entry with the [`json`](https://docs.python.org/3/library/json.html#) library.
Alternatively, you can specify a [Pydantic model](/develop/python/serialization#pydantic) or [custom serializer](/develop/python/serialization#custom-serialization).
Note that if you wait for an awakeable in an [exclusive handler](/foundations/handlers#handler-behavior) in a Virtual Object, all other calls to this object will be queued.
### Resolving/rejecting Awakeables
External processes complete awakeables in two ways:
* **Resolve** with success data → handler continues normally
* **Reject** with error reason → throws a [terminal error](/develop/python/error-handling) in the waiting handler
#### Via SDK (from other handlers)
**Resolve:**
```python {"CODE_LOAD::python/src/develop/awakeables.py#resolve"} theme={null}
ctx.resolve_awakeable(name, review)
```
**Reject:**
```python {"CODE_LOAD::python/src/develop/awakeables.py#reject"} theme={null}
ctx.reject_awakeable(name, "Cannot be reviewed")
```
#### Via HTTP API
External systems can complete awakeables using Restate's HTTP API:
**Resolve with data:**
```shell theme={null}
curl localhost:8080/restate/awakeables/sign_1PePOqp/resolve \
--json '"Looks good!"'
```
**Reject with error:**
```shell theme={null}
curl localhost:8080/restate/awakeables/sign_1PePOqp/reject \
-H 'content-type: text/plain' \
-d 'Review rejected: insufficient documentation'
```
## Durable Promises
**Best for:** Workflows where you need to signal between different workflow handlers.
**Key differences from awakeables:**
* No ID management - use logical names instead
* Scoped to workflow execution lifetime
Use this for:
* Sending data to the run handler
* Have handlers wait for events emitted by the run handler
After a workflow's run handler completes, other handlers can still be called for up to 24 hours (default).
The results of resolved Durable Promises remain available during this time.
Update the retention time via the [service configuration](/services/configuration).
### Creating and waiting for promises
Wait for a promise by name:
```py {"CODE_LOAD::python/src/develop/durable_promise.py#promise"} theme={null}
review = await ctx.promise("review").value()
```
### Resolving/rejecting promises
Resolve/reject from any workflow handler:
```py {"CODE_LOAD::python/src/develop/durable_promise.py#resolve_promise"} theme={null}
await ctx.promise("review").resolve(review)
```
### Complete workflow example
```py expandable {"CODE_LOAD::python/src/develop/durable_promise.py#review"} theme={null}
review_workflow = restate.Workflow("ReviewWorkflow")
@review_workflow.main()
async def run(ctx: restate.WorkflowContext, document_id: str):
# Send document for review
await ctx.run_typed("ask review", ask_review, document_id=document_id)
# Wait for external review submission
review = await ctx.promise("review").value()
# Process the review result
return process_review(document_id, review)
@review_workflow.handler()
async def submit_review(ctx: restate.WorkflowSharedContext, review: str):
# Signal the waiting run handler
await ctx.promise("review").resolve(review)
app = restate.app([review_workflow])
```
### Two signaling patterns
**External → Workflow** (shown above): External handlers signal the run handler
* Use for human approvals, external API responses, manual interventions
* External handlers call the handler which resolves the promise
**Workflow → External**: Run handler signals other handlers waiting for workflow events
* Use for step completion notifications, status updates, result broadcasting
* Run handler resolves promises that external handlers are awaiting
## Best Practices
* **Use awakeables** for services/objects coordinating with external systems
* **Use durable promises** for workflow signaling
* **Always handle rejections** to gracefully manage failures
* **Include timeouts** for long-running external processes
# Serialization
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/python/serialization
Customize serialization for SDK actions.
Restate sends data over the network for storing state, journaling actions, awakeables, etc.
Therefore, Restate needs to serialize and deserialize the journal entries.
## Default Serialization
By default, the SDK serializes the journal entry with the [`json`](https://docs.python.org/3/library/json.html#) library.
If this does not work for your data type, then you need to specify a custom serializer, as shown below.
## Pydantic
[Pydantic](https://docs.pydantic.dev/latest/) is a data validation and parsing library for Python.
You can use Pydantic models to define the structure of your data: handler input/output, state, etc.
### Using Pydantic
Make sure to install the optional `serde` dependency of the Restate SDK: `restate-sdk[serde]`.
Then do the following:
```python {"CODE_LOAD::python/src/develop/serialization.py#using_pydantic"} theme={null}
class Delivery(BaseModel):
timestamp: datetime
dimensions: tuple[int, int]
class CompletedDelivery(BaseModel):
status: str
timestamp: datetime
# For the input/output serialization of your handlers
@my_object.handler()
async def deliver(ctx: ObjectContext, delivery: Delivery) -> CompletedDelivery:
# To get state
await ctx.get("delivery", type_hint=Delivery)
# To serialize awakeable payloads
ctx.awakeable(type_hint=Delivery)
# To serialize the results of actions
await ctx.run_typed("some-task", do_something, restate.RunOptions(type_hint=Delivery))
return CompletedDelivery(status="delivered", timestamp=datetime.now())
```
### Pydantic & OpenAPI
Pydantic integrates well with [OpenAPI](https://www.openapis.org/). Restate generates the OpenAPI specifications for you.
If you use Pydantic, you can use the OpenAPI-generated clients to interact with your services.
You can find example clients in the UI playground (click on your service in the overview and then on playground).
## Dataclasses
You can also use Python's built-in `dataclasses` to define the structure of your data.
Make sure to install the optional `serde` dependency of the Restate SDK `restate-sdk[serde]`.
Then add a type hint in a similar way as you would with Pydantic.
## Custom Serialization
To write a custom serializer, you implement the `Serde` interface.
For example a custom JSON serializer could look like this:
```python {"CODE_LOAD::python/src/develop/serialization.py#custom"} theme={null}
class MyData(typing.TypedDict):
"""Represents a response from the GPT model."""
some_value: str
my_number: int
class MySerde(Serde[MyData]):
def deserialize(self, buf: bytes) -> typing.Optional[MyData]:
if not buf:
return None
data = json.loads(buf)
return MyData(some_value=data["some_value"], my_number=data["some_number"])
def serialize(self, obj: typing.Optional[MyData]) -> bytes:
if obj is None:
return bytes()
data = {"some_value": obj["some_value"], "some_number": obj["my_number"]}
return bytes(json.dumps(data), "utf-8")
```
You then use this serializer in your handlers, as follows:
```python {"CODE_LOAD::python/src/develop/serialization.py#using_custom_serde"} theme={null}
# For the input/output serialization of your handlers
@my_object.handler(input_serde=MySerde(), output_serde=MySerde())
async def my_handler(ctx: ObjectContext, greeting: str) -> str:
# To serialize state
await ctx.get("my_state", serde=MySerde())
ctx.set("my_state", MyData(some_value="Hi", my_number=15), serde=MySerde())
# To serialize awakeable payloads
ctx.awakeable(serde=MySerde())
# etc.
return "some-output"
```
# Service Communication
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/python/service-communication
Call other services from your handler.
Your Restate handler can call other handlers in three ways:
* **[Request-response calls](#request-response-calls)**: Call and wait for a response
* **[One-way messages](#sending-messages)**: Send a message and continue
* **[Delayed messages](#delayed-messages)**: Send a message after a delay
To call a service from an external application, see the [HTTP](/services/invocation/http) or [Kafka](/services/invocation/kafka) documentation.
[Learn how Restate how it works](/foundations/key-concepts#resilient-communication)
## Request-response calls
To call a Restate handler and wait for its result:
```python {"CODE_LOAD::python/src/develop/service_communication.py#request_response"} theme={null}
# import my_service # Import the service module to get to the handler, not the service itself
# To call a Service:
response = await ctx.service_call(my_service.my_handler, arg="Hi")
# To call a Virtual Object:
response = await ctx.object_call(my_object.my_handler, key="Mary", arg="Hi")
# To call a Workflow:
# `run` handler — can only be called once per workflow ID
response = await ctx.workflow_call(my_workflow.run, key="my_workflow_id", arg="Hi")
# Other handlers can be called anytime within workflow retention
response = await ctx.workflow_call(
my_workflow.interact_with_workflow, key="my_workflow_id", arg="Hi"
)
```
Use generic calls when you don't have the service definition or need dynamic service names:
```python {"CODE_LOAD::python/src/develop/service_communication.py#request_response_generic"} theme={null}
response = await ctx.generic_call(
"MyObject", "my_handler", key="Mary", arg=json.dumps("Hi").encode("utf-8")
)
```
After a workflow's run handler completes, other handlers can still be called for up to 24 hours (default).
Update this via the [service configuration](/services/configuration).
Request-response calls between [exclusive handlers](/foundations/handlers#handler-behavior) of Virtual Objects may lead to deadlocks:
* Cross deadlock: A → B and B → A (same keys).
* Cycle deadlock: A → B → C → A.
Use the UI or CLI to [cancel](/services/invocation/managing-invocations#cancel) and unblock deadlocked invocations.
## Sending messages
To send a message to another Restate handler without waiting for a response:
```python {"CODE_LOAD::python/src/develop/service_communication.py#one_way"} theme={null}
# To message a Service:
ctx.service_send(my_service.my_handler, arg="Hi")
# To message a Virtual Object:
ctx.object_send(my_object.my_handler, key="Mary", arg="Hi")
# To message a Workflow:
# `run` handler — can only be called once per workflow ID
ctx.workflow_send(my_workflow.run, key="my_wf_id", arg="Hi")
# Other handlers can be called anytime within workflow retention
ctx.workflow_send(my_workflow.interact_with_workflow, key="my_wf_id", arg="Hi")
```
Restate handles message delivery and retries, so the handler can complete and return without waiting for the message to be processed.
Use generic send when you don't have the service definition:
```python {"CODE_LOAD::python/src/develop/service_communication.py#one_way_generic"} theme={null}
ctx.generic_send("MyService", "my_handler", arg=json.dumps("Hi").encode("utf-8"))
```
Calls to a Virtual Object execute in order of arrival, serially.
Example:
```python {"CODE_LOAD::python/src/develop/service_communication.py#ordering"} theme={null}
ctx.object_send(my_object.my_handler, key="Mary", arg="I'm call A")
ctx.object_send(my_object.my_handler, key="Mary", arg="I'm call B")
```
Call A is guaranteed to execute before B. However, other invocations may interleave between A and B.
## Delayed messages
To send a message after a delay:
```python {"CODE_LOAD::python/src/develop/service_communication.py#delayed"} theme={null}
# To message a Service with a delay:
ctx.service_send(my_service.my_handler, arg="Hi", send_delay=timedelta(hours=5))
# To message a Virtual Object with a delay:
ctx.object_send(
my_object.my_handler, key="Mary", arg="Hi", send_delay=timedelta(hours=5)
)
# To message a Workflow with a delay:
ctx.workflow_send(
my_workflow.run, key="my_workflow_id", arg="Hi", send_delay=timedelta(hours=5)
)
```
Use generic send with a delay when you don't have the service definition:
```python {"CODE_LOAD::python/src/develop/service_communication.py#delayed_generic"} theme={null}
ctx.generic_send(
"MyService",
"my_handler",
arg=json.dumps("Hi").encode("utf-8"),
send_delay=timedelta(hours=5),
)
```
Learn [how this is different](/develop/python/durable-timers#scheduling-async-tasks) from sleeping and then sending a message.
## Using an idempotency key
To prevent duplicate executions of the same call, add an idempotency key:
```python {"CODE_LOAD::python/src/develop/service_communication.py#idempotency_key"} theme={null}
await ctx.service_call(
my_service.my_handler,
arg="Hi",
idempotency_key="my-idempotency-key",
)
```
Restate automatically deduplicates calls made during the same handler execution, so there's no need to provide an idempotency key in that case.
However, if multiple handlers might call the same service independently, you can use an idempotency key to ensure deduplication across those calls.
## Attach to an invocation
To wait for or get the result of a previously sent message:
```python {"CODE_LOAD::python/src/develop/service_communication.py#attach"} theme={null}
# Send a request, get the invocation id
handle = ctx.service_send(
my_service.my_handler, arg="Hi", idempotency_key="my-idempotency-key"
)
invocation_id = await handle.invocation_id()
# Now re-attach
result = await ctx.attach_invocation(invocation_id)
```
* With an idempotency key: Wait for completion and retrieve the result.
* Without an idempotency key: Can only wait, not retrieve the result.
## Cancel an invocation
To cancel a running handler:
```python {"CODE_LOAD::python/src/develop/service_communication.py#cancel"} theme={null}
ctx.cancel_invocation(invocation_id)
```
## See also
* **[Error Handling](/develop/python/error-handling)**: Handle failures and terminal errors in service calls
* **[Durable Timers](/develop/python/durable-timers)**: Implement timeouts for your service calls
* **[Serialization](/develop/python/serialization)**: Customize how data is serialized between services
* **[Sagas](/guides/sagas)**: Roll back or compensate for canceled service calls.
# Services
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/python/services
Implementing Restate services with the Python SDK.
The Restate Python SDK is open source and available on [GitHub](https://github.com/restatedev/sdk-python).
The Restate SDK lets you implement **handlers**. Handlers can be part of a **[Basic Service](/foundations/services#basic-service)**, a **[Virtual Object](/foundations/services#virtual-object)**, or a **[Workflow](/foundations/services#workflow)**. This page shows how to define them with the Python SDK.
## Prerequisites
* Python >= v3.11
## Getting started
Get started quickly with the [Python Quickstart](/quickstart#python).
Add the `restate_sdk[serde]` dependency to your Python project to start developing Restate services.
## Basic Services
[Basic Services](/foundations/services) group related **handlers** and expose them as callable endpoints:
```python {"CODE_LOAD::python/src/develop/my_service.py"} theme={null}
import restate
my_service = restate.Service("MyService")
@my_service.handler("myHandler")
async def my_handler(ctx: restate.Context, greeting: str) -> str:
return f"${greeting}!"
app = restate.app([my_service])
```
* Initialize the Service and specify the service name (here `MyService`).
* Annotate each handler with `@my_service.handler()`. Optionally, you can override the handler name (here `myHandler`).
* Each handler can then be called at `/myService/myHandler`
* Handlers take the `restate.Context` as the first argument.
* Handlers can take one optional JSON-serializable input and must return an optional output. These can be of any primitive type, `TypedDict` or Pydantic model (or see [custom serialization](/develop/python/serialization) for advanced types).
* Finally, initialize the app, bind the service(s) to it, and serve it. Look at the [serving docs](/develop/python/serving) to learn more about serving your app.
## Virtual Objects
[Virtual Objects](/foundations/services) are services that are stateful and key-addressable — each object instance has a unique ID and persistent state.
```python {"CODE_LOAD::python/src/develop/my_virtual_object.py"} theme={null}
import restate
my_object = restate.VirtualObject("MyVirtualObject")
@my_object.handler("myHandler")
async def my_handler(ctx: restate.ObjectContext, greeting: str) -> str:
return f"${greeting} ${ctx.key()}!"
@my_object.handler(kind="shared")
async def my_concurrent_handler(ctx: restate.ObjectSharedContext, greeting: str) -> str:
return f"${greeting} ${ctx.key()}!"
app = restate.app([my_object])
```
* Initialize a `restate.VirtualObject` and specify the object's name (here `MyVirtualObject`).
* Each instance is identified by a key (accessible via `ctx.key()`).
* Virtual Objects can have [exclusive and shared handlers](/foundations/handlers#handler-behavior).
* Exclusive handlers receive an `ObjectContext`, allowing read/write access to object state.
* Shared handlers have the annotation `kind="shared"` and receive an `ObjectSharedContext`.
## Workflows
[Workflows](/foundations/services) are long-lived processes with a defined lifecycle. They run once per key and are ideal for orchestrating multi-step operations, which require external interaction via signals and queries.
```python {"CODE_LOAD::python/src/develop/my_workflow.py"} theme={null}
import restate
my_workflow = restate.Workflow("MyWorkflow")
@my_workflow.main()
async def run(ctx: restate.WorkflowContext, req: str) -> str:
# ... implement workflow logic here ---
return "success"
@my_workflow.handler()
async def interact_with_workflow(ctx: restate.WorkflowSharedContext, req: str):
# ... implement interaction logic here ...
return
app = restate.app([my_workflow])
```
* Initialize a `restate.Workflow` and specify its name (here `MyWorkflow`).
* Every workflow **must** include a `run` handler:
* This is the main orchestration entry point and is annotated with `@my_workflow.main()`.
* It runs exactly once per workflow execution and uses the `WorkflowContext`
* Resubmission of the same workflow will fail with "Previously accepted". The invocation ID can be found in the request header `x-restate-id`.
* Use `ctx.key()` to access the workflow's unique ID
* Additional handlers are annotated with `@my_workflow.handler()`. They must use the `WorkflowSharedContext` and can signal or query the workflow. They can run concurrently with the `run` handler and until the retention time expires.
## Configuring services
Check out the [service configuration docs](/services/configuration) to learn how to configure service behavior, including timeouts and retention policies.
# Serving
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/python/serving
Create an endpoint to serve your services.
Restate services can run in a few ways: as an HTTP handler, or as an AWS Lambda handler.
## Creating the app
Create the app and bind one or multiple services to it:
```python {"CODE_LOAD::python/src/develop/serving.py#endpoint"} theme={null}
import restate
app = restate.app(services=[my_service, my_object])
```
If you are using [FastAPI](https://fastapi.tiangolo.com/), you can mount the Restate app on a route as follows:
```python {"CODE_LOAD::python/src/develop/serving.py#fastapi"} theme={null}
from fastapi import FastAPI
app = FastAPI()
app.mount("/restate/v1", restate.app(services=[my_service, my_object]))
```
Then, [register the service](/services/versioning#registering-a-deployment) with Restate specifying this route, here `localhost:9080/restate/v1`.
## Serving the app
The Python SDK follows the [ASGI](https://asgi.readthedocs.io/en/latest/introduction.html) standard for the serving of the services.
[Hypercorn](https://pypi.org/project/Hypercorn/) and [Uvicorn](https://www.uvicorn.org/) are two popular ASGI servers that you can use to serve your Restate services.
### Hypercorn
The templates and examples use [Hypercorn](https://pypi.org/project/Hypercorn/) to serve the services.
#### Hypercorn development server
To use Hypercorn during development, you can run the app with:
```python {"CODE_LOAD::python/src/develop/serving.py#hypercorn"} theme={null}
if __name__ == "__main__":
import hypercorn
import hypercorn.asyncio
import asyncio
conf = hypercorn.Config()
conf.bind = ["0.0.0.0:9080"]
asyncio.run(hypercorn.asyncio.serve(app, conf))
```
Check out the [Quickstart](/quickstart) for more instructions.
#### Hypercorn production server
To run the app in production, we recommend using a configuration file for Hypercorn:
```toml hypercorn-config.toml theme={null}
bind = "0.0.0.0:9080"
h2_max_concurrent_streams = 2147483647
keep_alive_max_requests = 2147483647
keep_alive_timeout = 2147483647
workers = 8
```
Run the app with:
```shell theme={null}
python -m hypercorn --config hypercorn-config.toml example:app
```
This serves the app you specified in an `example.py` file, which contains [the `app`](/develop/python/serving#creating-the-app).
### Uvicorn
You can also use [Uvicorn](https://www.uvicorn.org/) to serve [the app](/develop/python/serving#creating-the-app).
To run the app with Uvicorn:
```shell theme={null}
uvicorn example:app
```
Uvicorn does not support HTTP/2, so you need to tell Restate to use HTTP/1.1 when [registering the deployment](/services/versioning#deployments-supporting-only-http1-1).
## Validating request identity
SDKs can validate that incoming requests come from a particular Restate
instance. You can find out more about request identity in the [Security docs](/services/security#locking-down-service-access)
```python {"CODE_LOAD::python/src/develop/serving.py#identity"} theme={null}
app = restate.app(
services=[my_service],
identity_keys=["publickeyv1_w7YHemBctH5Ck2nQRQ47iBBqhNHy4FV7t2Usbye2A6f"],
)
```
# State
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/python/state
Store key-value state in Restate.
Restate lets you persist key-value (K/V) state using its embedded K/V store.
## Key characteristics
[Learn about Restate's embedded K/V store](/foundations/key-concepts#consistent-state).
**State is only available for Virtual Objects and Workflows.**
Scope & retention:
* For Virtual Objects: State is scoped per object key and retained indefinitely. It is persisted and shared across all invocations for that object until explicitly cleared.
* For Workflows: State is scoped per workflow execution (workflow ID) and retained only for the duration of the workflow’s configured retention time.
Access Rules:
* [Exclusive handlers](/foundations/handlers#handler-behavior) (e.g., `run()` in workflows) can read and write state.
* [Shared handlers](/foundations/handlers#handler-behavior) can only read state and cannot mutate it.
You can inspect and edit the K/V state via the UI and the [CLI](/services/introspection#inspecting-application-state).
## List all state keys
To retrieve all keys for which the current Virtual Object has stored state:
```python {"CODE_LOAD::python/src/develop/state.py#statekeys"} theme={null}
state_keys = ctx.state_keys()
```
## Get state value
To read a value by key:
```python {"CODE_LOAD::python/src/develop/state.py#get"} theme={null}
my_string = await ctx.get("my-string-key", type_hint=str) or "default-key"
my_number = await ctx.get("my-number-key", type_hint=int) or 123
```
The return value is `None` if no value was stored.
See the [serialization docs](/develop/python/serialization) to customize the state serializer.
## Set state value
To write or update a value:
```python {"CODE_LOAD::python/src/develop/state.py#set"} theme={null}
ctx.set("my-key", "my-new-value")
```
## Clear state key
To delete a specific key:
```python {"CODE_LOAD::python/src/develop/state.py#clear"} theme={null}
ctx.clear("my-key")
```
## Clear all state keys
To remove all stored state for the current Virtual Object:
```python {"CODE_LOAD::python/src/develop/state.py#clear_all"} theme={null}
ctx.clear_all()
```
## Advanced: Eager vs. lazy state loading
Restate supports two modes for loading state in handlers:
### Eager state (default)
* **How it works**: State is automatically sent with the request when invoking a handler
* **Benefits**: State is available immediately when the handler starts executing
* **Behavior**: All reads and writes to state are local to the handler execution
* **Best for**: Small to medium state objects that are frequently accessed
### Lazy state
* **How it works**: State is fetched on-demand on `get` calls from the Restate Server
* **Benefits**: Reduces initial request size and memory usage
* **Setup**: Enable lazy state in the [service or handler configuration](/services/configuration)
* **Best for**: Large state objects that aren't needed in every handler execution
# Testing
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/python/testing
Utilities to test your handler logic.
The Python SDK has a testing harness to test your Restate handlers.
This uses [Testcontainers](https://testcontainers.com/) to run a Restate Server in a Docker container and provides a client to let you test your Restate handlers.
## Setup
Install the package:
```bash theme={null}
pip install restate_sdk[harness]
```
## Testing handlers
If you have a service as follows:
```python {"CODE_LOAD::python/src/develop/testing.py#service"} theme={null}
import restate
greeter = restate.Service("greeter")
@greeter.handler()
async def greet(ctx: restate.Context, name: str) -> str:
return f"Hello {name}!"
app = restate.app(services=[greeter])
```
Then you can test it via:
```python {"CODE_LOAD::python/src/develop/testing.py#testing"} theme={null}
import restate
with restate.test_harness(app) as harness:
restate_client = harness.ingress_client()
print(restate_client.post("/greeter/greet", json="Alice").json())
```
# Rust SDK
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/rust
# Concurrent Tasks
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/ts/concurrent-tasks
Execute multiple tasks concurrently and gather results.
When building resilient applications, you often need to perform multiple operations in parallel to improve performance and user experience. Restate provides durable concurrency primitives that allow you to run tasks concurrently while maintaining deterministic execution during replays.
## When to use concurrent tasks
Use concurrent tasks when you need to:
* Call multiple external services simultaneously (e.g., fetching data from different APIs)
* Race multiple operations and use the first result (e.g., trying multiple LLM providers)
* Implement timeouts by racing an operation against a timer
* Perform batch operations where individual tasks can run in parallel
## Key benefits
* **Deterministic replay**: Restate logs the order of completion, ensuring consistent behavior during failures
* **Fault tolerance**: If your handler fails, tasks that were already completed will be replayed with their results, while pending tasks will be retried
## Parallelizing tasks
Start multiple durable operations concurrently by calling them without immediately awaiting:
```typescript {"CODE_LOAD::ts/src/develop/journaling_results.ts#parallel"} theme={null}
const call1 = ctx.run("fetch_user", async () =>
fetchUserData({ userId: 123 })
);
const call2 = ctx.run("fetch_orders", async () =>
fetchOrderHistory({ userId: 123 })
);
const call3 = ctx
.serviceClient(analyticsService)
.calculateMetrics({ userId: 123 });
const user = await call1;
const orders = await call2;
const metrics = await call3;
```
Check out the guide on [parallelizing work](guides/parallelizing-work).
## Retrieving results
Restate provides several patterns for coordinating concurrent tasks. All patterns use `RestatePromise` combinators that log the order of completion, ensuring deterministic behavior during replays.
### Wait for all tasks to complete
Similar to [`Promise.all`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all), waits for all promises to resolve successfully:
```typescript {"CODE_LOAD::ts/src/develop/journaling_results.ts#all"} theme={null}
const sleepPromise = ctx.sleep({ milliseconds: 100 });
const callPromise = ctx.serviceClient(myService).myHandler("Hi");
const externalCallPromise = ctx.run(() => httpCall());
const resultArray = await RestatePromise.all([
sleepPromise,
callPromise,
externalCallPromise,
]);
```
### Wait for the first successful completion
Similar to [`Promise.any`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any), returns the first promise that resolves successfully:
```typescript {"CODE_LOAD::ts/src/develop/journaling_results.ts#any"} theme={null}
const sleepPromise1 = ctx.sleep({ milliseconds: 100 });
const sleepPromise2 = ctx.sleep({ milliseconds: 200 });
const callPromise = ctx.serviceClient(myService).myHandler("Hi");
const firstResult = await RestatePromise.any([
sleepPromise1,
sleepPromise2,
callPromise,
]);
```
### Wait for the first to complete
Similar to [`Promise.race`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race), returns the first promise to settle (resolve or reject):
```typescript {"CODE_LOAD::ts/src/develop/journaling_results.ts#race"} theme={null}
const sleepPromise3 = ctx.sleep({ milliseconds: 100 });
const callPromise2 = ctx.serviceClient(myService).myHandler("Hi");
const firstToComplete = await RestatePromise.race([
sleepPromise3,
callPromise2,
]);
```
### Wait for all to settle
Similar to [`Promise.allSettled`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled), waits for all promises to complete regardless of success or failure:
```typescript {"CODE_LOAD::ts/src/develop/journaling_results.ts#allSettled"} theme={null}
const sleepPromise4 = ctx.sleep({ milliseconds: 100 });
const callPromise3 = ctx.serviceClient(myService).myHandler("Hi");
const externalCallPromise2 = ctx.run(() => httpCall());
const allResults = await RestatePromise.allSettled([
sleepPromise4,
callPromise3,
externalCallPromise2,
]);
```
# Durable Steps
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/ts/durable-steps
Persist results of operations.
Restate uses an execution log to replay operations after failures and suspensions. Non-deterministic operations (database calls, HTTP requests, UUID generation) must be wrapped to ensure deterministic replay.
## Run
Use `ctx.run` to safely wrap any non-deterministic operation, like HTTP calls or database responses, and have Restate store its result in the execution log.
```typescript {"CODE_LOAD::ts/src/develop/journaling_results.ts#side_effect"} theme={null}
const result = await ctx.run(async () => doDbRequest());
```
Note that inside `ctx.run`, you cannot use the Restate context (e.g., `ctx.get`, `ctx.sleep`, or nested `ctx.run`).
By default, results are serialized using [JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON). See the [serialization docs](/develop/ts/serialization) to customize this.
Failures in `ctx.run` are treated the same as any other handler error. Restate will retry it unless configured otherwise or unless a [`TerminalError`](/develop/ts/error-handling) is thrown.
You can customize how `ctx.run` retries via:
```typescript {"CODE_LOAD::ts/src/develop/retries.ts#here"} theme={null}
try {
const myRunRetryPolicy = {
initialRetryInterval: { milliseconds: 500 },
retryIntervalFactor: 2,
maxRetryInterval: { seconds: 1 },
maxRetryAttempts: 5,
maxRetryDuration: { seconds: 1 },
};
await ctx.run("write", () => writeToOtherSystem(), myRunRetryPolicy);
} catch (e) {
if (e instanceof restate.TerminalError) {
// Undo or compensate here (see Sagas guide)
}
throw e;
}
```
* You can limit retries by time or count
* When the policy is exhausted, a `TerminalError` is thrown
* See the [Error Handling Guide](/guides/error-handling) and the [Sagas Guide](/guides/sagas) for patterns like compensation
If Restate doesn't receive new journal entries from a service for more than one minute (by default), it will automatically abort the invocation and retry it.
However, some business logic can take longer to complete—for example, an LLM call that takes up to 3 minutes to respond.
In such cases, you can adjust the service’s [abort timeout and inactivity timeout](/services/configuration) settings to accommodate longer execution times.
For more information, see the [error handling guide](/guides/error-handling).
## Deterministic randoms
The SDK provides deterministic helpers for random values — seeded by the invocation ID — so they return the **same result on retries**.
### UUIDs
To generate stable UUIDs for things like idempotency keys:
```typescript {"CODE_LOAD::ts/src/develop/journaling_results.ts#uuid"} theme={null}
const uuid = ctx.rand.uuidv4();
```
Do not use this in cryptographic contexts.
### Random numbers
To generate a deterministic float between `0` and `1`:
```typescript {"CODE_LOAD::ts/src/develop/journaling_results.ts#random_nb"} theme={null}
const randomNumber = ctx.rand.random();
```
This behaves like `Math.random()` but is deterministically replayable.
### Deterministic time
To get the current millis since midnight, January 1, 1970, that is consistent across retries:
```typescript {"CODE_LOAD::ts/src/develop/journaling_results.ts#time"} theme={null}
const now = await ctx.date.now();
```
# Scheduling & Timers
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/ts/durable-timers
Durable timers, scheduled actions, and sleep, backed by Restate.
Restate provides durable, fault-tolerant timers that allow you to:
* [Sleep](#durable-sleep): Pause a handler for a given duration
* [Send delayed messages](#scheduling-async-tasks): Schedule handler invocations in the future
* [Set timeouts](#timers-and-timeouts) for async operations
* Implement patterns like [cron jobs](#cron-jobs)
## How it works
Restate tracks and manages timers, ensuring they survive failures and restarts.
For example, if a service is sleeping for 12 hours and fails after 8 hours, then Restate will make sure it only sleeps 4 hours more.
If your handler runs on function-as-a-service platforms like AWS Lambda, Restate suspends the handler while it is sleeping, to free up resources.
Since these platforms charge on execution time, this saves costs.
## Durable sleep
To pause a handler for a set duration:
```typescript {"CODE_LOAD::ts/src/develop/durable_timers.ts#sleep"} theme={null}
await ctx.sleep({ seconds: 10 });
```
There is no hard limit on how long you can sleep. You can sleep for months or even years, but keep in mind:
* If you sleep in an [exclusive handler](/foundations/handlers#handler-behavior) in a Virtual Object, all other calls to this object will be queued.
* You need to keep the deployment version until the invocation completes (see [versioning](/services/versioning)).
So for long sleeps, we recommend breaking this up into multiple handlers that call each other with [delayed messages](/develop/ts/service-communication#delayed-messages).
The Restate SDK calculates the wake-up time based on the delay you specify.
The Restate Server then uses this calculated time to wake up the handler.
If the Restate Server and the SDK have different system clocks, the sleep duration might not be accurate.
So make sure that the system clock of the Restate Server and the SDK have the same timezone and are synchronized.
A mismatch can cause timers to fire earlier or later than expected.
## Scheduling async tasks
To invoke a handler at a later time, use [delayed messages](/develop/ts/service-communication#delayed-messages).
In theory, you can schedule future work in Restate in two ways:
1. **Delayed messages** (recommended)
2. **Sleep + send** - sleeping in the current handler, then sending a message
At first sight, both approaches might seem to achieve similar results.
However, we recommend to use delayed messages for the following reasons:
* **Handler completes immediately**: The calling handler can finish execution and complete the invocation without waiting for the delay to finish
* **No Virtual Object blocking**: Other requests to the same Virtual Object can be processed during the delay period
* **Better for service versioning**: No long-running invocations that require keeping old service deployments around for a long time (see [service versioning](/services/versioning))
## Timers and timeouts
Most context actions are async actions that return a `RestatePromise`.
`RestatePromise` supports setting timeouts, allowing you to bound how long your code waits for an operation.
When an operation times out, it throws a `TimeoutError`.
```typescript {"CODE_LOAD::ts/src/develop/durable_timers.ts#timer"} theme={null}
try {
await ctx
.serviceClient(myService)
.myHandler("Hi")
.orTimeout({ seconds: 5 });
} catch (error) {
if (error instanceof restate.TimeoutError) {
console.error("Operation timed out:", error);
} else {
throw error; // Re-throw other errors
}
}
```
For alternative ways of racing async operations, have a look at the [`RestatePromise` Combinator docs](/develop/ts/concurrent-tasks).
## Cron jobs
Restate does not yet include native cron support, but you can implement your own cron scheduler using:
* Durable timers
* Virtual Objects
* A repeat loop or sleep-schedule pattern
Check out the guide on [implementing cron jobs](/guides/cron).
# Error Handling
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/ts/error-handling
Stop infinite retries with Terminal Errors.
Restate handles retries for failed invocations.
Check out the [Error Handling guide](/guides/error-handling) to learn more about how Restate handles transient errors, terminal errors, retries, and timeouts.
## Retry strategies
By default, Restate does infinite retries with an exponential backoff strategy.
Check out the [error handling guide](/guides/error-handling) to learn how to customize this.
## Terminal Errors
For failures for which you do not want retries, but instead want the invocation to end and the error message
to be propagated back to the caller, you can throw a **terminal error**.
You can throw a `TerminalError` with an optional HTTP status code and a message anywhere in your handler, as follows:
```typescript {"CODE_LOAD::ts/src/develop/error_handling.ts#terminal"} theme={null}
throw new TerminalError("Something went wrong.", { errorCode: 500 });
```
You can catch terminal errors, and build your control flow around it.
When you throw a terminal error, you might need to undo the actions you did earlier in your handler to make sure that your system remains in a consistent state.
Have a look at our [sagas guide](/guides/sagas) to learn more.
## Mapping errors to `TerminalError`
If you're using external libraries (e.g., for validation), you might want to automatically convert certain error types into terminal errors.
You can do this using the `asTerminalError` option in your [service configuration](/services/configuration).
For example, to fail with `TerminalError` for each `MyValidationError`, do the following:
```typescript {"CODE_LOAD::ts/src/develop/error_handling.ts#as_terminal"} theme={null}
class MyValidationError extends Error {}
const greeter = restate.service({
name: "greeter",
handlers: {
greet: async (ctx: restate.Context, name: string) => {
if (name.length === 0) {
throw new MyValidationError("Length too short");
}
return `Hello ${name}`;
},
},
options: {
asTerminalError: (err) => {
if (err instanceof MyValidationError) {
// My validation error is terminal
return new restate.TerminalError(err.message, { errorCode: 400 });
}
},
},
});
```
# External Events
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/ts/external-events
Handle external events and human-in-the-loop patterns with durable waiting primitives.
Sometimes your handlers need to pause and wait for external processes to complete. This is common in:
* **Human-in-the-loop workflows** (approvals, reviews, manual steps)
* **External system integration** (waiting for webhooks, async APIs)
* **AI agent patterns** (tool execution, human oversight)
This pattern is also known as the **callback** or **task token** pattern.
## Two Approaches
Restate provides two primitives for handling external events:
| Primitive | Use Case | Key Feature |
| -------------------- | -------------------------- | ------------------------------------ |
| **Awakeables** | Services & Virtual Objects | Unique ID-based completion |
| **Durable Promises** | Workflows only | Named promises for simpler signaling |
## How it works
Implementing this pattern in a distributed system is tricky, since you need to ensure that the handler can recover from failures and resume waiting for the external event.
Restate makes promises are durable and distributed. They survive crashes and can be resolved or rejected by any handler in the workflow.
To save costs on FaaS deployments, Restate lets the handler [suspend](/foundations/key-concepts#suspensions-on-faas) while awaiting the promise, and invokes it again when the result is available.
## Awakeables
**Best for:** Services and Virtual Objects where you need to coordinate with external systems.
### Creating and waiting for awakeables
1. **Create an awakeable** - Get a unique ID and promise
2. **Send the ID externally** - Pass the awakeable ID to your external system
3. **Wait for result** - Your handler [suspends](/foundations/key-concepts#suspensions-on-faas) until the external system responds
```ts {"CODE_LOAD::ts/src/develop/awakeable.ts#here"} theme={null}
// Create awakeable and get unique ID
const { id, promise } = ctx.awakeable();
// Send ID to external system (email, queue, webhook, etc.)
await ctx.run(() => requestHumanReview(name, id));
// Handler suspends here until external completion
const review = await promise;
```
Both awakeables and durable promises use built-in [JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON) for (de)serialization. Complex objects are automatically serialized/deserialized. See the [serialization docs](/develop/ts/serialization) for customization options.
Note that if you wait for an awakeable in an [exclusive handler](/foundations/handlers#handler-behavior) in a Virtual Object, all other calls to this object will be queued.
### Resolving/rejecting Awakeables
External processes complete awakeables in two ways:
* **Resolve** with success data → handler continues normally
* **Reject** with error reason → throws a [terminal error](/develop/ts/error-handling) in the waiting handler
#### Via SDK (from other handlers)
**Resolve:**
```ts {"CODE_LOAD::../snippets/ts/src/develop/awakeable.ts#resolve"} theme={null}
// Complete with success data
ctx.resolveAwakeable(id, { approved: true, comments: "Looks good!" });
```
**Reject:**
```ts {"CODE_LOAD::../snippets/ts/src/develop/awakeable.ts#reject"} theme={null}
// Complete with error
ctx.rejectAwakeable(id, "This cannot be reviewed.");
```
#### Via HTTP API
External systems can complete awakeables using Restate's HTTP API:
**Resolve with data:**
```shell theme={null}
curl localhost:8080/restate/awakeables/sign_1PePOqp/resolve \
--json '"Looks good!"'
```
**Reject with error:**
```shell theme={null}
curl localhost:8080/restate/awakeables/sign_1PePOqp/reject \
-H 'content-type: text/plain' \
-d 'Review rejected: insufficient documentation'
```
## Durable Promises
**Best for:** Workflows where you need to signal between different workflow handlers.
**Key differences from awakeables:**
* No ID management - use logical names instead
* Scoped to workflow execution lifetime
Use this for:
* Sending data to the run handler
* Have handlers wait for events emitted by the run handler
After a workflow's run handler completes, other handlers can still be called for up to 24 hours (default).
The results of resolved Durable Promises remain available during this time.
Update the retention time via the [service configuration](/services/configuration).
### Creating and waiting for promises
Wait for a promise by name:
```ts {"CODE_LOAD::../snippets/ts/src/develop/awakeable.ts#promise"} theme={null}
const review = await ctx.promise("review");
```
### Resolving/rejecting promises
Resolve/reject from any workflow handler:
```ts {"CODE_LOAD::../snippets/ts/src/develop/awakeable.ts#resolve_promise"} theme={null}
await ctx.promise("review").resolve(review);
```
### Complete workflow example
```ts expandable {"CODE_LOAD::../snippets/ts/src/develop/awakeable.ts#review"} theme={null}
restate.workflow({
name: "reviewWorkflow",
handlers: {
// Main workflow execution
run: async (ctx: WorkflowContext, documentId: string) => {
// Send document for review
await ctx.run(() => askReview(documentId));
// Wait for external review submission
const review = await ctx.promise("review");
// Process the review result
return processReview(documentId, review);
},
// External endpoint to submit reviews
submitReview: async (
ctx: restate.WorkflowSharedContext,
review: string
) => {
// Signal the waiting run handler
await ctx.promise("review").resolve(review);
},
},
});
```
### Two signaling patterns
**External → Workflow** (shown above): External handlers signal the run handler
* Use for human approvals, external API responses, manual interventions
* External handlers call the handler which resolves the promise
**Workflow → External**: Run handler signals other handlers waiting for workflow events
* Use for step completion notifications, status updates, result broadcasting
* Run handler resolves promises that external handlers are awaiting
## Best Practices
* **Use awakeables** for services/objects coordinating with external systems
* **Use durable promises** for workflow signaling
* **Always handle rejections** to gracefully manage failures
* **Include timeouts** for long-running external processes
# Logging
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/ts/logging
Configure the log level of your services.
The TypeScript SDK includes an internal logger to help you monitor and debug your services.
## Log Levels
You can control the verbosity of logs using environment variables:
* **Set the log level:**\
Use the `RESTATE_LOGGING` environment variable.\
Possible values: `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`.
* **Default log level:**
* `INFO` if `NODE_ENV=production`
* `DEBUG` otherwise
* **Verbose journal logging:**\
If you set `RESTATE_LOGGING=TRACE`, you can enable even more detailed journal logs with:\
`RESTATE_JOURNAL_LOGGING=TRACE`
## Console Logging
By default, using the Node.js console logger will print log statements repeatedly during replays.\
**To avoid duplicate logs during replays, use the Restate context logger.**
The context logger wraps the console and suppresses duplicate log statements during replays:
```typescript {"CODE_LOAD::ts/src/develop/logging.ts"} theme={null}
import * as restate from "@restatedev/restate-sdk";
const service = restate.object({
name: "Greeter",
handlers: {
greet: async (ctx: restate.ObjectContext, name: string) => {
ctx.console.info("This will not be printed again during replays");
ctx.console.debug("This will not be printed again during replays");
// Any other console logging method can be used
},
},
});
```
* The context logger uses the same log level filtering as described above.
* Use `ctx.console` for all logging inside handlers to ensure clean, non-redundant logs.
# Serialization
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/ts/serialization
Customize serialization for SDK actions.
Restate sends data over the network for storing state, journaling actions, awakeables, etc.
Therefore, Restate needs to serialize and deserialize the journal entries.
## Default serialization
By default, Typescript SDK uses the built-in [JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON) support to perform (de)serialization.
## Zod schemas
You can use [Zod](https://zod.dev/) to define the schema of your handler input/output.
Make sure to install the extra dependency for zod integration: [`@restatedev/restate-sdk-zod`](https://www.npmjs.com/package/@restatedev/restate-sdk-zod).
Then do the following:
```typescript {"CODE_LOAD::ts/src/develop/serialization.ts#zod"} theme={null}
import * as restate from "@restatedev/restate-sdk";
import { z } from "zod";
import { serde } from "@restatedev/restate-sdk-zod";
const Greeting = z.object({
name: z.string(),
});
const GreetingResponse = z.object({
result: z.string(),
});
const greeter = restate.service({
name: "Greeter",
handlers: {
greet: restate.handlers.handler(
{ input: serde.zod(Greeting), output: serde.zod(GreetingResponse) },
async (ctx: restate.Context, { name }) => {
return { result: `You said hi to ${name}!` };
}
),
},
});
```
## Custom serialization
It is possible to implement customized serialization using the `Serde` interface.
For example, to implement custom serializers for the handler input and output:
```typescript {"CODE_LOAD::ts/src/develop/serialization.ts#service_definition"} theme={null}
const myService = restate.service({
name: "MyService",
handlers: {
myHandler: restate.handlers.handler(
{
// Set the input serde here
input: restate.serde.binary,
// Set the output serde here
output: restate.serde.binary,
},
async (ctx: Context, data: Uint8Array): Promise => {
// Process the request
return data;
}
),
},
});
```
When sending a request to a handler configured with custom serde(s) you always need to manually specify them, because the client does not automatically infer what serde(s) should be used.
To customize the serde to use on requests:
```typescript {"CODE_LOAD::ts/src/develop/serialization.ts#client"} theme={null}
ctx.serviceClient(myService).myHandler(
input,
restate.rpc.opts({
input: restate.serde.binary,
output: restate.serde.binary,
})
);
```
To customize the serde for other actions on the context:
```typescript {"CODE_LOAD::ts/src/develop/serialization.ts#actions"} theme={null}
ctx.get("my-binary-data", restate.serde.binary);
ctx.set("my-binary-data", new Uint8Array(), restate.serde.binary);
ctx.awakeable(restate.serde.binary);
await ctx.run("my-side-effect", () => new Uint8Array(), {
serde: restate.serde.binary,
});
```
# Service Communication
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/ts/service-communication
Call other services from your handler.
Your Restate handler can call other handlers in three ways:
* **[Request-response calls](#request-response-calls)**: Call and wait for a response
* **[One-way messages](#sending-messages)**: Send a message and continue
* **[Delayed messages](#delayed-messages)**: Send a message after a delay
To call a service from an external application, see the [HTTP](/services/invocation/http), [Kafka](/services/invocation/kafka), or [SDK Clients](/services/invocation/clients/typescript-sdk) documentation.
[Learn how Restate how it works](/foundations/key-concepts#resilient-communication)
## Request-response calls
To call a Restate handler and wait for its result:
```ts {"CODE_LOAD::ts/src/develop/service_communication.ts#request_response"} theme={null}
// To call a Service:
const svcResponse = await ctx.serviceClient(myService).myHandler("Hi");
// To call a Virtual Object:
const objResponse = await ctx
.objectClient(myObject, "Mary")
.myHandler("Hi");
// To call a Workflow:
// `run` handler — can only be called once per workflow ID
const wfResponse = await ctx
.workflowClient(myWorkflow, "wf-id")
.run("Hi");
// Other handlers can be called anytime within workflow retention
const result = await ctx
.workflowClient(myWorkflow, "wf-id")
.interactWithWorkflow();
```
Use generic calls when you don't have the service definition or need dynamic service names:
```ts {"CODE_LOAD::ts/src/develop/service_communication.ts#generic_call"} theme={null}
const response = await ctx.genericCall({
service: "MyObject",
method: "myHandler",
parameter: "Hi",
key: "Mary", // drop this for Service calls
inputSerde: restate.serde.json,
outputSerde: restate.serde.json,
});
```
After a workflow's run handler completes, other handlers can still be called for up to 24 hours (default).
Update this via the [service configuration](/services/configuration).
Request-response calls between [exclusive handlers](/foundations/handlers#handler-behavior) of Virtual Objects may lead to deadlocks:
* Cross deadlock: A → B and B → A (same keys).
* Cycle deadlock: A → B → C → A.
Use the UI or CLI to [cancel](/services/invocation/managing-invocations#cancel) and unblock deadlocked invocations.
## Sending messages
To send a message to another Restate handler without waiting for a response:
```ts {"CODE_LOAD::ts/src/develop/service_communication.ts#one_way"} theme={null}
// To message a Service:
ctx.serviceSendClient(myService).myHandler("Hi");
// To message a Virtual Object:
ctx.objectSendClient(myObject, "Mary").myHandler("Hi");
// To message a Workflow:
// `run` handler — can only be called once per workflow ID
ctx.workflowSendClient(myWorkflow, "wf-id").run("Hi");
// Other handlers can be called anytime within workflow retention
ctx.workflowSendClient(myWorkflow, "wf-id").interactWithWorkflow();
```
Restate handles message delivery and retries, so the handler can complete and return without waiting for the message to be processed.
Use generic send when you don't have the service definition:
```ts {"CODE_LOAD::ts/src/develop/service_communication.ts#generic_send"} theme={null}
ctx.genericSend({
service: "MyService",
method: "myHandler",
parameter: "Hi",
inputSerde: restate.serde.json,
});
```
Calls to a Virtual Object execute in order of arrival, serially.
Example:
```typescript {"CODE_LOAD::ts/src/develop/service_communication.ts#ordering"} theme={null}
ctx.objectSendClient(myObject, "Mary").myHandler("I'm call A");
ctx.objectSendClient(myObject, "Mary").myHandler("I'm call B");
```
Call A is guaranteed to execute before B. However, other invocations may interleave between A and B.
## Delayed messages
To send a message after a delay:
```ts {"CODE_LOAD::ts/src/develop/service_communication.ts#delayed"} theme={null}
// To message a Service with a delay:
ctx
.serviceSendClient(myService)
.myHandler("Hi", restate.rpc.sendOpts({ delay: { hours: 5 } }));
// To message a Virtual Object with a delay:
ctx
.objectSendClient(myObject, "Mary")
.myHandler("Hi", restate.rpc.sendOpts({ delay: { hours: 5 } }));
// To message a Workflow with a delay:
ctx
.workflowSendClient(myWorkflow, "Mary")
.run("Hi", restate.rpc.sendOpts({ delay: { hours: 5 } }));
```
Use generic send with a delay when you don't have the service definition:
```ts {"CODE_LOAD::ts/src/develop/service_communication.ts#generic_delayed"} theme={null}
ctx.genericSend({
service: "MyService",
method: "myHandler",
parameter: "Hi",
inputSerde: restate.serde.json,
delay: { seconds: 5 },
});
```
Learn [how this is different](/develop/ts/durable-timers#scheduling-async-tasks) from sleeping and then sending a message.
## Using an idempotency key
To prevent duplicate executions of the same call, add an idempotency key:
```ts {"CODE_LOAD::ts/src/develop/service_communication.ts#idempotency_key"} theme={null}
// For request-response
const response = await ctx.serviceClient(myService).myHandler(
"Hi",
restate.rpc.opts({
idempotencyKey: "my-idempotency-key",
})
);
// For sending a message
ctx.serviceSendClient(myService).myHandler(
"Hi",
restate.rpc.sendOpts({
idempotencyKey: "my-idempotency-key",
})
);
```
Restate automatically deduplicates calls made during the same handler execution, so there's no need to provide an idempotency key in that case.
However, if multiple handlers might call the same service independently, you can use an idempotency key to ensure deduplication across those calls.
## Attach to an invocation
To wait for or get the result of a previously sent message:
```ts {"CODE_LOAD::ts/src/develop/service_communication.ts#attach"} theme={null}
const handle = ctx.serviceSendClient(myService).myHandler(
"Hi",
restate.rpc.sendOpts({
idempotencyKey: "my-idempotency-key",
})
);
const invocationId = await handle.invocationId;
// Later...
const response = ctx.attach(invocationId);
```
* With an idempotency key: Wait for completion and retrieve the result.
* Without an idempotency key: Can only wait, not retrieve the result.
## Cancel an invocation
To cancel a running handler:
```ts {"CODE_LOAD::ts/src/develop/service_communication.ts#cancel"} theme={null}
const handle = ctx.serviceSendClient(myService).myHandler("Hi");
const invocationId = await handle.invocationId;
// Cancel the invocation
ctx.cancel(invocationId);
```
## Advanced: sharing service type definitions
When calling services, you need access to their type definitions for type safety. Here are four approaches to share service definitions without exposing implementation details:
Export only the service type information from where you define your service:
```typescript {"CODE_LOAD::ts/src/develop/my_service.ts#api_export"} theme={null}
export const myService = restate.service({
name: "MyService",
handlers: {
myHandler: async (ctx: restate.Context, greeting: string) => {
return `${greeting}!`;
},
},
});
export type MyService = typeof myService;
```
Import and use this definition in calling handlers:
```ts {"CODE_LOAD::ts/src/develop/service_communication.ts#export_definition"} theme={null}
const response = await ctx
.serviceClient({ name: "MyService" })
.myHandler("Hi");
```
Create a TypeScript interface matching the service's handler signatures. Useful when the service is in a different language or when you can't import the type definition:
```ts {"CODE_LOAD::ts/src/develop/service_communication.ts#interface"} theme={null}
interface MyService {
myHandler(ctx: unknown, greeting: string): Promise;
}
const response = await ctx
.serviceClient({ name: "MyService" })
.myHandler("Hi");
```
When your service is published as a separate package, import it as a dev dependency to access its types:
```ts {"CODE_LOAD::ts/src/develop/service_communication.ts#dev_dependency"} theme={null}
// import type { MyService } from "my-service-package";
const response = await ctx
.serviceClient({ name: "MyService" })
.myHandler("Hi");
```
## See also
* **[SDK Clients](/develop/ts/service-communication)**: Call Restate services from external applications
* **[Error Handling](/develop/ts/error-handling)**: Handle failures and terminal errors in service calls
* **[Durable Timers](/develop/ts/durable-timers)**: Implement timeouts for your service calls
* **[Serialization](/develop/ts/serialization)**: Customize how data is serialized between services
* **[Sagas](/guides/sagas)**: Roll back or compensate for canceled service calls.
# Services
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/ts/services
Implementing Restate services with the TypeScript SDK.
The Restate TypeScript SDK is open source and available on [GitHub](https://github.com/restatedev/sdk-typescript).
The Restate SDK lets you implement **handlers**. Handlers can be part of a **[Basic Service](/foundations/services#basic-service)**, a **[Virtual Object](/foundations/services#virtual-object)**, or a **[Workflow](/foundations/services#workflow)**. This page shows how to define them with the TypeScript SDK.
## Prerequisites
* [NodeJS](https://nodejs.org/en/) >= v20 or [Bun](https://bun.sh/docs/installation) or [Deno](https://deno.land/#installation)
* [npm CLI](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) >= 9.6.7
## Getting started
Get started quickly with the [TypeScript Quickstart](/quickstart).
Add the [`@restatedev/restate-sdk`](https://www.npmjs.com/package/@restatedev/restate-sdk) dependency to your project to start developing Restate services.
## Basic Services
[Basic Services](/foundations/services) group related **handlers** and expose them as callable endpoints:
```ts {"CODE_LOAD::ts/src/develop/service.ts"} theme={null}
import * as restate from "@restatedev/restate-sdk";
export const myService = restate.service({
name: "MyService",
handlers: {
myHandler: async (ctx: restate.Context, greeting: string) => {
return `${greeting}!`;
},
},
});
restate.serve({ services: [myService] });
```
* Define a service using [`restate.service`](https://restatedev.github.io/sdk-typescript/functions/_restatedev_restate-sdk.service).
* The service has a name and a list of handlers.
* Each handler has a name and can be called at `/myService/myHandler`
* Handlers take the [`Context`](https://restatedev.github.io/sdk-typescript/interfaces/_restatedev_restate-sdk.Context) as the first argument.
* Handlers can take one optional JSON-serializable input and must return a JSON-serializable output (see [custom serialization](/develop/ts/serialization) for advanced types).
* Serve the service over HTTP (port `9080` by default).
## Virtual Objects
[Virtual Objects](/foundations/services) are services that are stateful and key-addressable — each object instance has a unique ID and persistent state.
```ts {"CODE_LOAD::ts/src/develop/virtual_object.ts"} theme={null}
import * as restate from "@restatedev/restate-sdk";
export const myObject = restate.object({
name: "MyObject",
handlers: {
myHandler: async (ctx: restate.ObjectContext, greeting: string) => {
return `${greeting} ${ctx.key}!`;
},
myConcurrentHandler: restate.handlers.object.shared(
async (ctx: restate.ObjectSharedContext, greeting: string) => {
return `${greeting} ${ctx.key}!`;
}
),
},
});
restate.serve({ services: [myObject] });
```
* Define a Virtual Object using [`restate.object(...)`](https://restatedev.github.io/sdk-typescript/functions/_restatedev_restate-sdk.object)
* Each instance is identified by a key (accessible via `ctx.key`).
* Virtual Objects can have [exclusive and shared handlers](/foundations/handlers#handler-behavior).
* Exclusive handlers receive an [`ObjectContext`](https://restatedev.github.io/sdk-typescript/interfaces/_restatedev_restate-sdk.ObjectContext), allowing read/write access to object state.
* Shared handlers are wrapped in `handlers.object.shared(...)` and use the [`ObjectSharedContext`](https://restatedev.github.io/sdk-typescript/interfaces/_restatedev_restate-sdk.ObjectSharedContext)
* Serve the Virtual Object over HTTP (port `9080` by default).
## Workflows
[Workflows](/foundations/services) are long-lived processes with a defined lifecycle. They run once per key and are ideal for orchestrating multi-step operations, which require external interaction via signals and queries.
```ts {"CODE_LOAD::ts/src/develop/workflow.ts"} theme={null}
import * as restate from "@restatedev/restate-sdk";
export const myWorkflow = restate.workflow({
name: "MyWorkflow",
handlers: {
run: async (ctx: restate.WorkflowContext, req: string) => {
// implement workflow logic here
return "success";
},
interactWithWorkflow: async (ctx: restate.WorkflowSharedContext) => {
// implement interaction logic here
// e.g. resolve a promise that the workflow is waiting on
},
},
});
restate.serve({ services: [myWorkflow] });
```
* Define a workflow with [`restate.workflow(...)`](https://restatedev.github.io/sdk-typescript/functions/_restatedev_restate-sdk.workflow)
* Every workflow **must** include a `run` handler:
* This is the main orchestration entry point
* It runs exactly once per workflow execution and uses the [`WorkflowContext`](https://restatedev.github.io/sdk-typescript/interfaces/_restatedev_restate-sdk.WorkflowContext)
* Resubmission of the same workflow will fail with "Previously accepted". The invocation ID can be found in the request header `x-restate-id`.
* Use `ctx.key` to access the workflow's unique ID
* Additional handlers must use the [`WorkflowSharedContext`](https://restatedev.github.io/sdk-typescript/interfaces/_restatedev_restate-sdk.WorkflowSharedContext) and can signal or query the workflow. They can run concurrently with the `run` handler and until the retention time expires.
* Serve the Workflow over HTTP (port `9080` by default).
## Configuring services
Check out the [service configuration docs](/services/configuration) to learn how to configure service behavior, including timeouts and retention policies.
# Serving
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/ts/serving
Create an endpoint to serve your services.
Restate services can run in a few ways: as a Node.js HTTP handler, as an AWS
Lambda handler, or on other Javascript runtimes like Bun, Deno and Cloudflare
Workers.
## Creating a Node.js HTTP handler
Use `restate.serve` to serve the provided services, starting an HTTP/2 server on port `9080`.
```typescript {"CODE_LOAD::ts/src/develop/serving.ts#endpoint"} theme={null}
import * as restate from "@restatedev/restate-sdk";
restate.serve({
services: [myService, myVirtualObject, myWorkflow],
});
```
If you need to manually control or customize the HTTP2 server, use `restate.createEndpointHandler` to create a Node HTTP/2 handler, and then use it to manually instantiate the HTTP server:
```ts {"CODE_LOAD::ts/src/develop/serving.ts#custom_endpoint"} theme={null}
const http2Handler = restate.createEndpointHandler({
services: [myService, myVirtualObject, myWorkflow],
});
const httpServer = http2.createServer(http2Handler);
httpServer.listen();
```
## Creating a Lambda handler
To register your service as a Lambda function, use the `/lambda` import
component and use `restate.createEndpointHandler`:
```typescript {"CODE_LOAD::ts/src/develop/serving_lambda.ts#lambda"} theme={null}
import * as restate from "@restatedev/restate-sdk/lambda";
export const handler = restate.createEndpointHandler({
services: [myService, myVirtualObject, myWorkflow],
});
```
The implementation of your services and handlers remains the same for both deployment options.
Have a look at the [deployment section](/services/deploy/lambda)
for guidance on how to deploy your services on AWS Lambda.
## Creating a Deno/Cloudflare Workers handler
Other Javascript runtimes like Deno and Cloudflare Workers have
built on top of the [Fetch Standard](https://github.com/whatwg/fetch) for
defining HTTP server handlers. To register your service as a fetch handler, use
the `/fetch` import component.
```typescript {"CODE_LOAD::ts/src/develop/serving_fetch.ts#fetch"} theme={null}
import * as restate from "@restatedev/restate-sdk/fetch";
const handler = restate.createEndpointHandler({
services: [myService, myVirtualObject, myWorkflow],
});
// Cloudflare expects the handler as a default export
export default handler;
// Deno expects to be passed the fetch function
Deno.serve({ port: 9080 }, handler);
```
By default, a fetch handler will not advertise itself as working
bidirectionally; the SDK will end the HTTP request at each suspension point,
and the Restate runtime will re-invoke the service when there is more work to
do.
However, you can use the option `bidirectional: true` to change this on supported platforms,
which will improve latencies once the service is re-registered with the runtime.
* Deno (including Deno Deploy) supports HTTP2 and therefore bidirectional mode can be enabled.
* Cloudflare Workers do not support end-to-end HTTP2 or bidirectional HTTP1.1,
and enabling bidirectional mode will cause invocations to stall and time out.
Services running on Workers must be discovered with the `--use-http1.1`
CLI flag.
Cloudflare Workers minification is not working correctly with the Restate SDK. If you see an issue similar to:
```
✘ [ERROR] restate-cloudflare-worker: Uncaught TypeError: Cannot read properties of undefined (reading '__wbindgen_malloc')
```
Then most likely you have enabled minification when deploying. Disable it with `minify = false` in your `workers.toml` file.
## Validating request identity
SDKs can validate that incoming requests come from a particular Restate
instance. You can find out more about request identity in the [Security docs](/services/security#locking-down-service-access)
```typescript {"CODE_LOAD::ts/src/develop/serving.ts#identity"} theme={null}
restate.serve({
services: [myService],
identityKeys: ["publickeyv1_w7YHemBctH5Ck2nQRQ47iBBqhNHy4FV7t2Usbye2A6f"],
});
```
# State
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/ts/state
Store key-value state in Restate.
Restate lets you persist key-value (K/V) state using its embedded K/V store.
## Key characteristics
[Learn about Restate's embedded K/V store](/foundations/key-concepts#consistent-state).
**State is only available for Virtual Objects and Workflows.**
Scope & retention:
* For Virtual Objects: State is scoped per object key and retained indefinitely. It is persisted and shared across all invocations for that object until explicitly cleared.
* For Workflows: State is scoped per workflow execution (workflow ID) and retained only for the duration of the workflow’s configured retention time.
Access Rules:
* [Exclusive handlers](/foundations/handlers#handler-behavior) (e.g., `run()` in workflows) can read and write state.
* [Shared handlers](/foundations/handlers#handler-behavior) can only read state and cannot mutate it.
You can inspect and edit the K/V state via the UI and the [CLI](/services/introspection#inspecting-application-state).
## List all state keys
To retrieve all keys for which the current Virtual Object has stored state:
```typescript {"CODE_LOAD::ts/src/develop/state.ts#statekeys"} theme={null}
const stateKeys = ctx.stateKeys();
```
## Get state value
To read a value by key:
```typescript {"CODE_LOAD::ts/src/develop/state.ts#get"} theme={null}
const myString = (await ctx.get("my-string-key")) ?? "my-default";
const myNumber = (await ctx.get("my-number-key")) ?? 0;
```
Returns null if the key doesn't exist.
See the [serialization docs](/develop/ts/serialization) to customize the state serializer.
## Set state value
To write or update a value:
```typescript {"CODE_LOAD::ts/src/develop/state.ts#set"} theme={null}
ctx.set("my-key", "my-new-value");
```
## Clear state key
To delete a specific key:
```typescript {"CODE_LOAD::ts/src/develop/state.ts#clear"} theme={null}
ctx.clear("my-key");
```
## Clear all state keys
To remove all stored state for the current Virtual Object:
```typescript {"CODE_LOAD::ts/src/develop/state.ts#clear_all"} theme={null}
ctx.clearAll();
```
## Advanced: Eager vs. lazy state loading
Restate supports two modes for loading state in handlers:
### Eager state (default)
* **How it works**: State is automatically sent with the request when invoking a handler
* **Benefits**: State is available immediately when the handler starts executing
* **Behavior**: All reads and writes to state are local to the handler execution
* **Best for**: Small to medium state objects that are frequently accessed
### Lazy state
* **How it works**: State is fetched on-demand on `get` calls from the Restate Server
* **Benefits**: Reduces initial request size and memory usage
* **Setup**: Enable lazy state in the [service or handler configuration](/services/configuration)
* **Best for**: Large state objects that aren't needed in every handler execution
## Advanced: Typed State
You can specify the types of your Object's K/V state, to ensure type safety and better developer experience:
```typescript {"CODE_LOAD::ts/src/develop/state.ts#typed_state"} theme={null}
type GreeterContext = restate.ObjectContext<{
name: string;
count: number;
}>;
export const greeter = restate.object({
name: "Greeter",
handlers: {
greet: async (ctx: GreeterContext, name: string) => {
const count: number = (await ctx.get("count")) ?? 0;
ctx.set("name", name);
},
},
});
```
# Testing
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/develop/ts/testing
Utilities to test your handler logic.
The Typescript SDK has a companion library which makes it easy to test against a Restate container:
[`@restatedev/restate-sdk-testcontainers`](https://www.npmjs.com/package/@restatedev/restate-sdk-testcontainers).
This uses [Testcontainers](https://testcontainers.com/) to run a Restate Server in a Docker container and let you test your Restate handlers.
## Setup
`RestateTestEnvironment.start` creates a Restate container and executes a user-provided closure to register services.
An optional second argument allows you to specify a custom [Testcontainer](https://node.testcontainers.org/) for Restate.
```typescript {"CODE_LOAD::ts/src/develop/testing.test.ts#setup"} theme={null}
describe("ExampleObject", () => {
// import { RestateTestEnvironment } from "@restatedev/restate-sdk-testcontainers";
let restateTestEnvironment: RestateTestEnvironment;
// import * as clients from "@restatedev/restate-sdk-clients";
let restateIngress: clients.Ingress;
beforeAll(async () => {
restateTestEnvironment = await RestateTestEnvironment.start({services: [router]});
restateIngress = clients.connect({ url: restateTestEnvironment.baseUrl() });
}, 20_000);
afterAll(async () => {
if (restateTestEnvironment !== undefined) {
await restateTestEnvironment.stop();
}
});
```
## Calling services
The Restate ingress client can be used as usual (see the [clients documentation](/services/invocation/clients/typescript-sdk))
```typescript {"CODE_LOAD::ts/src/develop/testing.test.ts#methods"} theme={null}
it("Can call methods", async () => {
const client = restateIngress.objectClient(router, "myKey");
await client.greet("Test!");
});
```
## Checking and mutating state
The `stateOf` method on the `RestateTestEnvironment` class can be used to obtain a handle on the Virtual Object / Workflow state
for a particular key.
```typescript {"CODE_LOAD::ts/src/develop/testing.test.ts#state"} theme={null}
it("Can read state", async () => {
const state = restateTestEnvironment.stateOf(router, "myKey");
expect(await state.getAll()).toStrictEqual({});
expect(await state.get("count")).toBeNull();
});
it("Can write state", async () => {
const state = restateTestEnvironment.stateOf(router, "myKey");
await state.setAll({
count: 123,
});
await state.set("count", 321);
});
```
## Typed state
`stateOf` can be provided with a type for the services state, to allow for type-safe state operations.
```typescript {"CODE_LOAD::ts/src/develop/testing.test.ts#typedstate"} theme={null}
type ServiceState = { count: number };
it("Can operate on typed state", async () => {
const state = restateTestEnvironment.stateOf(router, "myKey");
await state.setAll({ count: 1 });
await state.set("count", 2);
});
```
# Documentation
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/docs
Build, deploy, and operate resilient applications with Restate
Complete reference for building, deploying, and operating resilient applications with Restate.
## Build your services
Choose your SDK and start building:
## Deploy and operate your services
Deploy to Kubernetes, AWS Lambda, Vercel, Cloudflare Workers, or Deno Deploy
Call services via HTTP, SDK clients, or Kafka events
Manage service versions and compatibility
Query system state and inspect running services
## Hosting Restate
Choose between managed cloud or self-hosted deployment:
**Managed platform** with instant setup, automatic scaling, and built-in monitoring.
Perfect for getting started quickly without infrastructure management.
**Full control** over your infrastructure with flexible deployment options.
Single node, cluster, and Kubernetes deployment available.
## References
System design • [Server config](/references/server-config)
[TypeScript](https://restatedev.github.io/sdk-typescript) • [Java](https://restatedev.github.io/sdk-java/javadocs) • [Kotlin](https://restatedev.github.io/sdk-java/ktdocs) • [Go](https://pkg.go.dev/github.com/restatedev/sdk-go)
## New to Restate?
Build your first service
[AI agents](/use-cases/ai-agents) • [Microservices](/use-cases/microservice-orchestration) • [Workflows](/use-cases/workflows) • [Event Processing](/use-cases/event-processing)
Core concepts and building blocks
# Examples
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/examples
# Actions
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/foundations/actions
Essential context actions for building reliable handlers
Context actions are methods available on the Restate Context object (`ctx`) that provide Restate's core capabilities.
These actions enable durable execution, state management, service communication, and timing control.
### Durable steps
Use `run` to safely wrap any non-deterministic operation, like HTTP calls or database responses, and have Restate persist its result.
```typescript TypeScript {"CODE_LOAD::ts/src/foundations/actions/actions.ts#durable_steps"} theme={null}
// External API call
const apiResult = await ctx.run("fetch-data", async () => {
const response = await fetch("https://api.example.com/data");
return response.json();
});
// Database operation
const dbResult = await ctx.run("update-user", () => {
return updateUserDatabase(userId, { name: "John" });
});
// Idempotency key generation
const id = ctx.rand.uuidv4();
```
```java Java {"CODE_LOAD::java/src/main/java/foundations/actions/Actions.java#durable_steps"} theme={null}
// External API call
String apiResult = ctx.run(String.class, () -> fetchData("https://api.example.com/data"));
// Database operation
boolean dbResult = ctx.run(Boolean.class, () -> updateUserDatabase(userId, user));
// Idempotency key generation
String id = ctx.random().nextUUID().toString();
```
```python Python {"CODE_LOAD::python/src/foundations/actions/actions.py#durable_steps"} theme={null}
# External API call
api_result = await ctx.run_typed(
"fetch-data", fetch_url, url="https://api.example.com/data"
)
# Database operation
db_result = await ctx.run_typed(
"update-user", update_user_database, id=user_id, data={"name": "John"}
)
# Idempotency key generation
id = ctx.uuid()
```
```go Go {"CODE_LOAD::go/foundations/actions/actions.go#durable_steps"} theme={null}
// External API call
apiResult, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
return fetchData("https://api.example.com/data")
})
if err != nil {
return err
}
// Database operation
success, err := restate.Run(ctx, func(ctx restate.RunContext) (bool, error) {
return updateUserDatabase(userId, user)
})
if err != nil {
return err
}
// Idempotency key generation
id := restate.Rand(ctx).UUID().String()
```
Without `run()`, these operations would produce different results during replay, breaking deterministic recovery.
## State management
Available in Virtual Object and Workflow handlers for persistent key-value storage.
### Get
Retrieve stored state by key.
```typescript TypeScript {"CODE_LOAD::ts/src/foundations/actions/actions.ts#state_get"} theme={null}
// Get with type and default value
const profile = await ctx.get("profile");
const count = (await ctx.get("count")) ?? 0;
const cart = (await ctx.get("cart")) ?? [];
```
```java Java {"CODE_LOAD::java/src/main/java/foundations/actions/StateExample.java#state_get"} theme={null}
// Get with type and default value
UserProfile profile = ctx.get(PROFILE).orElse(null);
int count = ctx.get(COUNT).orElse(0);
ShoppingCart cart = ctx.get(CART).orElse(new ShoppingCart());
```
```python Python {"CODE_LOAD::python/src/foundations/actions/actions.py#state_get"} theme={null}
# Get with type and default value
profile = await ctx.get("profile", type_hint=UserProfile)
count = await ctx.get("count", type_hint=int) or 0
cart = await ctx.get("cart", type_hint=ShoppingCart) or ShoppingCart()
```
```go Go {"CODE_LOAD::go/foundations/actions/actions.go#state_get"} theme={null}
// Get with type and default value
profile, err := restate.Get[UserProfile](ctx, "profile")
if err != nil {
return err
}
count, err := restate.Get[int](ctx, "count")
if err != nil {
return err
}
cart, err := restate.Get[ShoppingCart](ctx, "cart")
if err != nil {
return err
}
```
### Set
Store state that persists across function invocations.
```typescript TypeScript {"CODE_LOAD::ts/src/foundations/actions/actions.ts#state_set"} theme={null}
// Store simple values
ctx.set("lastLogin", request.date);
ctx.set("count", count + 1);
// Store complex objects
ctx.set("profile", {
name: "John Doe",
email: "john@example.com",
});
```
```java Java {"CODE_LOAD::java/src/main/java/foundations/actions/StateExample.java#state_set"} theme={null}
// Store simple values
ctx.set(COUNT, count + 1);
ctx.set(LAST_LOGIN, request.date());
// Store complex objects
ctx.set(PROFILE, new UserProfile("John Doe", "john@example.com"));
```
```python Python {"CODE_LOAD::python/src/foundations/actions/actions.py#state_set"} theme={null}
# Store simple values
ctx.set("lastLogin", request["date"])
ctx.set("count", count + 1)
# Store complex objects
ctx.set("profile", UserProfile(name="John Doe", email="john@example.com"))
```
```go Go {"CODE_LOAD::go/foundations/actions/actions.go#state_set"} theme={null}
// Store simple values
restate.Set(ctx, "lastLogin", request.Date)
restate.Set(ctx, "count", count+1)
// Store complex objects
restate.Set(ctx, "profile", UserProfile{
Name: "John Doe",
Email: "john@example.com",
})
```
### Clear
State is retained indefinitely for Virtual Objects, or for the configured retention period for Workflows.
To clear state:
```typescript TypeScript {"CODE_LOAD::ts/src/foundations/actions/actions.ts#state_clear"} theme={null}
// Clear specific keys
ctx.clear("shoppingCart");
ctx.clear("sessionToken");
// Clear all user data
ctx.clearAll();
```
```java Java {"CODE_LOAD::java/src/main/java/foundations/actions/StateExample.java#state_clear"} theme={null}
// Clear specific keys
ctx.clear(StateKey.of("shoppingCart", ShoppingCart.class));
ctx.clear(StateKey.of("sessionToken", String.class));
// Clear all user data
ctx.clearAll();
```
```python Python {"CODE_LOAD::python/src/foundations/actions/actions.py#state_clear"} theme={null}
# Clear specific keys
ctx.clear("shoppingCart")
ctx.clear("sessionToken")
# Clear all user data
ctx.clear_all()
```
## Service communication
### Request-response calls
Make request-response calls to other services. Your function waits for the result.
```typescript TypeScript {"CODE_LOAD::ts/src/foundations/actions/actions.ts#service_calls"} theme={null}
// Call another service
const validation = await ctx
.serviceClient(ValidationService)
.validateOrder(order);
// Call Virtual Object function
const profile = await ctx.objectClient(UserAccount, userId).getProfile();
// Submit Workflow
const result = await ctx
.workflowClient(OrderWorkflow, orderId)
.run(order);
```
```java Java {"CODE_LOAD::java/src/main/java/foundations/actions/Actions.java#service_calls"} theme={null}
// Call another service
var validation = ValidationServiceClient.fromContext(ctx).validateOrder(req.order()).await();
// Call Virtual Object function
var profile = UserAccountClient.fromContext(ctx, req.userId()).getProfile().await();
// Submit Workflow
var result = OrderWorkflowClient.fromContext(ctx, req.orderId()).run(req.order()).await();
```
```python Python {"CODE_LOAD::python/src/foundations/actions/actions.py#service_calls"} theme={null}
# Call another service
from validation_service import validate_order
validation = await ctx.service_call(validate_order, order)
# Call Virtual Object function
from user_account import get_profile
profile = await ctx.object_call(get_profile, key=user_id, arg=None)
# Submit Workflow
from order_workflow import run
result = await ctx.workflow_call(run, key=order_id, arg=order)
```
```go Go {"CODE_LOAD::go/foundations/actions/actions.go#service_calls"} theme={null}
// Call another service
validation, err := restate.Service[bool](ctx, "ValidationService", "ValidateOrder").Request(order)
if err != nil {
return err
}
// Call Virtual Object function
profile, err := restate.Object[UserProfile](ctx, "UserAccount", userId, "GetProfile").Request(restate.Void{})
if err != nil {
return err
}
// Submit Workflow
_, err = restate.Workflow[restate.Void](ctx, "OrderWorkflow", orderId, "Run").Request(order)
if err != nil {
return err
}
```
### Sending messages
Make one-way calls that don't return results. Your function continues immediately.
```typescript TypeScript {"CODE_LOAD::ts/src/foundations/actions/actions.ts#sending_messages"} theme={null}
// Fire-and-forget notification
ctx
.serviceSendClient(NotificationService)
.sendEmail({ userId, message: "Welcome!" });
// Background analytics
ctx
.serviceSendClient(AnalyticsService)
.recordEvent({ kind: "user_signup", userId });
// Cleanup task
ctx.objectSendClient(ShoppingCartObject, userId).emtpyExpiredCart();
```
```java Java {"CODE_LOAD::java/src/main/java/foundations/actions/Actions.java#sending_messages"} theme={null}
// Fire-and-forget notification
NotificationServiceClient.fromContext(ctx)
.send()
.sendEmail(new EmailRequest(userId, "Welcome!"));
// Background analytics
AnalyticsServiceClient.fromContext(ctx).send().recordEvent(event);
// Cleanup task
ShoppingCartObjectClient.fromContext(ctx, userId).send().emptyExpiredCart();
```
```python Python {"CODE_LOAD::python/src/foundations/actions/actions.py#sending_messages"} theme={null}
# Fire-and-forget notification
from notification_service import send_email
ctx.service_send(send_email, {"userId": user_id, "message": "Welcome!"})
# Background analytics
from analytics_service import record_event
ctx.service_send(record_event, {"kind": "user_signup", "userId": user_id})
# Cleanup task
from shopping_cart_object import empty_expired_cart
ctx.object_send(empty_expired_cart, key=user_id, arg=None)
```
```go Go {"CODE_LOAD::go/foundations/actions/actions.go#sending_messages"} theme={null}
// Fire-and-forget notification
restate.ServiceSend(ctx, "NotificationService", "SendEmail").Send(message)
// Background analytics
restate.ServiceSend(ctx, "AnalyticsService", "RecordEvent").Send(event)
// Cleanup task
restate.ObjectSend(ctx, "ShoppingCartObject", userId, "EmptyExpiredCart").Send(restate.Void{})
```
### Delayed messages
Schedule handlers to run in the future.
```typescript TypeScript {"CODE_LOAD::ts/src/foundations/actions/actions.ts#delayed_messages"} theme={null}
// Schedule reminder for tomorrow
ctx.serviceSendClient(ReminderService).sendReminder(
{ userId, message },
sendOpts({
delay: { days: 1 },
})
);
```
```java Java {"CODE_LOAD::java/src/main/java/foundations/actions/Actions.java#delayed_messages"} theme={null}
// Schedule reminder for tomorrow
ReminderServiceClient.fromContext(ctx).send().sendReminder(reminderRequest, Duration.ofDays(1));
```
```python Python {"CODE_LOAD::python/src/foundations/actions/actions.py#delayed_messages"} theme={null}
# Schedule reminder for tomorrow
from notification_service import send_reminder
ctx.service_send(
send_reminder,
{"userId": user_id, "message": message},
send_delay=timedelta(days=1),
)
```
```go Go {"CODE_LOAD::go/foundations/actions/actions.go#delayed_messages"} theme={null}
// Schedule reminder for tomorrow
restate.ServiceSend(ctx, "ReminderService", "SendReminder").Send(
message,
restate.WithDelay(24*time.Hour),
)
```
## Durable timers and timeouts
Pause function execution for a specific duration.
```typescript TypeScript {"CODE_LOAD::ts/src/foundations/actions/actions.ts#durable_timers"} theme={null}
// Sleep for specific duration
await ctx.sleep({ minutes: 5 }); // 5 minutes
// Wait for action or timeout
const result = await ctx
.workflowClient(OrderWorkflow, orderId)
.run(order)
.orTimeout({ minutes: 5 });
```
```java Java {"CODE_LOAD::java/src/main/java/foundations/actions/Actions.java#durable_timers"} theme={null}
// Sleep for specific duration
ctx.sleep(Duration.ofMinutes(5)); // 5 minutes
// Wait for action or timeout
try {
OrderWorkflowClient.fromContext(ctx, req.orderId())
.run(req.order())
.await(Duration.ofMinutes(5));
} catch (TimeoutException e) {
// Handle timeout
}
```
```python Python {"CODE_LOAD::python/src/foundations/actions/actions.py#durable_timers"} theme={null}
# Sleep for specific duration
await ctx.sleep(timedelta(minutes=5)) # 5 minutes
# Wait for action or timeout
from order_workflow import run
match await restate.select(
result=ctx.workflow_call(run, key=order_id, arg=order),
timeout=ctx.sleep(timedelta(minutes=5)),
):
case ["result", result]:
return result
case _:
print("Order processing timed out")
```
```go Go {"CODE_LOAD::go/foundations/actions/actions.go#durable_timers"} theme={null}
// Sleep for specific duration
if err := restate.Sleep(ctx, 5*time.Minute); err != nil {
return err
}
// Wait for action or timeout
sleepFuture := restate.After(ctx, 5*time.Minute)
callFuture := restate.Workflow[restate.Void](ctx, "OrderWorkflow", orderId, "Run").RequestFuture(order)
selector := restate.Select(ctx, sleepFuture, callFuture)
switch selector.Select() {
case sleepFuture:
if err := sleepFuture.Done(); err != nil {
return err
}
// Timeout occurred
case callFuture:
if _, err := callFuture.Response(); err != nil {
return err
}
// Call completed
}
```
Handlers consume no resources while sleeping and resume at exactly the right time, even across restarts ([see suspensions](/foundations/key-concepts#suspensions-on-faas)).
## Workflow events
Use durable promises to wait for external events or human input in your workflows.
Create promises that external systems can resolve to send data to your workflow.
```typescript TypeScript {"CODE_LOAD::ts/src/foundations/actions/actions.ts#workflow_promises"} theme={null}
// Wait for external event
const paymentResult = await ctx.promise(
"payment-completed"
);
// Wait for human approval
const approved = await ctx.promise("manager-approval");
// Wait for multiple events
const [payment, inventory] = await RestatePromise.all([
ctx.promise("payment").get(),
ctx.promise("inventory").get(),
]);
```
```java Java {"CODE_LOAD::java/src/main/java/foundations/actions/WorkflowExample.java#workflow_promises"} theme={null}
// Wait for external event
PaymentResult paymentResult = ctx.promise(PAYMENT_COMPLETED).future().await();
// Wait for human approval
Boolean approved = ctx.promise(MANAGER_APPROVAL).future().await();
// Wait for multiple events
var paymentFuture = ctx.promise(PAYMENT).future();
var inventoryFuture = ctx.promise(INVENTORY).future();
PaymentResult payment = paymentFuture.await();
InventoryResult inventory = inventoryFuture.await();
```
```python Python {"CODE_LOAD::python/src/foundations/actions/actions.py#workflow_promises"} theme={null}
# Wait for external event
payment_result = await ctx.promise(
"payment-completed", type_hint=PaymentResult
).value()
# Wait for human approval
approved = await ctx.promise("manager-approval", type_hint=bool).value()
# Wait for multiple events using gather
payment_promise = ctx.promise("payment", type_hint=PaymentResult)
inventory_promise = ctx.promise("inventory", type_hint=InventoryConfirmation)
payment, inventory = await restate.gather(
payment_promise.value(), inventory_promise.value()
)
```
```go Go {"CODE_LOAD::go/foundations/actions/actions.go#workflow_promises"} theme={null}
// Wait for external event
paymentResult, err := restate.Promise[PaymentResult](ctx, "payment-completed").Result()
if err != nil {
return err
}
// Wait for human approval
approved, err := restate.Promise[bool](ctx, "manager-approval").Result()
if err != nil {
return err
}
// Wait for multiple events
paymentFuture := restate.Promise[PaymentResult](ctx, "payment")
inventoryFuture := restate.Promise[InventoryResult](ctx, "inventory")
payment, err := paymentFuture.Result()
if err != nil {
return err
}
inventory, err := inventoryFuture.Result()
if err != nil {
return err
}
```
Resolve promises from signal handlers.
```typescript TypeScript {"CODE_LOAD::ts/src/foundations/actions/actions.ts#signal_functions"} theme={null}
// In a signal function
confirmPayment: async (
ctx: WorkflowSharedContext,
result: PaymentResult
) => {
await ctx.promise("payment-completed").resolve(result);
},
// In a signal function
approveRequest: async (ctx: WorkflowSharedContext, approved: boolean) => {
await ctx.promise("manager-approval").resolve(approved);
},
```
```java Java {"CODE_LOAD::java/src/main/java/foundations/actions/WorkflowExample.java#signal_functions"} theme={null}
// In a signal function
@Shared
public void confirmPayment(SharedWorkflowContext ctx, PaymentResult result) {
ctx.promiseHandle(PAYMENT_COMPLETED).resolve(result);
}
// In a signal function
@Shared
public void approveRequest(SharedWorkflowContext ctx, Boolean approved) {
ctx.promiseHandle(MANAGER_APPROVAL).resolve(approved);
}
```
```python Python {"CODE_LOAD::python/src/foundations/actions/actions.py#signal_functions"} theme={null}
# In a signal function
@workflow_example_workflow.handler()
async def confirm_payment(ctx: WorkflowSharedContext, result: PaymentResult) -> None:
await ctx.promise("payment-completed").resolve(result)
# In a signal function
@workflow_example_workflow.handler()
async def approve_request(ctx: WorkflowSharedContext, approved: bool) -> None:
await ctx.promise("manager-approval").resolve(approved)
```
```go Go {"CODE_LOAD::go/foundations/actions/actions.go#signal_functions"} theme={null}
// In a signal function
func (WorkflowExample) ConfirmPayment(ctx restate.WorkflowSharedContext, result PaymentResult) error {
if err := restate.Promise[PaymentResult](ctx, "payment-completed").Resolve(result); err != nil {
return err
}
return nil
}
// In a signal function
func (WorkflowExample) ApproveRequest(ctx restate.WorkflowSharedContext, approved bool) error {
if err := restate.Promise[bool](ctx, "manager-approval").Resolve(approved); err != nil {
return err
}
return nil
}
```
To implement a similar pattern in Basic Services or Virtual Objects, have a look at awakeables ([TS](/develop/ts/external-events)/[Java/Kotlin](/develop/java/external-events)/[Go](/develop/ts/external-events)/[Python](/develop/python/external-events)).
# Handlers
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/foundations/handlers
Learn how to create, invoke and manage handlers in Restate services
Handlers are the core building blocks of Restate services. Each service has a list of handlers: durable functions that can be invoked over HTTP, Kafka, or using typed clients.
## Writing handlers
Handlers receive a Restate context object (`ctx`) and a single, optional input:
```typescript TypeScript {"CODE_LOAD::ts/src/foundations/functions/functions.ts#here"} theme={null}
async function myHandler(ctx: restate.Context, input: MyInput) {
const result = await ctx.run("process", () => processData(input));
return { success: true, result };
}
```
```java Java {"CODE_LOAD::java/src/main/java/foundations/functions/Functions.java#here"} theme={null}
@Handler
public ProcessingResult myHandler(Context ctx, MyInput myInput) {
return ctx.run(ProcessingResult.class, () -> processData(myInput));
}
```
```python Python {"CODE_LOAD::python/src/foundations/functions/functions.py#here"} theme={null}
async def my_handler(ctx: Context, req: MyInput) -> ProcessingResult:
return await ctx.run_typed("process", process_data, req=req)
```
```go Go {"CODE_LOAD::go/foundations/functions/functions.go#here"} theme={null}
func MyHandler(ctx restate.Context, input MyInput) (ProcessingResult, error) {
return restate.Run(ctx, func(ctx restate.RunContext) (ProcessingResult, error) {
return processData(input)
})
}
```
The input and output can be any JSON-serializable type. For other types, consult the serialization documentation of your SDK.
## Context types
The context type you use depends on your service type:
* **`Context`** - Basic Services (stateless handlers)
* **`ObjectContext`** - Virtual Objects (exclusive handlers with state access)
* **`ObjectSharedContext`** - Virtual Objects (concurrent read-only handlers)
* **`WorkflowContext`** - Workflows (main run handler)
* **`WorkflowSharedContext`** - Workflows (signal/query handlers)
## Handler behavior
Virtual Objects and Workflows support two handler types:
**Exclusive handlers** (`ObjectContext`, `WorkflowContext`) can read and write state but only one runs at a time per key to prevent conflicts.
**Shared handlers** (`ObjectSharedContext`, `WorkflowSharedContext`) can only read state but run concurrently without blocking—useful for querying status during long-running operations.
# Invocations
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/foundations/invocations
Invoke handlers in Restate services over HTTP, Kafka or with typed clients.
An invocation is a request to execute a handler that is part of a Restate service.
There are three ways to invoke a handler: over HTTP, using typed clients, or through Kafka topics.
## HTTP invocations
To call a function over HTTP, send a request to the Restate Server, specifying the service and the function you want to invoke.
For example, to call the `processPayment` function of the `PaymentService`:
```bash theme={null}
curl -X POST localhost:8080/PaymentService/processPayment \
--json '{"amount": 100, "currency": "USD"}'
```
The Restate Server ingress endpoint is here hosted at `localhost:8080`.
To call Virtual Objects or Workflows, you need to specify the object key or workflow ID in the URL.
For example, to call `updateBalance` of the `UserAccount` object with key `user123`:
```bash theme={null}
curl -X POST localhost:8080/UserAccount/user123/updateBalance \
--json '{"amount": 50}'
```
Check out the [HTTP invocation](/services/invocation/http) docs to learn more.
## Typed client invocations
Use typed clients from external applications to invoke Restate handlers:
```typescript TypeScript {"CODE_LOAD::ts/src/foundations/invocations/client.ts#here"} theme={null}
//import * as clients from "@restatedev/restate-sdk-clients";
const restateClient = clients.connect({ url: "http://localhost:8080" });
// To call a service:
const greet = await restateClient
.serviceClient(greeterService)
.greet({ greeting: "Hi" });
```
```java Java {"CODE_LOAD::java/src/main/java/foundations/invocations/Client.java#here"} theme={null}
var restate = Client.connect("http://localhost:8080");
// To call a service:
MyServiceClient.fromClient(restate).myHandler(req);
```
```go Go {"CODE_LOAD::go/foundations/invocations/client.go#here"} theme={null}
restateClient := restateingress.NewClient("http://localhost:8080")
// To call a service
response, err := restateingress.Service[*MyInput, *MyOutput](
restateClient, "MyService", "MyHandler").
Request(context.Background(), &input)
if err != nil {
return err
}
```
For service-to-service calls within handlers, see [Service Communication](/foundations/handlers#service-communication).
## Kafka invocations
You can connect your Restate handlers to Kafka topics, to let Restate invoke them for each message.
Consult the [Kafka Quickstart](/guides/kafka-quickstart) to get started.
## Idempotency
Add an idempotency key to your request header to let Restate deduplicate retries:
```bash theme={null}
curl -X POST localhost:8080/PaymentService/processPayment \
-H 'idempotency-key: payment-123' \
--json '{"amount": 100, "currency": "USD"}'
```
On retry, Restate returns the first invocation’s result or lets you attach to it if still running.
## Inspecting invocations
Use the Restate UI to monitor and troubleshoot invocations:
## Attaching to invocations
Attach to ongoing invocations to retrieve their results:
```typescript TypeScript {"CODE_LOAD::ts/src/foundations/invocations/client.ts#attach"} theme={null}
const handle = ctx
.serviceSendClient(myService)
.myHandler("Hi", sendOpts({ idempotencyKey: "my-key" }));
const invocationId = await handle.invocationId;
// Later...
const response = ctx.attach(invocationId);
```
```java Java {"CODE_LOAD::java/src/main/java/foundations/invocations/Client.java#attach"} theme={null}
var handle =
MyServiceClient.fromContext(ctx)
.send()
.myHandler("Hi", opt -> opt.idempotencyKey("my-key"));
var response = handle.attach().await();
```
```python Python {"CODE_LOAD::python/src/foundations/invocations/client.py#attach"} theme={null}
handle = ctx.service_send(my_handler, "Hi", idempotency_key="my-key")
invocation_id = await handle.invocation_id()
# Later...
await ctx.attach_invocation(invocation_id)
```
```go Go {"CODE_LOAD::go/foundations/invocations/client.go#attach"} theme={null}
// Execute the request and retrieve the invocation id
invocationId := restate.
ServiceSend(ctx, "MyService", "MyHandler").
Send("Hi", restate.WithIdempotencyKey("my-idempotency-key")).
GetInvocationId()
// Later re-attach to the request
response, err := restate.AttachInvocation[string](ctx, invocationId).Response()
```
Or over HTTP:
```bash theme={null}
curl localhost:8080/restate/invocation/inv_1234567890abcdef/attach
```
This is useful when another process needs the result of an ongoing operation or wants to check whether it has completed.
## Cancelling invocations
Cancel running invocations when they're no longer needed:
```bash CLI theme={null}
restate invocations cancel inv_1234567890abcdef
```
```bash HTTP theme={null}
curl -X PATCH http://localhost:9070/invocations/inv_1gdJBtdVEcM942bjcDmb1c1khoaJe11Hbz/cancel
```
Or programmatically:
```typescript TypeScript {"CODE_LOAD::ts/src/foundations/invocations/client.ts#cancel"} theme={null}
const handle = ctx.serviceSendClient(myService).myHandler("Hi");
const invocationId = await handle.invocationId;
// Cancel the invocation
ctx.cancel(invocationId);
```
```java Java {"CODE_LOAD::java/src/main/java/foundations/invocations/Client.java#cancel"} theme={null}
var handle = MyServiceClient.fromContext(ctx).send().myHandler("Hi");
// Cancel the invocation
handle.cancel();
```
```python Python {"CODE_LOAD::python/src/foundations/invocations/client.py#cancel"} theme={null}
handle = ctx.service_send(my_handler, "Hi", idempotency_key="my-key")
invocation_id = await handle.invocation_id()
# Cancel the invocation
ctx.cancel_invocation(invocation_id)
```
```go Go {"CODE_LOAD::go/foundations/invocations/client.go#cancel"} theme={null}
// Execute the request and retrieve the invocation id
invocationId := restate.
ServiceSend(ctx, "MyService", "MyHandler").
Send("Hi").
GetInvocationId()
// I don't need this invocation anymore, let me just cancel it
restate.CancelInvocation(ctx, invocationId)
```
To roll back the actions the handler already completed before cancellation, check out at the [sagas guide](/guides/sagas).
# Key Concepts
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/foundations/key-concepts
Core concepts of Restate applications
This page explains the key concepts and architecture that make Restate applications reliable and resilient.
## Application Structure
A Restate application consists of the following components:
### Restate Services
Your business logic lives in **services**: regular applications that embed the Restate SDK. Services contain **handlers** (durable functions) that process requests and execute business logic.
* **Deploy anywhere**: Services run in your infrastructure as containers, serverless functions, VMs, or Kubernetes pods
* **Familiar programming model**: Services look and feel like normal applications with added reliability by using the Restate SDK. Restate has SDKs for TypeScript, Java, Kotlin, Python, Go, and Rust.
### Restate Server
The **Restate Server** sits in front of your services, similar to a reverse proxy or message broker. It handles incoming requests, manages service discovery, and provides durable execution capabilities.
The server is a single binary written in Rust with a stream-processing architecture for **low latency and high throughput**.
It can be deployed in a high-availability cluster or as a single-node instance.
Look at the options for Restate Cloud for a fully managed Restate deployment.
### Invocations
An **invocation** represents a request to execute a handler. Restate tracks each invocation through completion, ensuring it runs exactly once regardless of failures.
You can invoke Restate handlers over HTTP, Kafka, or programmatically.
## Durable Execution
Restate's core feature is **durable execution**: the ability to make any code execution reliable and fault-tolerant without changing how you write business logic.
### How it works
Restate tracks every step of your code execution in a **journal**. When you call other services, update databases, set timers, or perform any side-effecting operation, Restate records both the operation and its result. If your function crashes or fails, Restate replays the journal, skipping completed steps and resuming from exactly where it left off.
In this example, if sending the receipt fails, Restate automatically retries the function but skips the payment processing since it already completed successfully. Each step is recorded in the journal and won't repeat on retry.
For a detailed technical walkthrough of the lifecycle of a request, read this **[guide](/guides/request-lifecycle)**.
### What it covers
Restate's implementation of Durable Execution protects your applications from common failure scenarios:
* **Service crashes or restarts**: Any ongoing executions are retried on new instances.
* **Restate Server crashes or restarts**: The Restate Server can be deployed as a high-availability cluster, so if a node fails, others can take over without losing any execution state.
If Restate is deployed as a single node, it can still recover from crashes by recovering from persistent disk.
If a handler calls an API that times out, Restate will retry the run action with exponential backoff. You can tune the retry policy through the [service configuration](/services/configuration#retries).
If you add an idempotency key to your request headers, Restate will automatically ensure that requests are deduplicated.
Duplicate requests will return the same result as the original request.
* **Between Server and service**: If the service cannot send journal entries to the Restate Server, it will stop the handler execution. The Restate Server will retry the execution on another service instance.
* **Between nodes in a Restate cluster**: Restate clusters rely on a consensus algorithm to accept new entries in the log. The consensus algorithm requires a majority of nodes to be available, so if a network partition occurs, the cluster will still be able to process requests as long as a majority can still communicate.
## Resilient Communication
Beyond executing individual handlers reliably, Restate also ensures that communication between services is resilient.
Restate proxies all communication towards and between services.
It provides a **resilient RPC framework**:
* Automatic retries and recovery: Failed calls are retried until they succeed
* No duplicates: Same call won’t execute twice
* Full observability: See all calls and call chains in the UI
* No message queues needed: Restate handles message delivery
## Consistent State
The Restate Server includes an embedded **key-value store** for persisting application state in Virtual Objects and Workflows.
Key characteristics:
* **Usage**: Implement consistent state machines, session state, or agent context and memory.
* **Scoped per entity**: Each Virtual Object and Workflow execution has its own isolated state.
* **Automatically journaled**: State updates are recorded alongside execution steps for consistency. State is never out of sync with the execution.
* **Single-writer guarantee**: Only one handler can modify state at a time, preventing race conditions.
* **Stateless services**: The Restate Server stores all state and execution history, delivering them with each request. Your services remain stateless, can scale horizontally, and run stateful logic even on serverless platforms.
Learn more about when to store state in Restate vs. in a database with [this guide](/guides/databases).
## Suspensions on FaaS
When running handlers on function-as-a-service platforms (FaaS), like AWS Lambda, Restate can suspend functions while they wait for external events (like user input or long-running tasks).
For example, if a handler needs to wait for a user to approve an order, Restate can suspend the function until the user responds.
This lets you run long-running workflows on FaaS platforms **without paying for wait time**.
## Next Steps
Now that you understand the core concepts, dive deeper into how to build with Restate:
* **[Services](/foundations/services)**: Understand the three service types and when to use each
* **[Handlers](/foundations/handlers)**: Learn how to write handlers that take advantage of durable execution
* **[Actions](/foundations/actions)**: Master the context helpers for state management, service calls, and timing control
# Services
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/foundations/services
Understanding Restate's three service types and when to use each
Restate provides three service types optimized for different use cases.
## Service types comparison
| | **Basic Service** | **Virtual Object** | **Workflow** |
| ---------------- | -------------------------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| **What** | Independent stateless handlers | Stateful entity with a unique key | Multi-step processes that execute exactly-once per ID |
| **State** | None | Isolated per object key | Isolated per workflow instance |
| **Concurrency** | Unlimited parallel execution | Single writer per key (+ concurrent readers) | Single `run` handler per ID (+ concurrent signals/queries) |
| **Key Features** | Durable execution, service calls | Built-in K/V state, single-writer consistency | Durable promises, signals, lifecycle management |
| **Best For** | ETL, sagas, parallelization, background jobs | User accounts, shopping carts, agents, state machines, stateful event processing | Approvals, onboarding workflows, multi-step flows |
## Basic Service
Basic Services group related handlers as callable endpoints.
```typescript TypeScript {"CODE_LOAD::ts/src/foundations/services/basic_service.ts#here"} theme={null}
const subscriptionService = restate.service({
name: "SubscriptionService",
handlers: {
add: async (ctx: restate.Context, req: SubscriptionRequest) => {
const paymentId = ctx.rand.uuidv4();
const payRef = await ctx.run(() =>
createRecurringPayment(req.creditCard, paymentId)
);
for (const subscription of req.subscriptions) {
await ctx.run(() =>
createSubscription(req.userId, subscription, payRef)
);
}
},
},
});
```
```java Java {"CODE_LOAD::java/src/main/java/foundations/services/SubscriptionService.java#here"} theme={null}
@Service
public class SubscriptionService {
@Handler
public void add(Context ctx, SubscriptionRequest req) {
var paymentId = ctx.random().nextUUID().toString();
String payRef =
ctx.run("pay", String.class, () -> createRecurringPayment(req.creditCard(), paymentId));
for (String subscription : req.subscriptions()) {
ctx.run("add-" + subscription, () -> createSubscription(req.userId(), subscription, payRef));
}
}
}
```
```python Python {"CODE_LOAD::python/src/foundations/services/basic_service.py#here"} theme={null}
subscription_service = restate.Service("SubscriptionService")
@subscription_service.handler()
async def add(ctx: Context, req: SubscriptionRequest) -> None:
payment_id = str(uuid.uuid4())
pay_ref = await ctx.run_typed(
"pay",
create_recurring_payment,
credit_card=req.credit_card,
payment_id=payment_id,
)
for subscription in req.subscriptions:
await ctx.run_typed(
"add-" + subscription,
create_subscription,
user_id=req.user_id,
subscription=subscription,
pay_ref=pay_ref,
)
```
```go Go {"CODE_LOAD::go/foundations/services/basic_service.go#here"} theme={null}
type SubscriptionService struct{}
func (SubscriptionService) Add(ctx restate.Context, req SubscriptionRequest) error {
paymentId := restate.Rand(ctx).UUID().String()
payRef, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
return createRecurringPayment(req.CreditCard, paymentId)
})
if err != nil {
return err
}
for _, subscription := range req.Subscriptions {
_, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
return createSubscription(req.UserId, subscription, payRef)
})
if err != nil {
return err
}
}
return nil
}
```
**Characteristics:**
* Use Durable Execution to run requests to completion
* Scale horizontally with high concurrency
* No shared state between requests
**Use for:** API calls, sagas, background jobs, task parallelization, ETL operations.
## Virtual Object
Stateful entities identified by a unique key.
```typescript TypeScript {"CODE_LOAD::ts/src/foundations/services/object.ts#here"} theme={null}
const cartObject = restate.object({
name: "ShoppingCart",
handlers: {
addItem: async (ctx: restate.ObjectContext, item: Item) => {
const items = (await ctx.get("cart")) ?? [];
items.push(item);
ctx.set("cart", items);
return items;
},
getTotal: restate.handlers.object.shared(
async (ctx: restate.ObjectSharedContext) => {
const items = (await ctx.get("cart")) ?? [];
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
),
},
});
```
```java Java {"CODE_LOAD::java/src/main/java/foundations/services/ShoppingCartObject.java#here"} theme={null}
@VirtualObject
public class ShoppingCartObject {
private static final StateKey CART = StateKey.of("cart", Cart.class);
@Handler
public Cart addItem(ObjectContext ctx, Item item) {
var cart = ctx.get(CART).orElse(new Cart());
var newCart = cart.addItem(item);
ctx.set(CART, newCart);
return cart;
}
@Shared
public double getTotal(SharedObjectContext ctx) {
var cart = ctx.get(CART).orElse(new Cart());
return cart.getTotalPrice();
}
}
```
```python Python {"CODE_LOAD::python/src/foundations/services/object.py#here"} theme={null}
cart_object = restate.VirtualObject("ShoppingCart")
@cart_object.handler()
async def add_item(ctx: ObjectContext, item: Item) -> Cart:
cart = await ctx.get("cart", type_hint=Cart) or Cart()
cart.items.append(item)
ctx.set("cart", cart)
return cart
@cart_object.handler(kind="shared")
async def get_total(ctx: ObjectSharedContext) -> float:
cart = await ctx.get("cart", type_hint=Cart) or Cart()
return sum(item.price * item.quantity for item in cart.items)
```
```go Go {"CODE_LOAD::go/foundations/services/object.go#here"} theme={null}
type ShoppingCartObject struct{}
func (ShoppingCartObject) AddItem(ctx restate.ObjectContext, item Item) ([]Item, error) {
items, err := restate.Get[[]Item](ctx, "items")
if err != nil {
return nil, err
}
items = append(items, item)
restate.Set(ctx, "items", items)
return items, nil
}
func (ShoppingCartObject) GetTotal(ctx restate.ObjectSharedContext) (float64, error) {
items, err := restate.Get[[]Item](ctx, "items")
if err != nil {
return 0, err
}
total := 0.0
for _, item := range items {
total += item.Price * float64(item.Quantity)
}
return total, nil
}
```
**Characteristics:**
* Use Durable Execution to run requests to completion
* K/V state retained indefinitely and shared across requests
* Horizontal scaling with state consistency:
* At most one handler with write access can run at a time per object key. Mimicks a queue per object key.
* Concurrent execution across different object keys
* Concurrent execution of shared handlers (read-only)
**Use for:** Modeling entities like user accounts, shopping carts, chat sessions, AI agents, state machines, or any business entity needing persistent state.
## Workflow
Workflows orchestrate multi-step processes with guaranteed once-per-ID execution.
```typescript TypeScript {"CODE_LOAD::ts/src/foundations/services/workflow.ts#here"} theme={null}
const signupWorkflow = restate.workflow({
name: "UserSignup",
handlers: {
run: async (
ctx: restate.WorkflowContext,
user: { name: string; email: string }
) => {
// workflow ID = user ID; workflow runs once per user
const userId = ctx.key;
await ctx.run("create", () => createUserEntry({ userId, user }));
const secret = ctx.rand.uuidv4();
await ctx.run("mail", () => sendVerificationEmail({ user, secret }));
const clickSecret = await ctx.promise("email-link-clicked");
return clickSecret === secret;
},
click: async (
ctx: restate.WorkflowSharedContext,
request: { secret: string }
) => {
await ctx.promise("email-link-clicked").resolve(request.secret);
},
},
});
```
```java Java {"CODE_LOAD::java/src/main/java/foundations/services/SignupWorkflow.java#here"} theme={null}
@Workflow
public class SignupWorkflow {
private static final DurablePromiseKey EMAIL_LINK_CLICKED =
DurablePromiseKey.of("email-link-clicked", String.class);
@Workflow
public boolean run(WorkflowContext ctx, User user) {
// workflow ID = user ID; workflow runs once per user
String userId = ctx.key();
ctx.run(() -> createUserEntry(userId, user));
String secret = ctx.random().nextUUID().toString();
ctx.run(() -> sendVerificationEmail(user, secret));
String clickSecret = ctx.promise(EMAIL_LINK_CLICKED).future().await();
return clickSecret.equals(secret);
}
@Shared
public void click(SharedWorkflowContext ctx, ClickRequest request) {
ctx.promiseHandle(EMAIL_LINK_CLICKED).resolve(request.secret());
}
}
```
```python Python {"CODE_LOAD::python/src/foundations/services/workflow.py#here"} theme={null}
signup_workflow = restate.Workflow("UserSignup")
@signup_workflow.main()
async def run(ctx: WorkflowContext, user: User) -> bool:
# workflow ID = user ID; workflow runs once per user
user_id = ctx.key()
await ctx.run_typed("create", create_user_entry, user_id=user_id, user=user)
secret = str(ctx.uuid())
await ctx.run_typed("mail", send_verification_email, user=user, secret=secret)
click_secret = await ctx.promise("email-link-clicked").value()
return click_secret == secret
@signup_workflow.handler()
async def click(ctx: WorkflowSharedContext, secret: str) -> None:
await ctx.promise("email-link-clicked").resolve(secret)
```
```go Go {"CODE_LOAD::go/foundations/services/workflow.go#here"} theme={null}
type SignupWorkflow struct{}
func (SignupWorkflow) Run(ctx restate.WorkflowContext, user User) (bool, error) {
// workflow ID = user ID; workflow runs once per user
userId := restate.Key(ctx)
_, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return createUserEntry(userId, user)
})
if err != nil {
return false, err
}
secret := restate.Rand(ctx).UUID().String()
_, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return sendVerificationEmail(user, secret)
})
if err != nil {
return false, err
}
clickSecret, err := restate.Promise[string](ctx, "email-link-clicked").Result()
if err != nil {
return false, err
}
return clickSecret == secret, nil
}
func (SignupWorkflow) Click(ctx restate.WorkflowSharedContext, secret string) error {
return restate.Promise[string](ctx, "email-link-clicked").Resolve(secret)
}
```
**Characteristics:**
* Use Durable Execution to run requests to completion
* The `run` handler executes exactly once per workflow ID
* Other handlers run concurrently with the `run` handler to signal, query state, or wait for events
* Optimized APIs for workflow interaction and lifecycle management
**Use for:** Processes requiring interaction capabilities like approval flows, user onboarding, multi-step transactions, and complex orchestration.
## Choosing the right service type
**Start with Basic Services** for most business logic, data processing, and API integrations.
**Use Virtual Objects** to model stateful entities.
**Use Workflows** for multi-step processes that execute exactly-once and require interaction.
You can combine these service types within the same application for different aspects of your business logic.
## Deployments, Endpoints, and Versions
Services deploy behind endpoints. Multiple services can bind to the same endpoint.
```typescript TypeScript {"CODE_LOAD::ts/src/foundations/services/app.ts#here"} theme={null}
restate.serve({
services: [subscriptionService, cartObject, signupWorkflow],
port: 9080,
});
```
```java Java {"CODE_LOAD::java/src/main/java/foundations/services/App.java#here"} theme={null}
public class App {
public static void main(String[] args) {
RestateHttpServer.listen(
Endpoint.bind(new SubscriptionService())
.bind(new ShoppingCartObject())
.bind(new SignupWorkflow()));
}
}
```
```python Python {"CODE_LOAD::python/src/foundations/services/app.py#here"} theme={null}
app = restate.app([subscription_service, cart_object, signup_workflow])
if __name__ == "__main__":
conf = hypercorn.Config()
conf.bind = ["0.0.0.0:9080"]
asyncio.run(hypercorn.asyncio.serve(app, conf))
```
```go Go {"CODE_LOAD::go/foundations/services/app.go#here"} theme={null}
func main() {
if err := server.NewRestate().
Bind(restate.Reflect(SubscriptionService{})).
Bind(restate.Reflect(ShoppingCartObject{})).
Bind(restate.Reflect(SignupWorkflow{})).
Start(context.Background(), ":9080"); err != nil {
log.Fatal(err)
}
}
```
Services run on your preferred platform: serverless (AWS Lambda), containers (Kubernetes), or dedicated servers.
Restate handles versioning through immutable deployments where each deployment represents a specific, unchangeable version of your service code. After deploying your
services to an endpoint, you must register that endpoint with Restate so it can discover and route requests to it:
```shell theme={null}
restate deployments register http://my-service:9080
```
When you update your services, you deploy the new version to a new endpoint and register it with Restate, which automatically routes new requests to the latest version while existing requests continue on their
original deployment until completion.
See [deployment](/deploy/services/kubernetes) and [versioning](/services/versioning) docs for details.
# Local Restate Cluster with Docker
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/guides/cluster
Learn how to deploy a Restate cluster using Docker Compose.
This guide shows how to deploy a distributed Restate cluster consisting of three nodes using Docker and Docker compose.
* [Docker](https://docs.docker.com/get-started/get-docker/)
* [Docker Compose](https://docs.docker.com/compose/install/)
To deploy a 3-node distributed Restate cluster, create a file `docker-compose.yml` and run `docker compose up`.
```yaml docker-compose.yml theme={null}
x-environment: &common-env
RESTATE_CLUSTER_NAME: "restate-cluster"
# For more on logging, see: https://docs.restate.dev/server/monitoring/logging
RESTATE_LOG_FILTER: "restate=info"
RESTATE_DEFAULT_REPLICATION: 2 # We require minimum of 2 nodes to accept writes
# The addresses where nodes can reach each other over the "internal" Docker Compose network
RESTATE_METADATA_CLIENT__ADDRESSES: '["http://restate-1:5122","http://restate-2:5122","http://restate-3:5122"]'
# Partition snapshotting, see: https://docs.restate.dev/server/snapshots
RESTATE_WORKER__SNAPSHOTS__DESTINATION: "s3://restate/snapshots"
RESTATE_WORKER__SNAPSHOTS__SNAPSHOT_INTERVAL_NUM_RECORDS: "1000"
RESTATE_WORKER__SNAPSHOTS__AWS_REGION: "local"
RESTATE_WORKER__SNAPSHOTS__AWS_ENDPOINT_URL: "http://minio:9000"
RESTATE_WORKER__SNAPSHOTS__AWS_ALLOW_HTTP: true
RESTATE_WORKER__SNAPSHOTS__AWS_ACCESS_KEY_ID: "minioadmin"
RESTATE_WORKER__SNAPSHOTS__AWS_SECRET_ACCESS_KEY: "minioadmin"
x-defaults: &defaults
image: docker.restate.dev/restatedev/restate:latest
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- restate-data:/restate-data
services:
restate-1:
<<: *defaults
ports:
- "8080:8080" # Ingress
- "9070:9070" # Admin
- "5122:5122" # Node-to-node communication
environment:
<<: *common-env
RESTATE_NODE_NAME: restate-1
RESTATE_FORCE_NODE_ID: 1
RESTATE_ADVERTISED_ADDRESS: "http://restate-1:5122" # Other Restate nodes must be able to reach us using this address
RESTATE_AUTO_PROVISION: "true" # Only the first node provisions the cluster
restate-2:
<<: *defaults
ports:
- "25122:5122"
- "29070:9070"
- "28080:8080"
environment:
<<: *common-env
RESTATE_NODE_NAME: restate-2
RESTATE_FORCE_NODE_ID: 2
RESTATE_ADVERTISED_ADDRESS: "http://restate-2:5122"
RESTATE_AUTO_PROVISION: "false"
restate-3:
<<: *defaults
ports:
- "35122:5122"
- "39070:9070"
- "38080:8080"
environment:
<<: *common-env
RESTATE_NODE_NAME: restate-3
RESTATE_FORCE_NODE_ID: 3
RESTATE_ADVERTISED_ADDRESS: "http://restate-3:5122"
RESTATE_AUTO_PROVISION: "false"
minio:
image: quay.io/minio/minio
entrypoint: "/bin/sh"
# Ensure a bucket called "restate" exists on startup:
command: "-c 'mkdir -p /data/restate && /usr/bin/minio server --quiet /data'"
ports:
- "9000:9000"
# We create a volume to persist data across container starts; delete it via `docker volume rm restate-data` if you want to start a fresh cluster
volumes:
restate-data:
```
The cluster uses the `replicated` Bifrost provider and replicates log writes to a minimum of two nodes.
Since we are running with 3 nodes, the cluster can tolerate one node failure without becoming unavailable.
By default, partition state is replicated to all workers (though each partition has only one acting leader at a time).
The `replicated` metadata cluster consists of all nodes since they all run the `metadata-server` role.
Since the `replicated` metadata cluster requires a majority quorum to operate, the cluster can tolerate one node failure without becoming unavailable.
Take a look at the [cluster deployment documentation](/deploy/server/cluster) for more information on how to configure and deploy a distributed Restate cluster.
In this example we also deployed a Minio server to host the cluster snapshots bucket. Visit [Snapshots](/server/snapshots) to learn more about whis is strongly recommended for all clusters.
You can check the status of the cluster by running the `restatectl status` command on any of the started Restate servers.
Note, it might take a few seconds until the cluster has fully started and the status is available.
```shell theme={null}
docker compose exec restate-1 restatectl status
```
Follow the [Quickstart guide](/quickstart) to create a simple Restate service.
You can register the service endpoint at any of the started Restate nodes since they all run the `admin` role.
```shell theme={null}
restate dp register http://host.docker.internal:9080
```
Or alternatively you can open the Restate UI at [http://localhost:9080](http://localhost:9080) and register the service endpoint there.
You can invoke the registered service at any of the started Restate nodes since they all run the ingress.
```shell theme={null}
curl localhost:8080/Greeter/greet --json '"Sarah"' &&
curl localhost:28080/Greeter/greet --json '"Bob"' &&
curl localhost:38080/Greeter/greet --json '"Eve"'
```
Try killing and restarting one of the Restate nodes and see how the cluster reacts.
```shell theme={null}
docker compose kill restate-1 &&
sleep 5 &&
docker compose up -d restate-1
```
Try instructing the partition processors to create a snapshot of their state in the object store bucket:
```shell theme={null}
docker compose exec restate-1 restatectl snapshot create
```
Navigate to the Minio console at [http://localhost:9000](http://localhost:9000) and browse the bucket contents (default credentials: `minioadmin`/`minioadmin`).
Here are some next steps for you to try:
* Try to configure a 5-node Restate cluster that can tolerate up to two node failures.
* Trim the logs (either manually, or by setting up automatic trimming) *before* adding more nodes.
* Try to deploy a 3-node Restate cluster using Kubernetes.
# Cron Jobs
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/guides/cron
Schedule tasks periodically with Restate
A cron job is a scheduled task that runs periodically at a specified time or interval. It is often used for background tasks like cleanup or sending notifications.
## How does Restate help?
Restate has many features that make it a good fit for implementing cron jobs:
* **Durable timers**: Schedule tasks reliably for future execution.
* **Task resiliency**: Automatic retries until success.
* **Task control**: Inspect and cancel running jobs.
* **K/V state**: Store and query cron job details using K/V storage.
* **FaaS support**: Integrates with platforms like AWS Lambda, scaling to zero when idle.
* **Scalability**: Handles many parallel jobs; scales horizontally.
* **Observability**: View execution history and job status in the Restate UI.
Restate doesn't have built-in cron functionality, but its building blocks make it easy to build reliable, custom schedulers.
## Example
To implement cron jobs, you can copy over the following file ([TS](https://github.com/restatedev/examples/blob/main/typescript/patterns-use-cases/src/cron/cron_service.ts) /
[Java](https://github.com/restatedev/examples/blob/main/java/patterns-use-cases/src/main/java/my/example/cron/Cron.java) /
[Go](https://github.com/restatedev/examples/blob/main/go/patterns-use-cases/src/cron/cron.go)) to your project:
```ts TypeScript expandable {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/patterns-use-cases/src/cron/cron_service.ts?collapse_prequel"} theme={null}
export const cronJobInitiator = restate.service({
name: "CronJobInitiator",
handlers: {
create: async (ctx: restate.Context, request: JobRequest) => {
// Create a new job ID and initiate the cron job object for that ID
// We can then address this job object by its ID
const jobId = ctx.rand.uuidv4();
const job = await ctx.objectClient(cronJob, jobId).initiate(request);
return `Job created with ID ${jobId} and next execution time ${job.next_execution_time}`;
},
},
});
export const cronJob = restate.object({
name: "CronJob",
handlers: {
initiate: async (ctx: restate.ObjectContext, request: JobRequest): Promise => {
if (await ctx.get(JOB_STATE)) {
throw new TerminalError("Job already exists for this ID.");
}
return await scheduleNextExecution(ctx, request);
},
execute: async (ctx: restate.ObjectContext) => {
const jobState = await ctx.get(JOB_STATE);
if (!jobState) {
throw new TerminalError("Job not found.");
}
// execute the task
const { service, method, key, payload } = jobState.request;
if (payload) {
ctx.genericSend({
service,
method,
parameter: payload,
key,
inputSerde: serde.json,
});
} else {
ctx.genericSend({
service,
method,
parameter: undefined,
key,
inputSerde: serde.empty,
});
}
await scheduleNextExecution(ctx, jobState.request);
},
cancel: async (ctx: restate.ObjectContext) => {
// Cancel the next execution
const jobState = await ctx.get(JOB_STATE);
if (jobState) {
ctx.cancel(jobState.next_execution_id);
}
// Clear the job state
ctx.clearAll();
},
getInfo: restate.handlers.object.shared(async (ctx: restate.ObjectSharedContext) => {
return ctx.get(JOB_STATE);
}),
},
});
const scheduleNextExecution = async (
ctx: restate.ObjectContext,
request: JobRequest,
): Promise => {
// Parse cron expression
// Persist current date in Restate for deterministic replay
const currentDate = await ctx.date.now();
let interval;
try {
interval = CronExpressionParser.parse(request.cronExpression, { currentDate });
} catch (e) {
throw new TerminalError(`Invalid cron expression: ${(e as Error).message}`);
}
const next = interval.next().toDate();
const delay = next.getTime() - currentDate;
// Schedule next execution for this job
const thisJobId = ctx.key; // This got generated by the CronJobInitiator
const handle = ctx.objectSendClient(cronJob, thisJobId, { delay }).execute();
// Store the job information
const jobState = {
request,
next_execution_time: next.toString(),
next_execution_id: await handle.invocationId,
};
ctx.set(JOB_STATE, jobState);
return jobState;
};
```
```java Java expandable {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/patterns-use-cases/src/main/java/my/example/cron/Cron.java"} theme={null}
package my.example.cron;
import static com.cronutils.model.CronType.UNIX;
import com.cronutils.model.definition.CronDefinitionBuilder;
import com.cronutils.model.time.ExecutionTime;
import com.cronutils.parser.CronParser;
import dev.restate.common.Request;
import dev.restate.common.Target;
import dev.restate.sdk.Context;
import dev.restate.sdk.ObjectContext;
import dev.restate.sdk.SharedObjectContext;
import dev.restate.sdk.annotation.*;
import dev.restate.sdk.common.StateKey;
import dev.restate.sdk.common.TerminalException;
import dev.restate.serde.TypeTag;
import java.time.ZonedDateTime;
import java.util.Optional;
/*
* A distributed cron service built with Restate that schedules tasks based on cron expressions.
*
* Features:
* - Create cron jobs with standard cron expressions (e.g., "0 0 * * *" for daily at midnight)
* - Schedule any Restate service handler or virtual object method
* - Guaranteed execution with Restate's durability
* - Cancel and inspect running jobs
*
* Usage:
* 1. Send requests to CronInitiator.create() to start new jobs
* 2. Each job gets a unique ID and runs as a CronJob virtual object
* 3. Jobs automatically reschedule themselves after each execution
*/
public class Cron {
public record JobRequest(
String cronExpression, // e.g. "0 0 * * *" (every day at midnight)
String service,
String method, // Handler to execute with this schedule
Optional key, // Optional Virtual Object key of the task to call
Optional payload) {} // Optional data to pass to the handler
public record JobInfo(JobRequest request, String nextExecutionTime, String nextExecutionId) {}
@Name("CronJobInitiator")
@Service
public static class JobInitiator {
@Handler
public String create(Context ctx, JobRequest request) {
// Create a new job ID and initiate the cron job object for that ID
// We can then address this job object by its ID
var jobId = ctx.random().nextUUID().toString();
var cronJob = CronJobClient.fromContext(ctx, jobId).initiate(request).await();
return String.format(
"Job created with ID %s and next execution time %s", jobId, cronJob.nextExecutionTime());
}
}
@Name("CronJob")
@VirtualObject
public static class Job {
private final StateKey JOB_STATE = StateKey.of("job-state", JobInfo.class);
private final CronParser PARSER =
new CronParser(CronDefinitionBuilder.instanceDefinitionFor(UNIX));
@Handler
public JobInfo initiate(ObjectContext ctx, JobRequest request) {
if (ctx.get(JOB_STATE).isPresent()) {
throw new TerminalException("Job already exists for this ID");
}
return scheduleNextExecution(ctx, request);
}
@Handler
public void execute(ObjectContext ctx) {
JobRequest request =
ctx.get(JOB_STATE).orElseThrow(() -> new TerminalException("Job not found")).request;
executeTask(ctx, request);
scheduleNextExecution(ctx, request);
}
@Handler
public void cancel(ObjectContext ctx) {
ctx.get(JOB_STATE)
.ifPresent(jobState -> ctx.invocationHandle(jobState.nextExecutionId).cancel());
// Clear the job state
ctx.clearAll();
}
@Shared
public Optional getInfo(SharedObjectContext ctx) {
return ctx.get(JOB_STATE);
}
private void executeTask(ObjectContext ctx, JobRequest job) {
Target target =
(job.key.isPresent())
? Target.virtualObject(job.service, job.method, job.key.get())
: Target.service(job.service, job.method);
var request =
(job.payload.isPresent())
? Request.of(
target, TypeTag.of(String.class), TypeTag.of(Void.class), job.payload.get())
: Request.of(target, new byte[0]);
ctx.send(request);
}
private JobInfo scheduleNextExecution(ObjectContext ctx, JobRequest request) {
// Parse cron expression
ExecutionTime executionTime;
try {
executionTime = ExecutionTime.forCron(PARSER.parse(request.cronExpression));
} catch (IllegalArgumentException e) {
throw new TerminalException("Invalid cron expression: " + e.getMessage());
}
// Calculate next execution time
var now = ctx.run(ZonedDateTime.class, ZonedDateTime::now);
var delay =
executionTime
.timeToNextExecution(now)
.orElseThrow(() -> new TerminalException("Cannot determine next execution time"));
var next =
executionTime
.nextExecution(now)
.orElseThrow(() -> new TerminalException("Cannot determine next execution time"));
// Schedule next execution for this job
String thisJobId = ctx.key(); // This got generated by the CronJobInitiator
var handle = CronJobClient.fromContext(ctx, thisJobId).send().execute(delay);
// Save job state
var jobState = new JobInfo(request, next.toString(), handle.invocationId());
ctx.set(JOB_STATE, jobState);
return jobState;
}
}
}
```
```go Go expandable {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/patterns-use-cases/src/cron/cron.go"} theme={null}
package main
import (
"fmt"
"time"
restate "github.com/restatedev/sdk-go"
"github.com/robfig/cron/v3"
)
// JobRequest represents the structure for creating a cron job
type JobRequest struct {
CronExpression string `json:"cronExpression"` // The cron expression e.g. "0 0 * * *" (every day at midnight)
Service string `json:"service"`
Method string `json:"method"` // Handler to execute with this schedule
Key string `json:"key,omitempty"` // Optional: Virtual Object key of task to call
Payload string `json:"payload,omitempty"` // Optional payload to pass to the handler
}
// JobInfo represents the stored job information
type JobInfo struct {
Request JobRequest `json:"request"`
NextExecutionTime time.Time `json:"next_execution_time"`
NextExecutionID string `json:"next_execution_id"`
}
const JOB_STATE = "job-state" // K/V state key for storing job info in the Restate
// CronJobInitiator service for creating new cron jobs
//
// A distributed cron service built with Restate that schedules tasks based on cron expressions.
//
// Features:
// - Create cron jobs with standard cron expressions (e.g., "0 0 * * *" for daily at midnight)
// - Schedule any Restate service handler or virtual object method
// - Guaranteed execution with Restate's durability
// - Cancel and inspect running jobs
//
// Usage:
// 1. Send requests to CronJobInitiator.Create() to start new jobs
// 2. Each job gets a unique ID and runs as a CronJob virtual object
// 3. Jobs automatically reschedule themselves after each execution
type CronJobInitiator struct{}
func (CronJobInitiator) Create(ctx restate.Context, req JobRequest) (string, error) {
// Create a new job ID and initiate the cron job object for that ID
// We can then address this job object by its ID
jobID := restate.Rand(ctx).UUID().String()
fmt.Printf("Creating new cron job with ID %s for service %s and method %s", jobID, req.Service, req.Method)
job, err := restate.Object[*JobInfo](ctx, "CronJob", jobID, "Initiate").Request(req)
if err != nil {
return "", err
}
return fmt.Sprintf("Job created with ID %s and next execution time %s",
jobID, job.NextExecutionTime.Format(time.RFC3339)), nil
}
type CronJob struct{}
func (CronJob) Initiate(ctx restate.ObjectContext, req JobRequest) (*JobInfo, error) {
// Check if jobState already exists
jobState, err := restate.Get[*JobInfo](ctx, JOB_STATE)
if err != nil {
return nil, err
}
if jobState != nil {
return nil, restate.TerminalErrorf("jobState already exists for this ID", 500)
}
return scheduleNextExecution(ctx, req)
}
func (CronJob) Execute(ctx restate.ObjectContext) error {
// Get the job information
jobState, err := restate.Get[*JobInfo](ctx, JOB_STATE)
if err != nil {
return err
}
if jobState == nil {
return restate.TerminalErrorf("job not found", 500)
}
// Add key if it's a virtual object call
req := jobState.Request
fmt.Printf("Executing job with ID: %s for service %s for method %s", restate.Key(ctx), req.Service, req.Method)
if req.Key != "" {
restate.ObjectSend(ctx, req.Service, req.Key, req.Method).Send(req.Payload)
} else {
restate.ServiceSend(ctx, req.Service, req.Method).Send(req.Payload)
}
// Schedule the next execution
_, err = scheduleNextExecution(ctx, req)
return err
}
func (CronJob) Cancel(ctx restate.ObjectContext) error {
// Get the job to cancel the next execution
job, err := restate.Get[*JobInfo](ctx, JOB_STATE)
if err != nil {
return err
}
if job == nil {
return restate.TerminalErrorf("job not found for cancellation", 404)
}
restate.CancelInvocation(ctx, job.NextExecutionID)
restate.ClearAll(ctx)
return nil
}
func (CronJob) GetInfo(ctx restate.ObjectSharedContext) (*JobInfo, error) {
return restate.Get[*JobInfo](ctx, JOB_STATE)
}
// scheduleNextExecution calculates and schedules the next execution of the cron job
func scheduleNextExecution(ctx restate.ObjectContext, req JobRequest) (*JobInfo, error) {
// Parse cron expression
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
schedule, err := parser.Parse(req.CronExpression)
if err != nil {
return nil, restate.TerminalErrorf("invalid cron expression: %v", err, 500)
}
// Get current time deterministically from Restate
currentTime, _ := restate.Run(ctx, func(ctx restate.RunContext) (time.Time, error) {
return time.Now(), nil
})
// Calculate next execution time
nextTime := schedule.Next(currentTime)
delay := nextTime.Sub(currentTime)
// Schedule next execution for this job
thisJobID := restate.Key(ctx) // This got generated by the CronJobInitiator
handle := restate.ObjectSend(ctx, "CronJob", thisJobID, "Execute").Send(nil, restate.WithDelay(delay))
// Store the job information
jobState := &JobInfo{
Request: req,
NextExecutionTime: nextTime,
NextExecutionID: handle.GetInvocationId(),
}
restate.Set(ctx, JOB_STATE, jobState)
return jobState, nil
}
```
This contains two services: a `CronJobInitiator` Service, and a `CronJob` Virtual Object:
Bind both services to your endpoint, and register them.
Usage:
* Send requests to `CronJobInitiator.create()` to start new jobs with standard [cron expressions](https://www.baeldung.com/cron-expressions):
```json theme={null}
{
"cronExpression": "0 0 * * *", # E.g. run every day at midnight
"service": "TaskService", # Schedule any Restate handler
"method": "executeTask",
"key": "taskId", # Optional, Virtual Object key
"payload": "Hello midnight!"
}
```
* Each job gets a unique ID and runs as a CronJob Virtual Object.
* Jobs automatically reschedule themselves after each execution.
This pattern is implementable with any of our SDKs. We are still working on translating all patterns to all SDK languages.
If you need help with a specific language, please reach out to us via [Discord](https://discord.com/invite/skW3AZ6uGd) or [Slack](https://join.slack.com/t/restatecommunity/shared_invite/zt-2v9gl005c-WBpr167o5XJZI1l7HWKImA).
## Running the example
```bash TypeScript theme={null}
restate example typescript-patterns-use-cases && cd typescript-patterns-use-cases
```
```bash Java theme={null}
restate example java-patterns-use-cases && cd java-patterns-use-cases
```
```bash Go theme={null}
restate example go-patterns-use-cases && cd go-patterns-use-cases
```
```bash theme={null}
restate-server
```
```bash TypeScript theme={null}
npm install
npx tsx watch ./src/cron/task_service.ts
```
```bash Java theme={null}
./gradlew -PmainClass=my.example.cron.TaskService run
```
```bash Go theme={null}
go run ./src/cron
```
```bash theme={null}
restate deployments register localhost:9080
```
For example, run `executeTask` every minute:
```bash TypeScript theme={null}
curl localhost:8080/CronJobInitiator/create --json '{
"cronExpression": "* * * * *",
"service": "TaskService",
"method": "executeTask",
"payload": "Hello new minute!"
}'
```
```bash Java theme={null}
curl localhost:8080/CronJobInitiator/create --json '{
"cronExpression": "* * * * *",
"service": "TaskService",
"method": "executeTask",
"payload": "Hello new minute!"
}'
```
```bash Go theme={null}
curl localhost:8080/CronJobInitiator/Create --json '{
"cronExpression": "* * * * *",
"service": "TaskService",
"method": "executeTask",
"payload": "Hello new minute!"
}'
```
For example, or run `executeTask` at midnight:
```bash TypeScript theme={null}
curl localhost:8080/CronJobInitiator/create --json '{
"cronExpression": "0 0 * * *",
"service": "TaskService",
"method": "executeTask",
"payload": "Hello midnight!"
}'
```
```bash Java theme={null}
curl localhost:8080/CronJobInitiator/create --json '{
"cronExpression": "0 0 * * *",
"service": "TaskService",
"method": "executeTask",
"payload": "Hello midnight!"
}'
```
```bash Go theme={null}
curl localhost:8080/CronJobInitiator/Create --json '{
"cronExpression": "0 0 * * *",
"service": "TaskService",
"method": "executeTask",
"payload": "Hello midnight!"
}'
```
You can also use the cron service to execute handlers on Virtual Objects, by specifying the Virtual Object key in the request.
You will get back a response with the job ID.
Using the job ID, you can then get information about the job:
```bash TypeScript theme={null}
curl localhost:8080/CronJob/myJobId/getInfo
```
```bash Java theme={null}
curl localhost:8080/CronJob/myJobId/getInfo
```
```bash Go theme={null}
curl localhost:8080/CronJob/myJobId/GetInfo
```
Or cancel the job later:
```bash TypeScript theme={null}
curl localhost:8080/CronJob/myJobId/cancel
```
```bash Java theme={null}
curl localhost:8080/CronJob/myJobId/cancel
```
```bash Go theme={null}
curl localhost:8080/CronJob/myJobId/Cancel
```
In the UI, you can see how the tasks are scheduled, and how the state of the cron jobs is stored in Restate.
You can kill and restart any of the services or the Restate Server, and the scheduled tasks will still be there.
## Adapt to your use case
Note that this implementation is fully resilient, but you might need to make some adjustments to make this fit your use case:
* Take into account time zones.
* Adjust how you want to handle tasks that fail until the next task gets scheduled. With the current implementation, you would have concurrent executions of the same cron job (one retrying and the other starting up).
If you want to cancel the failing task when a new one needs to start, you can do the following: at the beginning of the `execute` call, retrieve the `next_execution_id` from the job state and check if it is completed by [attaching to it](/develop/ts/service-communication#re-attach-to-an-invocation) with [a timeout set to 0](/develop/ts/durable-timers#timers-and-timeouts). If it is not completed, [cancel it](/develop/ts/service-communication#cancel-an-invocation) and start the new iteration.
# Databases and Restate
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/guides/databases
Learn when and how to use databases in combination with Restate.
Restate is not only a system for resilience and durability in communication and orchestration, it can also store data like a database system, to enable writing long-lived stateful logic with strong out-of-the-box resilience.
To avoid confusion: In this page, we refer to the type of state that outlives a single workflow or durable handler execution, and that uses Restate’s [Virtual Objects](/foundations/services#virtual-object). This is a feature not available in typical workflow systems or durable execution frameworks.
This guide discusses thoughts on when to use Restate for that type of state, when to use a database, and how to integrate Restate and databases.
## What state to store in Restate, what to store in databases
State stored directly in Restate ([Virtual Objects](/foundations/services#virtual-object)) has a set of advantages:
* **Ultra robust**: No coordination between systems is needed. Instead, state access/updates directly participate in the durable execution logic (go through the same consensus log), making the state always aligned with the business logic: no lost updates, duplications of state changes, stale views of state, etc.
* **Simple correctness model**: The single-writer-per-key and linearizable consistency model means developers don’t need to worry about locks, race conditions, dirty reads, change visibility, zombie processes, etc.
* **Efficient**: Computation and access patterns to state align naturally, you cannot get into situations where multiple function executions try to access/update the same database entry and contend on locks or interfere with each other's transaction.
No write amplification through separate durability in an external system (instead state is simply committed with the durable execution journal data).
* **Serverless**: when Restate invokes a handler, it attaches the relevant state to the request. On serverless, for example AWS lambda, this means your state is locally available when the function executes, no access delays, synchronization, wait times, etc.
* **No additional dependency** - the K/V store is integrated in the core of Restate. You don't need to do anything extra to start using state when developing locally, or migrating the Restate app to cloud/production.
State stored directly in Restate (Virtual Objects) has some limitations:
* K/V interface with single-key transactions (though a single key can store a document with flexible structures)
* SQL is supported only for analytics / introspection, not for updates/transactions.
* Modifiable only from the Virtual Object, not from other services. Any modification needs to be sent as a request to the Virtual Object.
* While state can be exported in a standard way (psql client), it is managed by a less standard system, compared to some databases.
## When to use which?
Use a database:
* When you need complex access patterns, full SQL, text search, time-series analysis, etc.
* For core business data, like your user database, products database, history of transactions, etc. that you want to access from other services as well
Use Restate for any state requiring tight integration with function logic, resilience, and correctness:
* State that is part of transactional state machines (payment status, activation status, ...)
* Session state (shopping cart, ongoing orders, LLM context, AI agent context, ...)
* State that is part of distributed orchestration and coordination (e.g., versions/life-cycle of shared resources, transaction IDs, reference counts, distributed locks/semaphores/leases, ...)
* Agents / Digital-twins (agent/twin memory and context)
* State in event processing pipelines (aggregates, joins)
* State of control planes (desired cluster status, resource references, ...)
* Consistent state/metadata overlay over eventual consistent infra
## How to interact with Databases from Restate
This set of examples shows various patterns to access databases from Restate handlers.
```ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/patterns-use-cases/src/database/main.ts"} theme={null}
import * as restate from "@restatedev/restate-sdk";
import { Sequelize, Transaction } from "sequelize";
import { runQueryAs2pcTxn } from "./2phasecommit";
// ----------------------------------------------------------------------------
//
// This example shows various patterns to access databases from Restate
// handlers.
//
// The basics are:
//
// (1) You don't need to do anything special, you can just interact with
// your database the same way as from other microservices or
// workflow activities.
//
// (2) But you can use Restate's state, journal, and concurrency mechanisms
// as helpers to improve common access problems, solve race conditions,
// or avoid inconsistencies in the presence of retries, concurrent requests,
// or zombie processes.
//
// The below example illustrate this step by step.
//
// ----------------------------------------------------------------------------
type Row = {
userId: string; // the primary key
name: string;
address: string;
credits: number;
version?: number; // the row version (used by some access methods)
};
const db = new Sequelize("postgres://restatedb:restatedb@localhost:5432/exampledb", {
logging: false,
});
// ----------------------------------------------------------------------------
/**
* This service implements the database accesses similar to the way you
* might access the database from other microservices.
* It doesn't use any Restate features to help making the DB access consistent.
*/
const simpledbAccess = restate.service({
name: "simple",
handlers: {
/**
* Simple read from a database row.
* This is the same as it would be from any non-Restate service.
*/
read: async (_ctx: restate.Context, userid: string) => {
const [results, _meta] = await db.query(
`SELECT *
FROM users
WHERE userid = '${userid}'`,
);
return results.length > 0 ? results[0] : "(row not found)";
},
/**
* This performs a read in a Restate durable action, persisting the result.
* The read access is the same as in a non-Restate service, but the result
* is persisted in the execution journal.
*
* This is useful for example, if you have decisions/branches based on
* the result and want to ensure consistent and deterministic behavior
* on retries (where the value in the database might have changed
* in-between retries).
*/
durableRead: async (ctx: restate.Context, userid: string) => {
const credits = await ctx.run("read credits", async () => {
const [results, _metadata] = await db.query(
`SELECT credits
FROM users
WHERE userid = '${userid}'`,
);
return (results[0] as any)?.credits;
});
if (credits === 0) {
// Execute some conditional logic, e.g., alerting, sending reminder.
// Automatic retries (on failure) will follow this branch again (using
// Restate's journalled value for 'credits') so code in this branch will
// reliably run to conclusion
console.log("Found an account without balance, doing something about it...");
}
return credits;
},
/**
* Inserts a row into the database. No Restate-specific handling, this
* simply uses the database's primary key uniqueness to avoid duplicate
* entries.
*/
insert: async (_ctx: restate.Context, row: Row) => {
const { userId, name, address, credits } = row;
const [_, numInserted] = await db.query(
`INSERT INTO users (userid, name, address, credits, version)
VALUES ('${userId}', '${name}', '${address}', ${credits}, 0)
ON CONFLICT (userid) DO NOTHING`,
);
return numInserted === 1;
},
/**
* Updates a row in a database. This handler makes no specific effort to
* add idempotency, so it purely relies on database / application semantics.
* It is vulnerable to concurrent updates overwriting each other, but can make
* sense if this is fine for a specific type of (rare) update.
*
* For an example that use Restate to add consistency, see the examples
* further below.
*/
update: async (_ctx: restate.Context, update: { userId: string; newName: string }) => {
const { userId, newName } = update;
// Update row - we execute this statement directly, because it is the only
// step in this handler. If the handler contains multiple steps, wrap this in
// a `ctx.run( () => { ... } )` block to ensure it does not get re-executed once
// during retries of successive steps
const [_, meta] = await db.query(
`UPDATE users
SET name = '${newName}'
WHERE userID = '${userId}'`,
);
return (meta as any).rowCount === 1;
},
},
});
// ----------------------------------------------------------------------------
/*
* When updating single rows by primary key (on a database or a key/value store),
* you can use Virtual Objects to serialize access per key. This happens automatically
* if you access the database from a Virtual Object and use the same key for object and
* primary key in the database.
*
* Most databases internally serialize updates on the same row anyways (either through locks,
* or by detecting conflicts during when attempting to commit transactions and rolling back), so
* making the access sequential from the application can help to avoid lock congestion and
* avoid pathological degradation of optimistic concurrency control around contended keys.
*
* Furthermore, this update pattern makes it possible to use conditional updates with versions
* that don't get duplicated on retries, resulting in proper exactly-once semantics.
*/
const keyedDbAccess = restate.object({
name: "keyed",
handlers: {
/**
* Updates the credits for a row in a database.
* This variant makes no effort to ensure the update does not get duplicated.
*
* In practice, this will can reasonably safe, because there is a very small window
* time where the query gets re-executed after success, whihc is when the query succeeded,
* but Restate did not see the completion of the handler.
*
* Note that this is especially rare, since the single-writer-per-key semantics of the
* Virtual Object ensure that no contention on rows can happen in the database.
*/
update: async (ctx: restate.ObjectContext, credits: number) => {
const userid = ctx.key;
await db.query(
`UPDATE users
SET credits = credits + ${credits}
WHERE userid = '${userid}'`,
);
},
/**
* Updates the 'credits' field and uses the 'version' number to make the update
* idempotent.
*
* The approach is to separate reads (current credits, version) and
* updates into separate queries. The reads are kept in the execution journal, and
* the updates are conditional on the fact that the version did not change in between.
* This is sometimes referred to as a _semantic lock pattern_.
*
* Because the reads go through a `ctx.run` step (strong consensus inside Restate),
* it is guaranteed that the same invocation will always work off of the same version.
* Regardless of partial failures/retries/zombies/network partitions, the code will
* always observe a single version.
*
* If all updates to the row go through this handler (or other handlers in this
* Virtual Object that use the versioning scheme) then no duplicates are possible.
*/
updateConditional: async (ctx: restate.ObjectContext, addCredits: number) => {
const userid = ctx.key;
// first read the current credits and the version.
// Because the reads go through a `ctx.run` step (strong consensus), the
// a single invocation and all if its retries can never see different values
// for version and credits.
const [credits, version] = await ctx.run("read credits, version", async () => {
const [results, _] = await db.query(
`SELECT credits, version
FROM users
WHERE userid = '${userid}'`,
);
if (results.length !== 1) {
throw new restate.TerminalError(`No single row with userid = '${userid}'`);
}
const row = results[0] as any;
return [Number(row.credits), Number(row.version)];
});
// Make the update conditional on the fact that the version is still the same (see
// 'AND version = ...'). If the version is no longer the same, it means that a
// previous execution attempt updated this and crashed before completing the handler).
// It cannot be completed by a concurrent query, because all operations on the userid
// are serialized through the Virtual Object.
const [_, meta] = await db.query(
`UPDATE users
SET credits = ${credits + addCredits},
version = ${version + 1}
WHERE userid = '${userid}'
AND version = ${version}`,
);
if ((meta as any).rowCount !== 1) {
console.debug("Update was not applied - version no longer matches.");
}
},
/**
* Reads a row from the database.
*
* This one is markes as a SHARED handler, which means it doesn't queue
* with the other invocations for this key. That way reads execute immediately
* and concurrently, while writes execute sequentially.
*/
read: restate.handlers.object.shared(async (ctx: restate.ObjectSharedContext) => {
const userid = ctx.key;
const [results, _meta] = await db.query(
`SELECT *
FROM users
WHERE userid = '${userid}'`,
);
return results[0] ?? "(row not found)";
}),
},
});
// ----------------------------------------------------------------------------
/**
* This service uses a second table in the database to remember idempotency tokens
* for each operation. The idempotency tokens are inserted into the database in the
* same transaction as the main operation.
*
* The code uses Restate's features to deterministically generate the tokens and
* to expire them after a day.
*/
const idempotencyKeyDbAccess = restate.service({
name: "idempotency",
handlers: {
/**
* Adds credits to a single user entry. This operation is _idempotent_ within a
* time window of a day.
*
* This handler uses a second table in the database to remember idempotency tokens
* for each update. The idempotency tokens are inserted into the database in the
* same transaction as the update.
*
* The code uses Restate's features to deterministically generate the tokens and
* to expire them after a day.
*/
update: async (ctx: restate.Context, update: { userId: string; addCredits: number }) => {
// create a persistent idempotency key. we use Restate's random number generator,
// because that is the most efficient way to generate a durable deterministic ID.
const idempotencyKey = ctx.rand.uuidv4();
// expire the idempotency key from the database after one day
// by scheduling a call to the handler that deletes the key
ctx
.serviceSendClient(idempotencyKeyDbAccess)
.expireIdempotencyKey(idempotencyKey, restate.rpc.sendOpts({ delay: { days: 1 } }));
// checking the existence of the idempotency key and making the update happens
// in one database transaction
const tx = await db.transaction({
isolationLevel: Transaction.ISOLATION_LEVELS.SERIALIZABLE,
});
try {
// first test whether the idempotency token exists by checking whether
// we can insert one into the table (primary key constraint prevents
// duplicates)
const [_tokenRows, numInserted] = await db.query(
`INSERT INTO user_idempotency (id)
VALUES ('${idempotencyKey}')
ON CONFLICT (id) DO NOTHING`,
{ transaction: tx },
);
if (numInserted !== 1) {
// idempotency key was inserted before, so this must be a retry
await tx.rollback();
return;
}
// now make the actual update
await db.query(
`UPDATE users
SET credits = credits + ${update.addCredits}
WHERE userid = '${update.userId}'`,
{ transaction: tx },
);
// if everything succeeds, commit idempotency token and update atomically
await tx.commit();
} catch (e) {
// speed up release of DB locks by eagerly aborting transaction
await tx.rollback();
throw e;
}
},
/**
* Deletes the idempotency key from the database.
* This handler is involked as a scheduled call from the handler that performs
* the update.
*/
expireIdempotencyKey: async (_ctx: restate.Context, key: string) => {
await db.query(
`DELETE FROM user_idempotency
WHERE id = '${key}'`,
);
},
},
});
// ----------------------------------------------------------------------------
/**
* Exactly-once updates to the database using a 2-phase commit integration between
* Restate and PostegreSQL.
*
* This uses PostgreSQL's PREPARE TRANSACTION feature to ensure that Restate
* kicks off a transaction once and only once, and it thus works without any
* additional idempotency or versioning mechanisms.
* BUT: It requires the Postgres database to enable prepared transactions in the
* configuration via `max_prepared_transactions = 100`.
*
* This is a _good use case_ of the 2-phase-commit protocol, because it is not
* the (rightfully) criticized use case of running distributed transactions across
* services and thus couple availability and liveliness of multiple services together.
* This case here keeps the same participants and dependencies and only uses 2pc as a
* way to split a single-participant transaction into two stages so that Restate can
* avoid executing the query against the database multiple times under retries.
*
* NOTE: This code currently works only on services which have a bidi streaming connection,
* (so any service deployed as http/2 or http1 bidi) due to the fact that it needs to
* differentiate re-tries of blocks from first executions, which currently works only
* for streaming connections. This is simply a missing feature and not a fundamental
* limitation.
*/
const twoPhaseCommitDbAccess = restate.service({
name: "twoPhaseCommit",
handlers: {
update: async (ctx: restate.Context, update: { userId: string; addCredits: number }) => {
const { userId, addCredits } = update;
const query = `UPDATE users
SET credits = credits + ${addCredits}
WHERE userid = '${userId}'`;
await runQueryAs2pcTxn(ctx, db, query);
},
},
});
// ----------------------------------------------------------------------------
restate.serve({
services: [simpledbAccess, keyedDbAccess, idempotencyKeyDbAccess, twoPhaseCommitDbAccess],
port: 9080,
});
```
### Two-phase commit
```ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/patterns-use-cases/src/database/2phasecommit.ts"} theme={null}
import * as restate from "@restatedev/restate-sdk";
import { randomUUID } from "node:crypto";
import { Sequelize, Transaction } from "sequelize";
/**
* Example 2-phase-commit integration library for Restate and PostegreSQL.
*
* This uses PostgreSQL's PREPARE TRANSACTION feature to ensure that Restate
* kicks off a transaction once and only once.
*
* Postgres' interface is far from optimal here, for example it does not provide a way
* to block/fence off discarded transaction IDs or establish other relationship between
* transaction IDs. As a result, this code is needs to be very aggressive in cleaning
* up previous transactions, to avoid leaving database locks in place.
*
* This code currently works only on services which have a bidi streaming connection !!!
*/
export async function runQueryAs2pcTxn(
ctx: restate.Context,
dbConnection: Sequelize,
query: string,
) {
const action = async (db: Sequelize, txn: Transaction) => {
await db.query(query, { transaction: txn });
};
return runAs2pcTxn(ctx, dbConnection, action);
}
/**
* See docs for {@link runQueryAs2pcTxn}.
*
* IMPORTANT: When using function, all queries issues must make use of the `tnx` object:
*
* await db.query(
* "UDATE ... SET ... WHERE ...",
* { transaction: txn }
* );
*
*/
export async function runAs2pcTxn(
ctx: restate.Context,
dbConnection: Sequelize,
action: (dbConnection: Sequelize, txn: Transaction) => Promise,
) {
// this code only works on a streaming bi-directional connection, see below.
checkRunsOnBidi(ctx);
// We loop here until we are are sure that there successfully made it through
// (1) running DB transaction, (2) preparing for commit (pre-commit), (3) recording
// that in the Restate journal.
// If we don't make it through the full squence in one execution attempt, we clear the
// pending transaction and try again under a different transaction ID. Successive re-tries
// and recoveries from failures also make sure they clean up the pending transaction.
// In the absence of failures, the first iteration will succeed.
let txnIdToCommit: string | undefined = undefined;
while (txnIdToCommit === undefined) {
// We generate the transaction ID and use a trick here to find out if we
// actually generated the ID, or whether the ID was restored from the journal.
// That is the reason this code only works on long-lived bidi connections (not Lambda),
// because on Lambda, every `ctx.run` block is only ack-ed through a re-invocation
// and journal replay.
let executedThisTime: boolean = false;
const txnId: string = await ctx.run("generate transaction id", () => {
executedThisTime = true;
return randomUUID();
});
// this is our cleanup hook for this specific transaction ID, to run in case
// we need to ensure the transaction is cleared
async function cleanup() {
try {
await dbConnection.query(`ROLLBACK PREPARED '${txnId}'`);
} catch (e: any) {
if (e.original?.code === "42704" && e instanceof Error) {
// code 42704 means "no txn with that ID found".
// => transaction was not prepared (crashed before) or already rolled back
// => we can ignore this
return;
}
console.error(e.message);
throw e;
}
}
try {
const txnRan = await ctx.run("run txn attempt (or cleanup old txn)", async () => {
// If we did not generate this transaction ID, a previous execution attempt did.
// We don't know whether that execution made it to the point that it already prepared
// the query (pre-committed) and crashed just before recording the completion of the
// `ctx.run` block in the Restate journal. As a result, we clean up that transaction ID
// to be on the safe side and not leave lingering database locks.
if (!executedThisTime) {
cleanup();
return false; // try again with a different txnId
}
// run:
// (1) the query transaction
// (2) pre-commit step (prepare transaction)
// (3) record this in the Restate journal
const txn = await dbConnection.transaction();
try {
// (1) run the main app-defined database query (or queries)
await action(dbConnection, txn);
// (2) pre-commit - after this step, the txn is immutable and not rolled back
// any more after closing/disposing/loss-of-connection
await dbConnection.query(`PREPARE TRANSACTION '${txnId}'`, {
transaction: txn,
});
// (3) if this durable block returns ('true' recoded in journal) step (3) will
// be completed
return true;
} catch (e) {
await txn.rollback();
throw e;
}
});
if (txnRan) {
txnIdToCommit = txnId;
}
} catch (e) {
// clean up in case the query didn't go through, to speed up release
// of possibly prepared txn
await cleanup();
throw e;
}
}
// now commit the prepared transaction - this step is idempotent, so if it was
// already committed, this does nothing
await ctx.run("commit prepared transaction", () =>
dbConnection.query(`COMMIT PREPARED '${txnIdToCommit}'`),
);
}
function checkRunsOnBidi(ctx: restate.Context): boolean {
// this needs to be replaced by the actual check, which needs an additional
// property to be exposed in Restate
return true; // dummy value for now
}
```
## Running the examples
This is purely optional, the example code and comments document the behavior well.
Running the example can be interesting, though, if you want to play with specific failure scenarios, like pausing/killing processes at specific points and observe the behavior.
[Check out the readme of the example on how to run the example.](https://github.com/restatedev/examples/blob/main/typescript/patterns-use-cases/README.md#database-interaction-patterns)
# Durable Webhooks
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/guides/durable-webhooks
Process webhook events from external services with exactly-once delivery guarantees.
Restate handlers can be used as **durable processors of webhook events**.
## How does Restate help?
* Restate persists all incoming events, and ensures that they are **processed exactly once**, across failures and restarts. Restate guarantees your handler runs till completion.
* Let Restate **deduplicate** events on an [idempotency key](/invoke/http#invoke-a-handler-idempotently). If the sender of the event retries, Restate will not process the event again.
* Use any of Restate's [**durable SDK constructs**](/concepts/durable_building_blocks) when processing the events: durable calls/messaging to other services, durable timers, scheduling tasks, K/V state, concurrency guarantees etc.
* Any handler can be a durable webhook endpoint. **No need to do anything special or extra!**
Just point your webhook endpoint to your handler: `restate:8080/MyService/myHandler`.
## Example
This example processes webhook callbacks from a payment provider.
The payment provider notifies us about payment success or failure of invoices by sending webhook events to our handler.
The handler then routes the event to the correct processor via a one-way message.
```ts TypeScript {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/patterns-use-cases/src/webhookcallbacks/webhook_callback_router.ts?collapse_prequel"} theme={null}
const webhookCallbackRouter = restate.service({
name: "WebhookCallbackRouter",
handlers: {
// Any handler can be a durable webhook processor that never loses events
// You don't need to do anything special for this.
// Just point your webhook to the handler endpoint: restate:8080/WebhookCallbackRouter/onStripeEvent
onStripeEvent: async (ctx: restate.Context, event: StripeEvent) => {
if (event.type === "invoice.payment_failed") {
ctx.objectSendClient(PaymentTracker, event.data.object.id).onPaymentFailed(event);
} else if (event.type === "invoice.payment_succeeded") {
ctx.objectSendClient(PaymentTracker, event.data.object.id).onPaymentSuccess(event);
}
},
},
});
restate.serve({
services: [webhookCallbackRouter],
port: 9080,
});
```
```go Go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/patterns-use-cases/src/webhookcallbacks/callbackrouter.go?collapse_prequel"} theme={null}
type WebhookCallbackRouter struct{}
// Any handler can be a durable webhook processor that never loses events
// You don't need to do anything special for this.
// Just point your webhook to the handler endpoint: restate:8080/WebhookCallbackRouter/OnStripeEvent
func (WebhookCallbackRouter) OnStripeEvent(ctx restate.Context, event StripeEvent) error {
if event.Type == "invoice.payment_failed" {
restate.ObjectSend(ctx, "PaymentTracker", "onPaymentFailed", event.Data.Object.ID).
Send(event)
} else if event.Type == "invoice.payment_succeeded" {
restate.ObjectSend(ctx, "PaymentTracker", "onPaymentSuccess", event.Data.Object.ID).
Send(event)
}
return nil
}
```
This pattern is implementable with any of our SDKs. We are still working on translating all patterns to all SDK languages.
If you need help with a specific language, please reach out to us via [Discord](https://discord.com/invite/skW3AZ6uGd) or [Slack](https://join.slack.com/t/restatecommunity/shared_invite/zt-2v9gl005c-WBpr167o5XJZI1l7HWKImA).
# Error Handling
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/guides/error-handling
Learn how to handle transient and terminal errors in your applications.
Restate handles retries for failed invocations. By default, Restate infinitely retries all errors with an exponential backoff strategy.
This guide helps you fine-tune the retry behavior for your use cases.
## Infrastructure errors (transient) vs. application errors (terminal)
In Restate, we distinguish between two types of errors: transient errors and terminal errors.
* **Transient errors** are temporary and can be retried. They are typically caused by infrastructure issues (network problems, service overload, API unavailability,...).
* **Terminal errors** are permanent and should not be retried. They are typically caused by application logic (invalid input, business rule violation, ...).
## Handling transient errors via retries
Restate assumes by default that all errors are transient errors and therefore retryable.
If you do not want an error to be retried, you need to specifically label it as a terminal error ([see below](#application-errors-terminal)).
Restate lets you configure the retry strategy at different levels: for the invocation and at the run-block-level.
### At Invocation level
Restate retries executing invocations that can't make any progress according to a retry policy.
This policy controls the retry intervals, the maximum number of attempts and whether to **pause** or **kill** the invocation when the attempts are exhausted.
The retry policy can be set on each individual handler, or for all the handlers of a service, or globally in the Restate configuration directly.
To configure the retry policy on a service/handler level, check [retry service configuration](/services/configuration#retries).
Via the [`restate-server` configuration file](/server/configuration):
```toml restate.toml theme={null}
[default-retry-policy]
initial-interval = "10s"
max-attempts = 100
max-interval = "60s"
```
Then run the Restate Server with:
```shell theme={null}
restate-server --config-file restate.toml
```
Or you can set these options via env variables:
```dotenv theme={null}
RESTATE_DEFAULT_RETRY_POLICY__INITIAL_INTERVAL="10s"
RESTATE_DEFAULT_RETRY_POLICY__MAX_ATTEMPTS=100
RESTATE_DEFAULT_RETRY_POLICY__MAX_INTERVAL="10s"
```
This retry policy will retry the invocation 100 times, after which the invocation will be paused if no progress can be made. To resume a paused invocation, check the paragraph below.
You can also retry forever, without ever pausing or killing the invocation:
```dotenv theme={null}
RESTATE_DEFAULT_RETRY_POLICY__MAX_ATTEMPTS=unlimited
```
Check the [configuration documentation](/server/configuration) and the [`default-retry-policy` reference](/references/server-config#param-default-retry-policy).
When a retry policy is unset, Restate by default will retry undefinitely, alike setting `max-attempts = "unlimited"`.
### At the Run-Block-Level
Handlers use run blocks to execute non-deterministic actions, often involving other systems and services (API call, DB write, ...).
These run blocks are especially prone to transient failures, and you might want to configure a specific retry policy for them.
```ts TypeScript {"CODE_LOAD::ts/src/guides/retries.ts#here"} theme={null}
const myRunRetryPolicy = {
initialRetryInterval: { milliseconds: 500 },
retryIntervalFactor: 2,
maxRetryInterval: { seconds: 1 },
maxRetryAttempts: 5,
maxRetryDuration: { seconds: 1 },
};
await ctx.run("write", () => writeToOtherSystem(), myRunRetryPolicy);
```
```python Python {"CODE_LOAD::python/src/guides/retries.py#here"} theme={null}
await ctx.run_typed(
"write",
write_to_other_system,
restate.RunOptions(
# Max number of retry attempts to complete the action.
max_attempts=3,
# Max duration for retrying, across all retries.
max_retry_duration=timedelta(seconds=10),
)
)
```
```java Java {"CODE_LOAD::java/src/main/java/guides/RetryRunService.java#here"} theme={null}
RetryPolicy myRunRetryPolicy =
RetryPolicy.exponential(Duration.ofMillis(500), 2)
.setMaxDelay(Duration.ofSeconds(10))
.setMaxAttempts(10)
.setMaxDuration(Duration.ofMinutes(5));
ctx.run("my-run", myRunRetryPolicy, () -> writeToOtherSystem());
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/guides/RetryRunService.kt#here"} theme={null}
val myRunRetryPolicy = retryPolicy {
initialDelay = 5.seconds
exponentiationFactor = 2.0f
maxDelay = 60.seconds
maxAttempts = 10
maxDuration = 5.minutes
}
ctx.runBlock("write", myRunRetryPolicy) { writeToOtherSystem() }
```
```go Go {"CODE_LOAD::go/guides/retries.go#here"} theme={null}
result, err := restate.Run(ctx,
func(ctx restate.RunContext) (string, error) {
return writeToOtherSystem()
},
// After 10 seconds, give up retrying
restate.WithMaxRetryDuration(time.Second*10),
// On the first retry, wait 100 milliseconds before next attempt
restate.WithInitialRetryInterval(time.Millisecond*100),
// Grow retry interval with factor 2
restate.WithRetryIntervalFactor(2.0),
)
if err != nil {
return err
}
```
Note that these retries are coordinated and initiated by the Restate Server.
So the handler goes through the regular retry cycle of suspension and re-invocation.
If you set a maximum number of attempts, then the run block will fail with a TerminalException once the retries are exhausted.
Service-level retry policies are planned and will come soon.
## Application errors (terminal)
By default, Restate infinitely retries all errors.
In some cases, you might not want to retry an error (e.g. because of business logic, because the issue is not transient, ...).
For these cases you can throw a terminal error. Terminal errors are permanent and are not retried by Restate.
You can throw a terminal error as follows:
```ts TypeScript {"CODE_LOAD::ts/src/develop/error_handling.ts#terminal"} theme={null}
throw new TerminalError("Something went wrong.", { errorCode: 500 });
```
```python Python {"CODE_LOAD::python/src/develop/error_handling.py#terminal"} theme={null}
from restate.exceptions import TerminalError
raise TerminalError("Something went wrong.")
```
```java Java {"CODE_LOAD::java/src/main/java/develop/ErrorHandling.java#here"} theme={null}
throw new TerminalException(500, "Something went wrong");
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/ErrorHandling.kt#here"} theme={null}
throw TerminalException(500, "Something went wrong")
```
```go Go {"CODE_LOAD::go/develop/errorhandling.go#here"} theme={null}
return restate.TerminalError(fmt.Errorf("Something went wrong."), 500)
```
```rust Rust {"CODE_LOAD::rust/src/guides/retries.rs#terminal_error"} theme={null}
Err(TerminalError::new("This is a terminal error"))
```
You can throw terminal errors from any place in your handler, including run blocks.
Unless catched, terminal errors stop the execution and are propagated back to the caller.
If the caller is another Restate service, the terminal error will propagate across RPCs, and will get thrown at the line where the RPC was made.
If this is not caught, it will propagate further up the call stack until it reaches the original caller.
You can catch terminal errors just like any other error, and build control flow around this.
For example, the catch block can run undo actions for the actions you did earlier in your handler, to bring it to a consistent state before rethrowing the terminal error.
For example, to catch a terminal error of a run block:
```ts TypeScript {"CODE_LOAD::ts/src/guides/retries.ts#catch"} theme={null}
try {
// Fails with a terminal error after 3 attempts or if the function throws one
await ctx.run("write", () => writeToOtherSystem(), {
maxRetryAttempts: 3,
});
} catch (e) {
if (e instanceof restate.TerminalError) {
// Handle the terminal error: undo previous actions and
// propagate the error back to the caller
}
throw e;
}
```
```python Python {"CODE_LOAD::python/src/guides/retries.py#catch"} theme={null}
try:
# Fails with a terminal error after 3 attempts or if the function throws one
await ctx.run_typed("write", write_to_other_system, restate.RunOptions(max_attempts=3))
except TerminalError as err:
# Handle the terminal error: undo previous actions and
# propagate the error back to the caller
raise err
```
```java Java {"CODE_LOAD::java/src/main/java/guides/RetryRunService.java#catch"} theme={null}
try {
// Fails with a terminal error after 3 attempts or if the function throws one
ctx.run("my-run", RetryPolicy.defaultPolicy().setMaxAttempts(3), () -> writeToOtherSystem());
} catch (TerminalException e) {
// Handle the terminal error: undo previous actions and
// propagate the error back to the caller
throw e;
}
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/guides/RetryRunService.kt#catch"} theme={null}
try {
// Fails with a terminal error after 3 attempts or if the function throws one
ctx.runBlock(
"write",
RetryPolicy(
initialDelay = 500.milliseconds, maxAttempts = 3, exponentiationFactor = 2.0f)) {
writeToOtherSystem()
}
} catch (e: TerminalException) {
// Handle the terminal error: undo previous actions and
// propagate the error back to the caller
throw e
}
```
```go Go {"CODE_LOAD::go/guides/retries.go#catch"} theme={null}
result, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
return writeToOtherSystem()
})
if err != nil {
if restate.IsTerminalError(err) {
// Handle the terminal error: undo previous actions and
// propagate the error back to the caller
}
return err
}
```
```rust Rust {"CODE_LOAD::rust/src/guides/retries.rs#catch"} theme={null}
// Fails with a terminal error after 3 attempts or if the function throws one
if let Err(e) = ctx
.run(|| write_to_other_system())
.retry_policy(RunRetryPolicy::default().max_attempts(3))
.await
{
// Handle the terminal error: undo previous actions and
// propagate the error back to the caller
return Err(e);
}
```
When you throw a terminal error, you might need to undo the actions you did earlier in your handler to make sure that your system remains in a consistent state.
Have a look at our [sagas guide](/guides/sagas) to learn more.
## Cancellations are Terminal Errors
You can cancel invocations via the [CLI](/services/invocation/managing-invocations#cancel), UI and programmatically.
When you cancel an invocation, it throws a terminal error in the handler processing the invocation the next time it awaits a Promise or Future of a Restate Context action (e.g. run block, RPC, sleep,...; `RestatePromise` in TypeScript, `DurableFuture` in Java).
Unless caught, This terminal error will propagate up the call stack until it reaches the original caller.
Here again, the handler needs to have [compensation logic](/guides/sagas) in place to make sure the system remains in a consistent state, when you cancel an invocation.
## Timeouts between Restate and the service
There are two types of timeouts describing the behavior between Restate and the service.
### Inactivity timeout
When the Restate Server does not receive a next journal entry from a running handler within the inactivity timeout, it will ask the handler to suspend.
This timer guards against stalled service/handler invocations. Once it expires, Restate triggers a graceful termination by asking the service invocation to suspend (which preserves intermediate progress).
By default, the inactivity timeout is set to one minute.
You can increase the inactivity timeout if you have long-running `ctx.run` blocks, that lead to long pauses between journal entries. Otherwise, this timeout might kill the ongoing execution.
### Abort timeout
This timer guards against stalled service/handler invocations that are supposed to terminate.
The abort timeout is started after the 'inactivity timeout' has expired and the service/handler invocation has been asked to gracefully terminate.
Once the timer expires, it will abort the service/handler invocation.
By default, the abort timeout is set to one minute.
This timer potentially interrupts user code.
If the user code needs longer to gracefully terminate, then this value needs to be set accordingly.
If you have long-running `ctx.run` blocks, you need to increase both timeouts to prevent the handler from terminating prematurely.
### Configuring the timeouts
As with the retry policy, you can configure these timeouts on specific handlers, on all the handlers of a service, or globally in the Restate configuration directly.
To configure these timeouts on a service/handler level, check [timeouts service configuration](/services/configuration#timeouts).
Via the [`restate-server` configuration file](/server/configuration):
```toml restate.toml theme={null}
[worker.invoker]
inactivity-timeout = "1m"
abort-timeout = "1m"
```
```shell theme={null}
restate-server --config-file restate.toml
```
Both timeouts follow the [jiff](https://docs.rs/jiff/latest/jiff/struct.Span.html) format.
Or set it [via environment variables](/server/configuration#environment-variables), for example:
```shell theme={null}
RESTATE_WORKER__INVOKER__INACTIVITY_TIMEOUT=5m \
RESTATE_WORKER__INVOKER__ABORT_TIMEOUT=5m \
restate-server
```
## Common patterns
These are some common patterns for handling errors in Restate:
### Sagas
Have a look at the [sagas guide](/guides/sagas) to learn how to revert your system back to a consistent state after a terminal error.
Keep track of compensating actions throughout your business logic and apply them in the catch block after a terminal error.
### Dead-letter queue
A [dead-letter queue (DLQ)](https://aws.amazon.com/what-is/dead-letter-queue/) is a queue where you can send messages that could not be processed due to errors.
You can implement this in Restate by wrapping your handler in a try-catch block. In the catch block you can forward the failed invocation to a DLQ Kafka topic or a catch-all handler which for example reports them or backs them up.
Some errors might happen before the handler code gets invoked/starts running (e.g. service does not exist, request decoding errors in SDK HTTP server, ...).
By default, Restate fails these requests with `400`.
Handle these as follows:
* In case the caller waited for the response of the failed call, the caller can handle the propagation to the DLQ.
* If the caller did not wait for the response (one-way send), you would lose these messages.
* Decoding errors can be caught by doing the decoding inside the handler.
The called handler then takes raw input and does the decoding and validation itself.
In this case, it would be included in the try-catch block which would do the dispatching:
```ts TypeScript {"CODE_LOAD::ts/src/guides/retries.ts#raw"} theme={null}
myHandler: async (ctx: restate.Context) => {
try {
const rawRequest = ctx.request().body;
const decodedRequest = decodeRequest(rawRequest);
// ... rest of your business logic ...
} catch (e) {
if (e instanceof restate.TerminalError) {
// Propagate to DLQ/catch-all handler
}
throw e;
}
},
```
```python Python {"CODE_LOAD::python/src/guides/retries.py#raw"} theme={null}
@my_service.handler()
async def my_handler(ctx: Context):
try:
raw_request = ctx.request().body
decoded_request = decode_request(raw_request)
# ... rest of your business logic ...
except TerminalError as err:
# Propagate to DLQ/catch-all handler
raise err
```
```java Java {"CODE_LOAD::java/src/main/java/guides/RetryRunService.java#raw"} theme={null}
@Handler
public void myHandler(Context ctx, @Accept("*/*") @Raw byte[] request) {
try {
var decodedRequest = decodeRequest(request);
// ... rest of your business logic ...
} catch (TerminalException e) {
// Propagate to DLQ/catch-all handler
}
}
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/guides/RetryRunService.kt#raw"} theme={null}
@Handler
suspend fun myHandler(ctx: Context, @Accept("*/*") @Raw request: ByteArray) {
try {
val decodedRequest = decodeRequest(request)
// ... rest of your business logic ...
} catch (e: TerminalException) {
// Propagate to DLQ/catch-all handler
throw e
}
}
```
```go Go {"CODE_LOAD::go/guides/retries.go#raw"} theme={null}
func (MyService) myHandler(ctx restate.Context) (string, error) {
rawRequest := ctx.Request().Body
decodedRequest, err := decodeRequest(rawRequest)
if err != nil {
if restate.IsTerminalError(err) {
// Propagate to DLQ/catch-all handler
}
return "", err
}
// ... rest of your business logic ...
return decodedRequest, nil
}
```
```rust Rust {"CODE_LOAD::rust/src/guides/retries.rs#raw"} theme={null}
// Use Vec to represent a binary request
async fn my_handler(&self, ctx: Context<'_>, request: Vec) -> Result<(), HandlerError> {
let decoded_request = decode_request(&request)?;
// ... rest of you business logic ...
Ok(())
}
```
The other errors mainly occur due to misconfiguration of your setup (e.g. wrong service name, wrong handler name, forgot service registration...).
You cannot handle those.
### Timeouts for context actions
You can set timeouts for context actions like calls, awakeables, etc. to bound the time they take:
```ts TypeScript {"CODE_LOAD::ts/src/guides/retries.ts#timeout"} theme={null}
try {
// If the timeout hits first, it throws a `TimeoutError`.
// If you do not catch it, it will lead to a retry.
await ctx
.serviceClient(myService)
.myHandler("hello")
.orTimeout({ seconds: 5 });
const { id, promise } = ctx.awakeable();
// do something that will trigger the awakeable
await promise.orTimeout({ seconds: 5 });
} catch (e) {
if (e instanceof restate.TimeoutError) {
// Handle the timeout error
}
throw e;
}
```
```python Python {"CODE_LOAD::python/src/guides/retries.py#timeout"} theme={null}
match await restate.select(
greeting=ctx.service_call(my_service_handler, "value"),
timeout=ctx.sleep(timedelta(seconds=5)),
):
case ["greeting", greeting]:
print("Greeting:", greeting)
case ["timeout", _]:
print("Timeout occurred")
```
```java Java {"CODE_LOAD::java/src/main/java/guides/RetryRunService.java#timeout"} theme={null}
try {
// If the timeout hits first, it throws a `TimeoutError`.
// If you do not catch it, it will lead to a retry.
MyServiceClient.fromContext(ctx).myHandler("Hello").await(Duration.ofSeconds(5));
var awakeable = ctx.awakeable(Boolean.class);
// ...Do something that will trigger the awakeable
awakeable.await(Duration.ofSeconds(5));
} catch (TimeoutException e) {
// Handle the timeout error
}
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/guides/RetryRunService.kt#timeout"} theme={null}
try {
ctx.awakeable().withTimeout(5.seconds).await()
} catch (e: TimeoutException) {
// Handle the timeout
}
```
```go Go {"CODE_LOAD::go/guides/retries.go#timeout"} theme={null}
awakeable := restate.Awakeable[string](ctx)
timeout := restate.After(ctx, 5*time.Second)
selector := restate.Select(ctx, awakeable, timeout)
switch selector.Select() {
case awakeable:
result, err := awakeable.Result()
if err != nil {
return err
}
slog.Info("Awakeable resolved first with: " + result)
case timeout:
if err := timeout.Done(); err != nil {
return err
}
slog.Info("Timeout hit first")
}
```
# Guides
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/guides/index
Learn how to do common tasks with Restate.
export const ProductCard = ({img, href, title, description, model, type}) => {
return
;
};
## Recipes
## Development Guides
## Deployment Guides
## Integrations
# Restate-Kafka Quickstart
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/guides/kafka-quickstart
Learn how to connect your Restate service to a Kafka topic.
In this guide, you will learn how to connect your Restate service handler to a Kafka topic.
Restate lets you connect any handler to a Kafka topic, and invoke the handler for each event that arrives on the topic.
This way, you can use Restate to process events in a lightweight, flexible, transactional way.
Get the Greeter service template and run the service}>
You can choose any of the SDK languages for this quickstart.
You do not need to adapt the handler code to be able to read from Kafka.
Any handler can be connected to Kafka.
Let's start a Kafka cluster with a single broker, and create a topic named `greetings`.
You can run your Kafka cluster in your preferred way. Here, we will use Docker Compose to start a Kafka cluster.
Create a `docker-compose.yaml` file with the following content:
```yaml docker-compose.yaml expandable theme={null}
version: '3'
services:
broker:
image: confluentinc/cp-kafka:7.5.0
container_name: broker
ports:
- "9092:9092"
- "9101:9101"
environment:
KAFKA_BROKER_ID: 1
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://broker:29092,PLAINTEXT_HOST://localhost:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_NODE_ID: 1
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@broker:29093
KAFKA_LISTENERS: PLAINTEXT://broker:29092,CONTROLLER://broker:29093,PLAINTEXT_HOST://0.0.0.0:9092
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_LOG_DIRS: /tmp/kraft-combined-logs
CLUSTER_ID: MkU3OEVBNTcwNTJENDM2Qk
init-kafka:
image: confluentinc/cp-kafka:7.5.0
depends_on:
- broker
entrypoint: [ '/bin/sh', '-c' ]
command: |
"# blocks until kafka is reachable
kafka-topics --bootstrap-server broker:29092 --list
echo -e 'Creating kafka topics'
kafka-topics --bootstrap-server broker:29092 --create --if-not-exists --topic greetings --replication-factor 1 --partitions 1
echo -e 'Successfully created the following topics:'
kafka-topics --bootstrap-server broker:29092 --list"
```
Start the Kafka broker:
```shell theme={null}
docker compose up
```
Now, let's start the Restate Server and let it know about the Kafka cluster via the configuration file.
Store the following configuration in a file named `restate.toml`:
```toml restate.toml theme={null}
[[ingress.kafka-clusters]]
name = "my-cluster"
brokers = ["PLAINTEXT://localhost:9092"]
```
Start the Restate Server from the same location as the configuration file:
```shell theme={null}
restate-server --config-file restate.toml
```
Let the Restate Server know about the Greeter service by registering it:
```shell theme={null}
restate deployments register localhost:9080
```
Now, we need to make Restate subscribe to the Kafka topics and tell it where it should push the events that arrive on the topic.
Execute the following curl command to create a subscription, and invoke the handler for each event:
```shell theme={null}
curl localhost:9070/subscriptions --json '{
"source": "kafka://my-cluster/greetings",
"sink": "service://Greeter/greet",
"options": {"auto.offset.reset": "earliest"}
}'
```
For Go, you need to capitalize the handler name: `service://Greeter/Greet`.
This curl command calls the Admin API of the Restate Server and tells it to invoke the `greet` handler of the `Greeter` service for each event that arrives on the `greetings` topic in the `my-cluster` Kafka cluster.
Create a Kafka producer and publish an event to the `greetings` topic:
```shell theme={null}
docker exec -it broker kafka-console-producer --bootstrap-server broker:29092 --topic greetings
```
Then type a message and press enter. The greeter takes a String name as an input:
```
{"name":"Sarah"}
```
For some greeter templates, you might need to send just the name as `"Sarah"`.
You now see your handler getting invoked.
The way this worked is that Restate reads the message off the Kafka topic and durably persisted it, similar to what it does for HTTP invocations.
It then **pushed** the message to the handler, as opposed to the pull mechanism that you would have with a Kafka consumer.
If the handler fails, Restate will retry the request until it eventually succeeds.
When invoking Basic Services, Restates ignores the key of the message.
When invoking Virtual Objects, Restate uses the key of the Kafka message as the Virtual Object key.
Whereas a Kafka partition contains multiple keys, Restate effectively keeps track of a queue per key.
Have a look at the [Event Processing use case page](/use-cases/event-processing) to learn about what you can do with Restate and event processing.
You can see the subscriptions that are active via the Admin API:
```shell theme={null}
curl localhost:9070/subscriptions
```
Example output:
```json theme={null}
{
"subscriptions": [
{
"id": "sub_11XHoawrCiWtv8kzhEyGtsR",
"source": "kafka://my-cluster/my-topic",
"sink": "service://Greeter/greet",
"options": {
"auto.offset.reset": "earliest",
"client.id": "restate",
"group.id": "sub_11XHoawrCiWtv8kzhEyGtsR"
}
}
]
}
```
As you can see, subscriptions have an ID that starts with `sub_`.
Now you can use the subscription ID to delete the subscription:
```shell theme={null}
curl -X DELETE localhost:9070/subscriptions/sub_11XHoawrCiWtv8kzhEyGtsR
```
## Related resources
* [Event processing use cases](/use-cases/event-processing): Have a look at examples of how Restate gives you lightweight, transactional event processing.
* Have a look at [the examples](https://github.com/restatedev/examples). There, you can find examples on Durable Execution and event processing.
* [Reference docs for invoking over Kafka](/services/invocation/kafka)
* [Blog post on Kafka integration](https://restate.dev/blog/restate--kafka-event-driven-apps-where-event-driven-is-an-implementation-detail/)
# Scaling to Multi-Node Deployments
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/guides/local-to-replicated
Migrate a single node to a multi-node cluster.
This guide shows how to scale an existing single-node deployment to a multi-node cluster. It assumes you have a running single-node Restate server that is running the replicated loglet and replicated metadata server, which are enabled by default in Restate >= v1.4.0.
Older versions of Restate ({'<='} v1.3.2) use the local loglet and local metadata server by default. The local loglet and local metadata server are suitable for development and single-node deployments. We recommend using the replicated loglet and replicated metadata server to ensure high availability and durability. They are also required for multi-node clusters. Starting with version v1.4.0, existing logs and metadata will be automatically migrated to the replicated equivalents.
Make sure to [upgrade](/server/upgrading) your single-node deployment to the latest Restate version before adding more nodes.
Check that the metadata service is running using the [`restatectl`](/server/clusters#controlling-clusters-with-restatectl) tool.
```shell theme={null}
restatectl metadata-server list-servers
```
You should see a single member node providing metadata service:
```
Metadata service
NODE STATUS VERSION LEADER MEMBERS APPLIED COMMITTED TERM LOG-LENGTH SNAP-INDEX SNAP-SIZE
N1 Member v1 N1 [N1] 2 2 2 1 1 6.7 KiB
```
If you see the node as unreachable with an error reason of "Unimplemented", verify that you are running the latest version. The older local metadata server is no longer supported in Restate v1.4.0 and newer.
The default configuration is cluster-ready. However, if you have explicitly specified server roles in configuration, you should make sure these include the `log-server` role. Similarly, if you have explicitly set the loglet provider to be `local`, you should remove this. While the local loglet is still supported, the default type is `replicated` starting from v1.4.0. If you have a configuration file and would like to make these settings explicit, it should contain the following:
```toml restate.toml theme={null}
roles = [
"worker",
"admin",
"metadata-server",
"log-server", # needed for replicated loglet
"http-ingress",
]
[bifrost]
default-provider = "replicated" # default
```
Confirm that cluster configuration uses the replicated loglet as the default log provider.
```shell theme={null}
restatectl config get
```
In the default configuration you should expect to see:
```
⚙️ Cluster Configuration
├ Number of partitions: 24
├ Partition replication: {node: 1}
└ Logs Provider: replicated
├ Log replication: {node: 1}
└ Nodeset size: 1
```
You can confirm that the type of logs in use by the server using the command:
```shell theme={null}
restatectl logs list
```
If you have enabled the `log-server` role and left the default provider unset (or set it to `replicated`), and still do not see the cluster configuration you can change the cluster log configuration using:
```shell theme={null}
restatectl config set --log-provider replicated --log-replication 1
```
This command sets the default log provider to `replicated` with a default replication of `1`.
As long as you have a single-node deployment, you must set the replication to `1`.
Otherwise, the server will become unavailable because it cannot provision the new log segments.
If you plan to extend your single-node deployment to a multi-node deployment, you also need to [configure the snapshot repository](/server/snapshots#configuring-automatic-snapshotting).
This allows new nodes to join the cluster by restoring the latest snapshot.
```toml restate.toml theme={null}
[worker.snapshots]
destination = "s3://snapshots-bucket/cluster-prefix"
```
For other nodes to join, you need to snapshot every partition because the local loglet is not accessible from other nodes.
Run the following command to create a snapshot for each partition.
```shell theme={null}
restatectl snapshots create --trim-log
```
Note that this also instructs Restate to trim the logs after partition state has been successfully published to the snapshot repository. This ensures that the logs no longer reference historic local loglets that may have existed on the original node.
To add more nodes to your cluster, you need to start new Restate servers with the same `cluster-name` and a metadata client with the address of the existing node.
```toml restate.toml theme={null}
cluster-name = "my-cluster"
[metadata-client]
addresses = ["http://metadata-node.cluster:5122"]
```
Metadata is critical to the operation of your cluster and we recommend that you run the `metadata-server` role on additional nodes. Make the cluster metadata service resilient to node failures by specifying the full list of metadata servers on all cluster nodes.
```toml restate.toml theme={null}
[metadata-client]
addresses = ["http://node1.cluster:5122", "http://node2.cluster:5122", "http://node3.cluster:5122"]
```
If everything is set up correctly, you should see the new nodes in the cluster status.
```shell theme={null}
restatectl status
```
## See also
* Try [growing your cluster](/server/clusters#growing-the-cluster) to tolerate node failures
# Parallelizing Work
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/guides/parallelizing-work
Execute a list of tasks in parallel and then gather their result.
This guide shows how to use the Restate to **execute a list of tasks in parallel and then gather their result**, also known as fan-out, fan-in.
## How does Restate help?
* Restate lets you schedule the tasks asynchronously and guarantees that all tasks will run, with **retries and recovery** on failures.
* Restate **turns Promises/Futures into durable, distributed constructs** that are persisted in Restate and can be recovered and awaited on another process.
* You can deploy the subtask executors on **serverless** infrastructure, like AWS Lambda, to let them scale automatically. The main task, that is idle while waiting on the subtasks, gets suspended until it can make progress.
**Fan out**: You can fan out tasks with Restate by creating a handler that processes a single subtask,
and then scheduling it repeatedly from another handler.
**Fan in**: You can fan in the results of the subtasks by using Restate's Promise Combinators to wait for all promises to resolve.
## Example
The example implements a worker service:
1. It splits a task into subtasks.
2. It schedules all the subtasks. Each subtask results in a promise that gets added to a list.
3. The result is gathered by waiting for all promises to resolve.
You can run this on FaaS infrastructure, like AWS Lambda, and it will scale automatically.
The `run` handler will then suspend while it waits for all subtasks to finish.
Restate will then resume the handler when all subtasks are done.
```ts TypeScript {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/patterns-use-cases/src/parallelizework/fan_out_worker.ts?collapse_prequel"} theme={null}
const fanOutWorker = restate.service({
name: "worker",
handlers: {
run: async (ctx: Context, task: Task): Promise => {
// Split the task in subtasks
const subtasks: SubTask[] = await ctx.run("split task", () => split(task));
// Fan out the subtasks - run them in parallel
const resultPromises = [];
for (const subtask of subtasks) {
const subResultPromise = ctx.serviceClient(fanOutWorker).runSubtask(subtask);
resultPromises.push(subResultPromise);
}
// Fan in - Aggregate the results
const results = await RestatePromise.all(resultPromises);
return aggregate(ctx, results);
},
// Can also run on FaaS
runSubtask: async (ctx: Context, subtask: SubTask): Promise => {
// Processing logic goes here ...
// Can be moved to a separate service to scale independently
return executeSubtask(ctx, subtask);
},
},
});
restate.serve({
services: [fanOutWorker],
port: 9080,
});
```
```java Java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/patterns-use-cases/src/main/java/my/example/parallelizework/FanOutWorker.java?collapse_prequel"} theme={null}
@Service
public class FanOutWorker {
@Handler
public Result run(Context ctx, Task task) {
// Split the task in subtasks
var subTasks = ctx.run(new TypeRef<>() {}, () -> split(task));
// Fan out the subtasks - run them in parallel
List> resultFutures = new ArrayList<>();
for (SubTask subTask : subTasks) {
resultFutures.add(FanOutWorkerClient.fromContext(ctx).runSubtask(subTask));
}
DurableFuture.all(resultFutures).await();
// Fan in - Aggregate the results
var results = resultFutures.stream().map(future -> (SubTaskResult) future.await()).toList();
return aggregate(results);
}
// Can also run on FaaS
@Handler
public SubTaskResult runSubtask(Context ctx, SubTask subTask) {
// Processing logic goes here ...
// Can be moved to a separate service to scale independently
return executeSubtask(ctx, subTask);
}
public static void main(String[] args) {
RestateHttpServer.listen(Endpoint.bind(new FanOutWorker()));
}
}
```
```kotlin Kotlin {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/kotlin/patterns-use-cases/src/main/kotlin/my/example/parallelizework/FanOutWorker.kt?collapse_prequel"} theme={null}
@Service
class FanOutWorker {
@Handler
suspend fun run(ctx: Context, task: Task): TaskResult {
// Split the task in subtasks
val subTasks = ctx.runBlock { task.split() }
// Fan out the subtasks - run them in parallel
// Fan in - Await all results and aggregate
val results = subTasks.map { FanOutWorkerClient.fromContext(ctx).runSubtask(it) }.awaitAll()
return results.aggregate()
}
@Handler
suspend fun runSubtask(ctx: Context, subTask: SubTask): SubTaskResult {
// Processing logic goes here ...
// Can be moved to a separate service to scale independently
return subTask.execute(ctx)
}
}
fun main() {
RestateHttpServer.listen(endpoint { bind(FanOutWorker()) })
}
```
```python Python {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/patterns-use-cases/parallelizework/app.py?collapse_prequel"} theme={null}
fan_out_worker = restate.Service("FanOutWorker")
@fan_out_worker.handler()
async def run(ctx: restate.Context, task: Task) -> Result:
# Split the task in subtasks
subtasks = await ctx.run_typed("split task", split, task=task)
# Fan out the subtasks - run them in parallel
result_promises = [
ctx.run_typed(f"execute {subtask}", execute_subtask, subtask=subtask)
for subtask in subtasks.subtasks
]
# Fan in - Aggregate the results
results_done = await restate.gather(*result_promises)
results = [await result for result in results_done]
return aggregate(results)
app = restate.app([fan_out_worker])
if __name__ == "__main__":
import hypercorn
import asyncio
conf = hypercorn.Config()
conf.bind = ["0.0.0.0:9080"]
asyncio.run(hypercorn.asyncio.serve(app, conf))
```
```go Go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/patterns-use-cases/src/parallelizework/fanoutworker.go?collapse_prequel"} theme={null}
type FanOutWorker struct{}
func (FanOutWorker) Run(ctx restate.Context, task Task) (Result, error) {
// Split the task into subtasks
subtasks, err := split(task)
if err != nil {
return Result{}, err
}
// Fan out the subtasks - run them in parallel
subtaskFutures := make([]restate.Selectable, 0, len(subtasks))
for _, subtask := range subtasks {
subtaskFutures = append(subtaskFutures,
restate.Service[SubTaskResult](ctx, "FanOutWorker", "RunSubtask").RequestFuture(subtask))
}
selector := restate.Select(ctx, subtaskFutures...)
// Fan in - Aggregate the results
subResults := make([]SubTaskResult, 0, len(subtasks))
for selector.Remaining() {
response, err := selector.Select().(restate.ResponseFuture[SubTaskResult]).Response()
if err != nil {
return Result{}, err
}
subResults = append(subResults, response)
}
// Fan in - Aggregate the results
return aggregate(subResults)
}
// RunSubtask can also run on FaaS
func (FanOutWorker) RunSubtask(ctx restate.Context, subtask SubTask) (SubTaskResult, error) {
// Processing logic goes here ...
// Can be moved to a separate service to scale independently
return executeSubtask(ctx, subtask)
}
func main() {
server := server.NewRestate().
Bind(restate.Reflect(FanOutWorker{}))
if err := server.Start(context.Background(), ":9080"); err != nil {
slog.Error("application exited unexpectedly", "err", err.Error())
os.Exit(1)
}
}
```
In this example, we parallelize RPC calls, but this can also be used to parallelize `ctx.run` actions.
This pattern is implementable with any of our SDKs. We are still working on translating all patterns to all SDK languages.
If you need help with a specific language, please reach out to us via [Discord](https://discord.com/invite/skW3AZ6uGd) or [Slack](https://join.slack.com/t/restatecommunity/shared_invite/zt-2v9gl005c-WBpr167o5XJZI1l7HWKImA).
## Running the example
```shell TypeScript theme={null}
restate example typescript-patterns-use-cases && cd typescript-patterns-use-cases
```
```shell Java theme={null}
restate example java-patterns-use-cases && cd java-patterns-use-cases
```
```shell Kotlin theme={null}
restate example kotlin-patterns-use-cases && cd kotlin-patterns-use-cases
```
```shell Python theme={null}
restate example python-patterns-use-cases && cd python-patterns-use-cases
```
```shell Go theme={null}
restate example go-patterns-use-cases && cd go-patterns-use-cases
```
```shell theme={null}
restate-server
```
```shell TypeScript theme={null}
npm install
npx tsx watch ./src/parallelizework/fan_out_worker.ts
```
```shell Java theme={null}
./gradlew -PmainClass=my.example.parallelizework.FanOutWorker run
```
```shell Kotlin theme={null}
./gradlew -PmainClass=my.example.parallelizework.FanOutWorkerKt run
```
```shell Python theme={null}
python parallelizework/app.py
```
```shell Go theme={null}
go run ./src/parallelizework
```
```shell theme={null}
restate deployments register localhost:9080
```
```shell TypeScript theme={null}
curl localhost:8080/worker/run \
--json '{"description": "get out of bed,shower,make coffee,have breakfast"}'
```
```shell Java theme={null}
curl localhost:8080/FanOutWorker/run \
--json '{"description": "get out of bed,shower,make coffee,have breakfast"}'
```
```shell Kotlin theme={null}
curl localhost:8080/FanOutWorker/run \
--json '{"description": "get out of bed,shower,make coffee,have breakfast"}'
```
```shell Python theme={null}
curl localhost:8080/FanOutWorker/run \
--json '{"description": "get out of bed,shower,make coffee,have breakfast"}'
```
```shell Go theme={null}
curl localhost:8080/FanOutWorker/Run \
--json '{"description": "get out of bed,shower,make coffee,have breakfast"}'
```
See how all tasks get spawned in parallel, finish at different times, and then get aggregated.
```shell TypeScript theme={null}
[restate] [worker/runSubtask][inv_17jBqoqRG0TN3msVqHEpZn2aQMOX5kSKrf][2025-01-17T08:51:44.993Z] INFO: Started executing subtask: get out of bed
[restate] [worker/runSubtask][inv_1f8R1NuF0LF27EdQ0R6s7PR8hld245OM8h][2025-01-17T08:51:44.995Z] INFO: Started executing subtask: shower
[restate] [worker/runSubtask][inv_101oPhGwxQqZ0sQebkQnpGyV9Rp3oj9CSJ][2025-01-17T08:51:44.997Z] INFO: Started executing subtask: make coffee
[restate] [worker/runSubtask][inv_1eKDShaxMCEB6DXasrR5OtRXJEvA2je33X][2025-01-17T08:51:44.998Z] INFO: Started executing subtask: have breakfast
[restate] [worker/runSubtask][inv_17jBqoqRG0TN3msVqHEpZn2aQMOX5kSKrf][2025-01-17T08:51:47.003Z] INFO: Execution subtask finished: get out of bed
[restate] [worker/runSubtask][inv_101oPhGwxQqZ0sQebkQnpGyV9Rp3oj9CSJ][2025-01-17T08:51:48.007Z] INFO: Execution subtask finished: make coffee
[restate] [worker/runSubtask][inv_1f8R1NuF0LF27EdQ0R6s7PR8hld245OM8h][2025-01-17T08:51:48.999Z] INFO: Execution subtask finished: shower
[restate] [worker/runSubtask][inv_1eKDShaxMCEB6DXasrR5OtRXJEvA2je33X][2025-01-17T08:51:49.001Z] INFO: Execution subtask finished: have breakfast
[restate] [worker/run][inv_18QHSeAYfvim1oNXRl9I5105veQcTW3BEl][2025-01-17T08:51:49.007Z] INFO: Aggregated result: get out of bed: DONE,shower: DONE,make coffee: DONE,have breakfast: DONE
```
```shell Java theme={null}
2025-01-17 10:00:58 INFO [FanOutWorker/run][inv_1jNoSMJtWluo4Ir43OUyDAxD9weMAQ4OeR] dev.restate.sdk.core.InvocationStateMachine - Start invocation
2025-01-17 10:00:58 INFO [FanOutWorker/runSubtask][inv_1kdpBvVXdqyo3saU6KThul6Jgkfot6LcRP] dev.restate.sdk.core.InvocationStateMachine - Start invocation
2025-01-17 10:00:58 INFO [FanOutWorker/runSubtask][inv_1kdpBvVXdqyo3saU6KThul6Jgkfot6LcRP] my.example.parallelizework.utils.Utils - Started executing subtask: get out of bed
2025-01-17 10:00:58 INFO [FanOutWorker/runSubtask][inv_162MCD5ertQ65pdG0uDIYRMgLBFYZkNPnb] dev.restate.sdk.core.InvocationStateMachine - Start invocation
2025-01-17 10:00:58 INFO [FanOutWorker/runSubtask][inv_162MCD5ertQ65pdG0uDIYRMgLBFYZkNPnb] my.example.parallelizework.utils.Utils - Started executing subtask: shower
2025-01-17 10:00:58 INFO [FanOutWorker/runSubtask][inv_10bPiFTjBUXX35qtzOTPNr0vfgoYYVehpf] dev.restate.sdk.core.InvocationStateMachine - Start invocation
2025-01-17 10:00:58 INFO [FanOutWorker/runSubtask][inv_10bPiFTjBUXX35qtzOTPNr0vfgoYYVehpf] my.example.parallelizework.utils.Utils - Started executing subtask: make coffee
2025-01-17 10:00:58 INFO [FanOutWorker/runSubtask][inv_1115lzidXq7M7CtLZn0aEyUvMC4zkXYLWF] dev.restate.sdk.core.InvocationStateMachine - Start invocation
2025-01-17 10:00:58 INFO [FanOutWorker/runSubtask][inv_1115lzidXq7M7CtLZn0aEyUvMC4zkXYLWF] my.example.parallelizework.utils.Utils - Started executing subtask: have breakfast
2025-01-17 10:00:59 INFO [FanOutWorker/runSubtask][inv_162MCD5ertQ65pdG0uDIYRMgLBFYZkNPnb] my.example.parallelizework.utils.Utils - Execution subtask finished: shower
2025-01-17 10:00:59 INFO [FanOutWorker/runSubtask][inv_162MCD5ertQ65pdG0uDIYRMgLBFYZkNPnb] dev.restate.sdk.core.InvocationStateMachine - End invocation
2025-01-17 10:01:00 INFO [FanOutWorker/runSubtask][inv_1kdpBvVXdqyo3saU6KThul6Jgkfot6LcRP] my.example.parallelizework.utils.Utils - Execution subtask finished: get out of bed
2025-01-17 10:01:00 INFO [FanOutWorker/runSubtask][inv_1kdpBvVXdqyo3saU6KThul6Jgkfot6LcRP] dev.restate.sdk.core.InvocationStateMachine - End invocation
2025-01-17 10:01:04 INFO [FanOutWorker/runSubtask][inv_10bPiFTjBUXX35qtzOTPNr0vfgoYYVehpf] my.example.parallelizework.utils.Utils - Execution subtask finished: make coffee
2025-01-17 10:01:04 INFO [FanOutWorker/runSubtask][inv_10bPiFTjBUXX35qtzOTPNr0vfgoYYVehpf] dev.restate.sdk.core.InvocationStateMachine - End invocation
2025-01-17 10:01:05 INFO [FanOutWorker/runSubtask][inv_1115lzidXq7M7CtLZn0aEyUvMC4zkXYLWF] my.example.parallelizework.utils.Utils - Execution subtask finished: have breakfast
2025-01-17 10:01:05 INFO [FanOutWorker/runSubtask][inv_1115lzidXq7M7CtLZn0aEyUvMC4zkXYLWF] dev.restate.sdk.core.InvocationStateMachine - End invocation
2025-01-17 10:01:05 INFO [FanOutWorker/run][inv_1jNoSMJtWluo4Ir43OUyDAxD9weMAQ4OeR] my.example.parallelizework.utils.Utils - Aggregated result: get out of bed: DONE, shower: DONE, make coffee: DONE, have breakfast: DONE
2025-01-17 10:01:05 INFO [FanOutWorker/run][inv_1jNoSMJtWluo4Ir43OUyDAxD9weMAQ4OeR] dev.restate.sdk.core.InvocationStateMachine - End invocation
```
```shell Kotlin theme={null}
2025-03-06 12:20:18 INFO [FanOutWorker/run][inv_1fGEUyfogPKK5cbSCSWzpCkcDpkQIKSzMB] dev.restate.sdk.core.InvocationStateMachine - Start invocation
2025-03-06 12:20:18 INFO [FanOutWorker/runSubtask][inv_146fBfVLISKb2sCWqesf6uMReXdRKvqmv7] dev.restate.sdk.core.InvocationStateMachine - Start invocation
2025-03-06 12:20:18 INFO [FanOutWorker/runSubtask][inv_146fBfVLISKb2sCWqesf6uMReXdRKvqmv7] FanOutWorker - Started executing subtask: get out of bed
2025-03-06 12:20:18 INFO [FanOutWorker/runSubtask][inv_18T9WW6paOhm6eciCeBt5iqHXRY4h2NRvP] dev.restate.sdk.core.InvocationStateMachine - Start invocation
2025-03-06 12:20:18 INFO [FanOutWorker/runSubtask][inv_18T9WW6paOhm6eciCeBt5iqHXRY4h2NRvP] FanOutWorker - Started executing subtask: shower
2025-03-06 12:20:18 INFO [FanOutWorker/runSubtask][inv_10kE3b5UcL8L64ghFpHQjeeAEowKNis4dH] dev.restate.sdk.core.InvocationStateMachine - Start invocation
2025-03-06 12:20:18 INFO [FanOutWorker/runSubtask][inv_10kE3b5UcL8L64ghFpHQjeeAEowKNis4dH] FanOutWorker - Started executing subtask: make coffee
2025-03-06 12:20:18 INFO [FanOutWorker/runSubtask][inv_1fCFmQ9ulbxL2MBwYCDRgV8Was8PDBedW1] dev.restate.sdk.core.InvocationStateMachine - Start invocation
2025-03-06 12:20:18 INFO [FanOutWorker/runSubtask][inv_1fCFmQ9ulbxL2MBwYCDRgV8Was8PDBedW1] FanOutWorker - Started executing subtask: have breakfast
2025-03-06 12:20:21 INFO [FanOutWorker/runSubtask][inv_10kE3b5UcL8L64ghFpHQjeeAEowKNis4dH] FanOutWorker - Execution subtask finished: make coffee
2025-03-06 12:20:21 INFO [FanOutWorker/runSubtask][inv_10kE3b5UcL8L64ghFpHQjeeAEowKNis4dH] dev.restate.sdk.core.InvocationStateMachine - End invocation
2025-03-06 12:20:24 INFO [FanOutWorker/runSubtask][inv_146fBfVLISKb2sCWqesf6uMReXdRKvqmv7] FanOutWorker - Execution subtask finished: get out of bed
2025-03-06 12:20:24 INFO [FanOutWorker/runSubtask][inv_146fBfVLISKb2sCWqesf6uMReXdRKvqmv7] dev.restate.sdk.core.InvocationStateMachine - End invocation
2025-03-06 12:20:25 INFO [FanOutWorker/runSubtask][inv_1fCFmQ9ulbxL2MBwYCDRgV8Was8PDBedW1] FanOutWorker - Execution subtask finished: have breakfast
2025-03-06 12:20:25 INFO [FanOutWorker/runSubtask][inv_1fCFmQ9ulbxL2MBwYCDRgV8Was8PDBedW1] dev.restate.sdk.core.InvocationStateMachine - End invocation
2025-03-06 12:20:27 INFO [FanOutWorker/runSubtask][inv_18T9WW6paOhm6eciCeBt5iqHXRY4h2NRvP] FanOutWorker - Execution subtask finished: shower
2025-03-06 12:20:27 INFO [FanOutWorker/runSubtask][inv_18T9WW6paOhm6eciCeBt5iqHXRY4h2NRvP] dev.restate.sdk.core.InvocationStateMachine - End invocation
2025-03-06 12:20:27 INFO [FanOutWorker/run][inv_1fGEUyfogPKK5cbSCSWzpCkcDpkQIKSzMB] FanOutWorker - Aggregated result: get out of bed: DONE, shower: DONE, make coffee: DONE, have breakfast: DONE
2025-03-06 12:20:27 INFO [FanOutWorker/run][inv_1fGEUyfogPKK5cbSCSWzpCkcDpkQIKSzMB] dev.restate.sdk.core.InvocationStateMachine - End invocation
```
```shell Python theme={null}
[2025-01-17 12:00:05,183] [12245] [INFO] - Started executing subtask: get out of bed
[2025-01-17 12:00:05,184] [12247] [INFO] - Started executing subtask: shower
[2025-01-17 12:00:05,184] [12245] [INFO] - Started executing subtask: make coffee
[2025-01-17 12:00:05,185] [12245] [INFO] - Started executing subtask: have breakfast
[2025-01-17 12:00:05,188] [12245] [INFO] - Execution subtask finished: make coffee
[2025-01-17 12:00:08,193] [12245] [INFO] - Execution subtask finished: get out of bed
[2025-01-17 12:00:10,194] [12247] [INFO] - Execution subtask finished: shower
[2025-01-17 12:00:15,196] [12245] [INFO] - Execution subtask finished: have breakfast
[2025-01-17 12:00:15,198] [12245] [INFO] - Aggregated result: get out of bed: DONE,shower: DONE,make coffee: DONE,have breakfast: DONE
```
```shell Go theme={null}
2025/01/16 16:41:22 INFO Handling invocation method=FanOutWorker/Run invocationID=inv_1lkcVTBmCorR3fSPhE0pNTiO8XFXoV34C5
2025/01/16 16:41:22 INFO Handling invocation method=FanOutWorker/RunSubtask invocationID=inv_1jpZWOrDK45b2ZwWCapl68GgXzNfoOh0BP
2025/01/16 16:41:22 Started executing subtask: get out of bed
2025/01/16 16:41:22 INFO Handling invocation method=FanOutWorker/RunSubtask invocationID=inv_10eVGnmjP1ET4PgI3z82rvXcVlCnSOep3P
2025/01/16 16:41:22 Started executing subtask: shower
2025/01/16 16:41:22 INFO Handling invocation method=FanOutWorker/RunSubtask invocationID=inv_1i3RduoDMNnb4ideAtWaWCLRDaIO62eghP
2025/01/16 16:41:22 Started executing subtask: make coffee
2025/01/16 16:41:22 INFO Handling invocation method=FanOutWorker/RunSubtask invocationID=inv_142WnXnWDxfy6k4JanZ7DQVqAL6zmktuxP
2025/01/16 16:41:22 Started executing subtask: have breakfast
2025/01/16 16:41:24 Execution subtask finished: get out of bed
2025/01/16 16:41:24 INFO Invocation completed successfully method=FanOutWorker/RunSubtask invocationID=inv_1jpZWOrDK45b2ZwWCapl68GgXzNfoOh0BP
2025/01/16 16:41:25 Execution subtask finished: shower
2025/01/16 16:41:25 INFO Invocation completed successfully method=FanOutWorker/RunSubtask invocationID=inv_10eVGnmjP1ET4PgI3z82rvXcVlCnSOep3P
2025/01/16 16:41:25 Execution subtask finished: have breakfast
2025/01/16 16:41:25 INFO Invocation completed successfully method=FanOutWorker/RunSubtask invocationID=inv_142WnXnWDxfy6k4JanZ7DQVqAL6zmktuxP
2025/01/16 16:41:26 Execution subtask finished: make coffee
2025/01/16 16:41:26 INFO Invocation completed successfully method=FanOutWorker/RunSubtask invocationID=inv_1i3RduoDMNnb4ideAtWaWCLRDaIO62eghP
2025/01/16 16:41:26 Aggregated result: get out of bed: DONE,shower: DONE,have breakfast: DONE,make coffee: DONE
2025/01/16 16:41:26 INFO Invocation completed successfully method=FanOutWorker/Run invocationID=inv_1lkcVTBmCorR3fSPhE0pNTiO8XFXoV34C5
```
## Related resources
* Concurrent tasks docs: [TS](/develop/ts/concurrent-tasks) /
[Java/Kotlin](/develop/java/concurrent-tasks) /
[Python](/develop/python/concurrent-tasks) /
[Go](/develop/go/concurrent-tasks) /
[Rust](https://docs.rs/restate-sdk/latest/restate_sdk/macro.select.html)
# Rate Limiting
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/guides/rate-limiting
Control request rates and prevent service overload with Restate
Rate limiting is a technique used to control the number of requests or operations that a service can handle within a specific time period. It helps prevent system overload and ensures fair resource usage.
## How does Restate help?
Restate provides several features that make it well-suited for implementing rate limiting:
* **Durable state**: Store and manage rate limit counters reliably.
* **Virtual objects**: Isolated rate limiters per key (user, API endpoint, etc.).
* **Durable timers**: Schedule token refills and cleanup operations.
Restate doesn't have built-in rate limiting functionality, but its building blocks make it easy to build this.
## Example
This implementation provides a token bucket rate limiter that can control the rate of operations for any service or resource. You can copy the following files to your project:
The **limiter client interface**, which you can use in your services:
```ts TypeScript expandable {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/patterns-use-cases/src/ratelimit/limiter_client.ts"} theme={null}
import { Context, TerminalError } from "@restatedev/restate-sdk";
import type { Limiter as LimiterObject, Reservation as ReservationResponse } from "./limiter";
export interface Reservation extends ReservationResponse {
// cancel indicates that the reservation holder will not perform the reserved action
// and reverses the effects of this Reservation on the rate limit as much as possible,
// considering that other reservations may have already been made.
cancel(): void;
}
export interface Limiter {
// limit returns the maximum overall event rate.
limit(): Promise;
// burst returns the maximum burst size. Burst is the maximum number of tokens
// that can be consumed in a single call to allow, reserve, or wait, so higher
// Burst values allow more events to happen at once.
// A zero Burst allows no events, unless limit == Inf.
burst(): Promise;
// tokens returns the number of tokens available at time t (defaults to now).
tokens(): Promise;
// allow reports whether n events may happen at time t.
// Use this method if you intend to drop / skip events that exceed the rate limit.
// Otherwise use reserve or wait.
allow(n?: number): Promise;
// reserve returns a Reservation that indicates how long the caller must wait before n events happen.
// The limiter takes this Reservation into account when allowing future events.
// The returned Reservation’s ok parameter is false if n exceeds the limiter's burst size, or provided waitLimitMillis.
// Use this method if you wish to wait and slow down in accordance with the rate limit without dropping events.
// If you need to cancel the delay, use wait instead.
// To drop or skip events exceeding rate limit, use allow instead.
reserve(n?: number, waitLimitMillis?: number): Promise;
// setLimit sets a new limit for the limiter. The new limit, and burst, may be violated
// or underutilized by those which reserved (using reserve or wait) but did not yet act
// before setLimit was called.
setLimit(newLimit: number): Promise;
// setBurst sets a new burst size for the limiter.
setBurst(newBurst: number): Promise;
// setRate sets a new limit and burst size for the limiter.
setRate(newLimit: number, newBurst: number): Promise;
// waitN blocks until the limiter permits n events to happen.
// It returns an error if n exceeds the limiter's burst size, the invocation is canceled,
// or the wait would be longer than the deadline.
// The burst limit is ignored if the rate limit is Inf.
wait(n?: number, waitLimitMillis?: number): Promise;
}
export namespace Limiter {
export function fromContext(ctx: Context, limiterID: string): Limiter {
const client = ctx.objectClient({ name: "limiter" }, limiterID);
return {
async limit() {
return (await client.state()).limit;
},
async burst() {
return (await client.state()).burst;
},
async tokens() {
return client.tokens();
},
async allow(n?: number) {
const r = await client.reserve({
n,
waitLimitMillis: 0,
});
return r.ok;
},
async reserve(n?: number, waitLimitMillis?: number) {
const r = await client.reserve({
n,
waitLimitMillis,
});
return {
cancel() {
ctx
.objectSendClient({ name: "limiter" }, limiterID)
.cancelReservation(r);
},
...r,
};
},
async setLimit(newLimit: number) {
return client.setRate({
newLimit,
});
},
async setBurst(newBurst: number) {
return client.setRate({
newBurst,
});
},
async setRate(newLimit: number, newBurst: number) {
return client.setRate({
newLimit,
newBurst,
});
},
async wait(n: number = 1, waitLimitMillis?: number) {
// Reserve
const r = await this.reserve(n, waitLimitMillis);
if (!r.ok) {
if (waitLimitMillis === undefined) {
throw new TerminalError(`rate: Wait(n=${n}) would exceed the limiters burst`, {
errorCode: 429,
});
} else {
throw new TerminalError(
`rate: Wait(n=${n}) would either exceed the limiters burst or the provided waitLimitMillis`,
{ errorCode: 429 },
);
}
}
// Wait if necessary
const delay = delayFrom(r, r.creationDate);
if (delay == 0) {
return;
}
try {
await ctx.sleep(delay);
} catch (e) {
// this only happens on invocation cancellation - cancel the reservation in the background
r.cancel();
throw e;
}
},
};
}
}
// delayFrom returns the duration in millis for which the reservation holder must wait
// before taking the reserved action. Zero duration means act immediately.
// Infinity means the limiter cannot grant the tokens requested in this
// Reservation within the maximum wait time.
function delayFrom(r: ReservationResponse, date: number): number {
if (!r.ok) {
return Infinity;
}
const delay = r.dateToAct - date;
if (delay < 0) {
return 0;
}
return Math.floor(delay);
}
```
```go Go expandable {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/patterns-use-cases/src/ratelimit/client/limitclient.go"} theme={null}
package client
import (
"time"
"github.com/restatedev/examples/go/patterns-use-cases/src/ratelimit/types"
restate "github.com/restatedev/sdk-go"
)
type Limiter struct {
ctx restate.Context
limiterID string
}
func NewLimiter(ctx restate.Context, limiterID string) *Limiter {
return &Limiter{
ctx: ctx,
limiterID: limiterID,
}
}
func (lim *Limiter) state() (types.LimiterState, error) {
return restate.Object[types.LimiterState](lim.ctx, "Limiter", lim.limiterID, "State").Request(restate.Void{})
}
// Limit returns the maximum overall event rate.
func (lim *Limiter) Limit() (types.Limit, error) {
state, err := lim.state()
if err != nil {
return 0.0, err
}
return state.Limit, nil
}
// Burst returns the maximum burst size. Burst is the maximum number of tokens
// that can be consumed in a single call to Allow, Reserve, or Wait, so higher
// Burst values allow more events to happen at once.
// A zero Burst allows no events, unless limit == Inf.
func (lim *Limiter) Burst() (int, error) {
state, err := lim.state()
if err != nil {
return 0, err
}
return state.Burst, nil
}
// Tokens returns the number of tokens available now.
func (lim *Limiter) Tokens() (float64, error) {
return restate.Object[float64](lim.ctx, "Limiter", lim.limiterID, "Tokens").Request(restate.Void{})
}
// Allow reports whether an event may happen now.
func (lim *Limiter) Allow() (bool, error) {
return lim.AllowN(1)
}
// AllowN reports whether n events may happen now.
// Use this method if you intend to drop / skip events that exceed the rate limit.
// Otherwise use Reserve or Wait.
func (lim *Limiter) AllowN(n int) (bool, error) {
r, err := restate.Object[types.Reservation](lim.ctx, "Limiter", lim.limiterID, "ReserveN").Request(types.ReserveRequest{
N: n,
MaxFutureReserve: 0,
})
if err != nil {
return false, err
}
return r.Ok, nil
}
type Reservation struct {
lim *Limiter
r types.Reservation
}
// Cancel indicates that the reservation holder will not perform the reserved action
// and reverses the effects of this Reservation on the rate limit as much as possible,
// considering that other reservations may have already been made.
func (r *Reservation) Cancel() {
restate.ObjectSend(r.lim.ctx, "Limiter", r.lim.limiterID, "CancelReservation").Send(r.r)
}
// Reserve is shorthand for ReserveN(1).
func (lim *Limiter) Reserve() (*Reservation, error) {
return lim.ReserveN(1)
}
// ReserveN returns a Reservation that indicates how long the caller must wait before n events happen.
// The Limiter takes this Reservation into account when allowing future events.
// The returned Reservation’s OK() method returns false if n exceeds the Limiter's burst size.
// Usage example:
//
// r := lim.ReserveN(1)
// if !r.OK() {
// // Not allowed to act! Did you remember to set lim.burst to be > 0 ?
// return
// }
// restate.Sleep(r.Delay())
// Act()
//
// Use this method if you wish to wait and slow down in accordance with the rate limit without dropping events.
// If you need to respect a deadline or cancel the delay, use Wait instead.
// To drop or skip events exceeding rate limit, use Allow instead.
func (lim *Limiter) ReserveN(n int) (*Reservation, error) {
return lim.reserveN(n, types.InfDuration)
}
func (lim *Limiter) reserveN(n int, maxFutureReserve time.Duration) (*Reservation, error) {
r, err := restate.Object[types.Reservation](lim.ctx, "Limiter", lim.limiterID, "ReserveN").Request(types.ReserveRequest{
N: n,
MaxFutureReserve: maxFutureReserve,
})
if err != nil {
return nil, err
}
return &Reservation{
lim: lim,
r: r,
}, nil
}
// Wait is shorthand for WaitN(1, types.InfDuration).
func (lim *Limiter) Wait() (err error) {
return lim.WaitN(1, types.InfDuration)
}
// SetLimit sets a new Limit for the limiter. The new Limit, and Burst, may be violated
// or underutilized by those which reserved (using Reserve or Wait) but did not yet act
// before SetLimit was called.
func (lim *Limiter) SetLimit(limit types.Limit) error {
_, err := restate.Object[types.Reservation](lim.ctx, "Limiter", lim.limiterID, "SetRate").Request(types.SetRateRequest{
Limit: &limit,
})
return err
}
// SetBurst sets a new burst size for the limiter.
func (lim *Limiter) SetBurst(burst int) error {
_, err := restate.Object[types.Reservation](lim.ctx, "Limiter", lim.limiterID, "SetRate").Request(types.SetRateRequest{
Burst: &burst,
})
return err
}
// SetRate is a convenience method to call both SetLimit and SetBurst atomically.
func (lim *Limiter) SetRate(limit types.Limit, burst int) error {
_, err := restate.Object[types.Reservation](lim.ctx, "Limiter", lim.limiterID, "SetRate").Request(types.SetRateRequest{
Limit: &limit,
Burst: &burst,
})
return err
}
// WaitN blocks until lim permits n events to happen.
// It returns an error if n exceeds the Limiter's burst size, the invocation is
// canceled, or the expected wait time exceeds the maxFutureReserve
// The burst limit is ignored if the rate limit is Inf.
func (lim *Limiter) WaitN(n int, maxFutureReserve time.Duration) (err error) {
r, err := lim.reserveN(n, maxFutureReserve)
if err != nil {
return err
}
if !r.r.Ok {
if maxFutureReserve == types.InfDuration {
return restate.WithErrorCode(restate.TerminalErrorf("rate: Wait(n=%d) would exceed the limiters burst", n), 429)
} else {
return restate.WithErrorCode(restate.TerminalErrorf("rate: Wait(n=%d) would either exceed the limiters burst or the provided maxFutureReserve", n), 429)
}
}
// Wait if necessary
delay := r.DelayFrom(r.r.CreationTime)
if delay == 0 {
return
}
if err := restate.Sleep(lim.ctx, delay); err != nil {
// this only happens on invocation cancellation - cancel the reservation in the background
r.Cancel()
return err
}
return nil
}
// Delay is shorthand for DelayFrom(time.Now()) using a deterministic timestamp.
func (r *Reservation) Delay() time.Duration {
t, _ := restate.Run(r.lim.ctx, func(ctx restate.RunContext) (time.Time, error) {
return time.Now(), nil
})
return r.DelayFrom(t)
}
// DelayFrom returns the duration for which the reservation holder must wait
// before taking the reserved action. Zero duration means act immediately.
// InfDuration means the limiter cannot grant the tokens requested in this
// Reservation within the maximum wait time.
func (r *Reservation) DelayFrom(t time.Time) time.Duration {
if !r.r.Ok {
return types.InfDuration
}
delay := r.r.TimeToAct.Sub(t)
if delay < 0 {
return 0
}
return delay
}
```
The **limiter implementation**, which manages the token bucket state and logic:
```ts TypeScript expandable {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/patterns-use-cases/src/ratelimit/limiter.ts"} theme={null}
// a faithful reimplementation of https://pkg.go.dev/golang.org/x/time/rate#Limiter
// using virtual object state
import { object, ObjectContext } from "@restatedev/restate-sdk";
type LimiterState = {
state: LimiterStateInner;
};
type LimiterStateInner = {
limit: number;
burst: number;
tokens: number;
// last is the last time the limiter's tokens field was updated, in unix millis
last: number;
// lastEvent is the latest time of a rate-limited event (past or future), in unix millis
lastEvent: number;
};
export interface Reservation {
ok: boolean;
tokens: number;
creationDate: number;
dateToAct: number;
// This is the Limit at reservation time, it can change later.
limit: number;
}
export const limiter = object({
name: "limiter",
handlers: {
state: async (ctx: ObjectContext): Promise => {
return getState(ctx);
},
tokens: async (ctx: ObjectContext): Promise => {
// deterministic date not needed, as there is only an output entry
const tokens = advance(await getState(ctx), Date.now());
return tokens;
},
reserve: async (
ctx: ObjectContext,
{ n = 1, waitLimitMillis = Infinity }: { n?: number; waitLimitMillis?: number },
): Promise => {
let lim = await getState(ctx);
if (lim.limit == Infinity) {
// deterministic date is not necessary, as this is part of a response body, which won't be replayed.
const now = Date.now();
return {
ok: true,
tokens: n,
creationDate: now,
dateToAct: now,
limit: 0,
};
}
let r: Reservation;
({ lim, r } = await ctx.run(() => {
const now = Date.now();
let tokens = advance(lim, now);
// Calculate the remaining number of tokens resulting from the request.
tokens -= n;
// Calculate the wait duration
let waitDurationMillis = 0;
if (tokens < 0) {
waitDurationMillis = durationFromTokens(lim.limit, -tokens);
}
// Decide result
const ok = n <= lim.burst && waitDurationMillis <= waitLimitMillis;
// Prepare reservation
const r = {
ok,
tokens: 0,
creationDate: now,
dateToAct: 0,
limit: lim.limit,
} satisfies Reservation;
if (ok) {
r.tokens = n;
r.dateToAct = now + waitDurationMillis;
// Update state
lim.last = now;
lim.tokens = tokens;
lim.lastEvent = r.dateToAct;
}
return { lim, r };
}));
setState(ctx, lim);
return r;
},
setRate: async (
ctx: ObjectContext,
{ newLimit, newBurst }: { newLimit?: number; newBurst?: number },
) => {
if (newLimit === undefined && newBurst === undefined) {
return;
}
let lim = await getState(ctx);
lim = await ctx.run(() => {
const now = Date.now();
const tokens = advance(lim, now);
lim.last = now;
lim.tokens = tokens;
if (newLimit !== undefined) lim.limit = newLimit;
if (newBurst !== undefined) lim.burst = newBurst;
return lim;
});
setState(ctx, lim);
},
cancelReservation: async (ctx: ObjectContext, r: Reservation) => {
let lim = await getState(ctx);
lim = await ctx.run(() => {
const now = Date.now();
if (lim.limit == Infinity || r.tokens == 0 || r.dateToAct < now) {
return lim;
}
// calculate tokens to restore
// The duration between lim.lastEvent and r.timeToAct tells us how many tokens were reserved
// after r was obtained. These tokens should not be restored.
const restoreTokens = r.tokens - tokensFromDuration(r.limit, lim.lastEvent - r.dateToAct);
if (restoreTokens <= 0) {
return lim;
}
// advance time to now
let tokens = advance(lim, now);
// calculate new number of tokens
tokens += restoreTokens;
if (tokens > lim.burst) {
tokens = lim.burst;
}
// update state
lim.last = now;
lim.tokens = tokens;
if (r.dateToAct == lim.lastEvent) {
const prevEvent = r.dateToAct + durationFromTokens(r.limit, -r.tokens);
if (prevEvent >= now) {
lim.lastEvent = prevEvent;
}
}
return lim;
});
setState(ctx, lim);
},
},
});
function advance(lim: LimiterStateInner, date: number): number {
let last = lim.last;
if (date <= last) {
last = date;
}
// Calculate the new number of tokens, due to time that passed.
const elapsedMillis = date - last;
const delta = tokensFromDuration(lim.limit, elapsedMillis);
let tokens = lim.tokens + delta;
if (tokens > lim.burst) {
tokens = lim.burst;
}
return tokens;
}
async function getState(ctx: ObjectContext): Promise {
return (
(await ctx.get("state")) ?? {
limit: 0,
burst: 0,
tokens: 0,
last: 0,
lastEvent: 0,
}
);
}
async function setState(ctx: ObjectContext, lim: LimiterStateInner) {
ctx.set("state", lim);
}
function durationFromTokens(limit: number, tokens: number): number {
if (limit <= 0) {
return Infinity;
}
return (tokens / limit) * 1000;
}
function tokensFromDuration(limit: number, durationMillis: number): number {
if (limit <= 0) {
return 0;
}
return (durationMillis / 1000) * limit;
}
export type Limiter = typeof limiter;
```
```go Go expandable {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/patterns-use-cases/src/ratelimit/service/limiter.go"} theme={null}
package service
import (
"math"
"time"
"github.com/restatedev/examples/go/patterns-use-cases/src/ratelimit/types"
restate "github.com/restatedev/sdk-go"
)
type Limiter struct{}
func (Limiter) State(ctx restate.ObjectContext) (types.LimiterState, error) {
return restate.Get[types.LimiterState](ctx, "state")
}
func (Limiter) Tokens(ctx restate.ObjectContext) (float64, error) {
lim, err := restate.Get[types.LimiterState](ctx, "state")
if err != nil {
return 0.0, err
}
// deterministic date not needed, as there is only an output entry
tokens := advance(&lim, time.Now())
return tokens, nil
}
func (Limiter) ReserveN(ctx restate.ObjectContext, req types.ReserveRequest) (types.Reservation, error) {
lim, err := restate.Get[types.LimiterState](ctx, "state")
if err != nil {
return types.Reservation{}, err
}
if lim.Limit == types.Inf {
// deterministic date is not necessary, as this is part of a response body, which won't be replayed.
t := time.Now()
return types.Reservation{
Ok: true,
CreationTime: t,
Tokens: req.N,
TimeToAct: t,
}, nil
}
type runResult struct {
types.LimiterState `json:"limiterState"`
types.Reservation `json:"reservation"`
}
result, err := restate.Run(ctx, func(ctx restate.RunContext) (runResult, error) {
t := time.Now()
tokens := advance(&lim, t)
// Calculate the remaining number of tokens resulting from the request.
tokens -= float64(req.N)
// Calculate the wait duration
var waitDuration time.Duration
if tokens < 0 {
waitDuration = durationFromTokens(lim.Limit, -tokens)
}
// Decide result
ok := req.N <= lim.Burst && waitDuration <= req.MaxFutureReserve
// Prepare reservation
r := types.Reservation{
Ok: ok,
CreationTime: t,
Limit: lim.Limit,
}
if ok {
r.Tokens = req.N
r.TimeToAct = t.Add(waitDuration)
// Update state
lim.Last = t
lim.Tokens = tokens
lim.LastEvent = r.TimeToAct
}
return runResult{lim, r}, nil
})
if err != nil {
return types.Reservation{}, err
}
restate.Set(ctx, "state", result.LimiterState)
return result.Reservation, nil
}
func (Limiter) SetRate(ctx restate.ObjectContext, req types.SetRateRequest) error {
if req.Limit == nil && req.Burst == nil {
return nil
}
lim, err := restate.Get[types.LimiterState](ctx, "state")
if err != nil {
return err
}
lim, err = restate.Run(ctx, func(ctx restate.RunContext) (types.LimiterState, error) {
t := time.Now()
tokens := advance(&lim, t)
lim.Last = t
lim.Tokens = tokens
if req.Limit != nil {
lim.Limit = *req.Limit
}
if req.Burst != nil {
lim.Burst = *req.Burst
}
return lim, nil
})
if err != nil {
return err
}
restate.Set(ctx, "state", lim)
return nil
}
func (Limiter) CancelReservation(ctx restate.ObjectContext, r types.Reservation) error {
lim, err := restate.Get[types.LimiterState](ctx, "state")
if err != nil {
return err
}
lim, err = restate.Run(ctx, func(ctx restate.RunContext) (types.LimiterState, error) {
t := time.Now()
if r.Limit == types.Inf || r.Tokens == 0 || r.TimeToAct.Before(t) {
return lim, nil
}
// calculate tokens to restore
// The duration between lim.lastEvent and r.timeToAct tells us how many tokens were reserved
// after r was obtained. These tokens should not be restored.
restoreTokens := float64(r.Tokens) - tokensFromDuration(r.Limit, lim.LastEvent.Sub(r.TimeToAct))
if restoreTokens <= 0 {
return lim, nil
}
// advance time to now
tokens := advance(&lim, t)
// calculate new number of tokens
tokens += restoreTokens
if burst := float64(lim.Burst); tokens > burst {
tokens = burst
}
// update state
lim.Last = t
lim.Tokens = tokens
if r.TimeToAct == lim.LastEvent {
prevEvent := r.TimeToAct.Add(durationFromTokens(r.Limit, float64(-r.Tokens)))
if !prevEvent.Before(t) {
lim.LastEvent = prevEvent
}
}
return lim, nil
})
restate.Set(ctx, "state", lim)
return nil
}
func advance(lim *types.LimiterState, t time.Time) float64 {
last := lim.Last
if t.Before(last) {
last = t
}
// Calculate the new number of tokens, due to time that passed.
elapsed := t.Sub(last)
delta := tokensFromDuration(lim.Limit, elapsed)
tokens := lim.Tokens + delta
if burst := float64(lim.Burst); tokens > burst {
tokens = burst
}
return tokens
}
// durationFromTokens is a unit conversion function from the number of tokens to the duration
// of time it takes to accumulate them at a rate of limit tokens per second.
func durationFromTokens(limit types.Limit, tokens float64) time.Duration {
if limit <= 0 {
return types.InfDuration
}
duration := (tokens / float64(limit)) * float64(time.Second)
// Cap the duration to the maximum representable int64 value, to avoid overflow.
if duration > float64(math.MaxInt64) {
return types.InfDuration
}
return time.Duration(duration)
}
// tokensFromDuration is a unit conversion function from a time duration to the number of tokens
// which could be accumulated during that duration at a rate of limit tokens per second.
func tokensFromDuration(limit types.Limit, d time.Duration) float64 {
if limit <= 0 {
return 0
}
return d.Seconds() * float64(limit)
}
```
This implementation provides a `RateLimiter` Virtual Object which implements the **Token Bucket Algorithm**:
* Tokens are added at a specified rate (limit)
* Tokens are consumed when operations are performed
* A burst capacity allows for short bursts of activity
**Key Methods** (via the client interface):
* `limit()` / `burst()`: Get maximum event rate and burst size
* `tokens()`: Get number of available tokens
* `wait()`: Block until events are permitted to happen
* `setRate()` / `setLimit()` / `setBurst()`: Configure rate limiting parameters
## Usage Example
Here's how to use the rate limiter in your services:
```ts TypeScript {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/patterns-use-cases/src/ratelimit/service.ts"} theme={null}
import { Context, service } from "@restatedev/restate-sdk";
import { Limiter } from "./limiter_client";
const LIMITER_NAME = "myService-expensiveMethod";
export const myService = service({
name: "myService",
handlers: {
expensiveMethod: async (ctx: Context) => {
const limiter = Limiter.fromContext(ctx, LIMITER_NAME);
await limiter.wait();
console.log("expensive!");
},
expensiveMethodBatch: async (ctx: Context, batchSize: number = 20) => {
const limiter = Limiter.fromContext(ctx, LIMITER_NAME);
await limiter.wait(batchSize);
for (let i = 0; i < batchSize; i++) {
console.log("expensive!");
}
},
},
});
export type MyService = typeof myService;
```
```go Go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/patterns-use-cases/src/ratelimit/example/main.go?collapse_prequel"} theme={null}
type LimitedTask struct{}
func (LimitedTask) RunTask(ctx restate.Context) error {
limiter := client.NewLimiter(ctx, "LimitedTask-RunTask")
if err := limiter.Wait(); err != nil {
return err
}
// Implement doing the work...
return nil
}
func main() {
server := server.NewRestate().
Bind(restate.Reflect(service.Limiter{})).
Bind(restate.Reflect(LimitedTask{}))
if err := server.Start(context.Background(), ":9080"); err != nil {
slog.Error("application exited unexpectedly", "err", err.Error())
os.Exit(1)
}
}
```
## Running the example
```bash TypeScript theme={null}
restate example typescript-patterns-use-cases && cd typescript-patterns-use-cases
```
```bash Go theme={null}
restate example go-patterns-use-cases && cd go-patterns-use-cases
```
```bash theme={null}
restate-server
```
```bash TypeScript theme={null}
npm install
npx tsx watch ./src/ratelimit/app.ts
```
```bash Go theme={null}
go run ./src/ratelimit/example/main.go
```
```bash theme={null}
restate deployments register localhost:9080
```
Set up the limiter named `myService-expensiveMethod` with a rate limit of 1 per second:
```bash TypeScript theme={null}
curl localhost:8080/limiter/myService-expensiveMethod/setRate \
--json '{"newLimit": 1, "newBurst": 1}'
```
```bash Go theme={null}
curl localhost:8080/Limiter/LimitedTask-RunTask/SetRate \
--json '{"limit": 1, "burst": 1}'
```
Try sending multiple requests quickly to see the rate limiting in action.
You can send requests that are subject to the limiter like this:
```bash TypeScript theme={null}
# send one request
curl localhost:8080/myService/expensiveMethod
# send lots
for i in $(seq 1 30); do curl localhost:8080/myService/expensiveMethod && echo "request completed"; done
```
```bash Go theme={null}
# send one request
curl localhost:8080/LimitedTask/RunTask
# send lots
for i in $(seq 1 30); do curl localhost:8080/LimitedTask/RunTask && echo "request completed"; done
```
You should observe that only one request is processed per second.
You can then try changing the limit or the burst and sending more requests.
In the Restate UI, you can observe:
* Invocations getting scheduled one per second in the Invocations tab
* Rate limiter state and token counts in the State tab
# Request Lifecycle
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/guides/request-lifecycle
Deep dive into the lifecycle of a request in Restate
This guide provides a detailed technical walkthrough of how Restate processes requests from start to finish, including the mechanisms that enable durable execution and failure recovery.
## Request Processing Lifecycle
When Restate receives a request to invoke a handler, it follows this detailed process:
### 1. Persist Request
Restate persists the request and creates a new execution journal for this invocation. From this point, Restate guarantees that it will process the request to completion, even in the face of failures.
### 2. Forward to Service
Restate opens a bidirectional streaming connection (HTTP/2) to the service for which the request is meant. This connection will be used to stream messages back and forth between the Server and the SDK. It then forwards the request to the service.
### 3. Invoke Handler
The Restate SDK, embedded in the service, calls the handler with the request data and a context object that provides access to Restate features like state management, service calls, and timers.
### 4. Track Execution
As the handler runs, it uses the Restate Context to call other services, update state, or perform side effects. The Restate SDK under-the-hood sends a message to the Restate server for each Context operation. The Restate Server then records it in the execution journal.
### 5. Handle Failures
If the handler crashes or fails, Restate either loses the connection or receives an error message. In this case, the Restate Server will send a retry request to the service, including the latest journal.
Failure detection mechanisms:
* Connection between Server and service dropped
* Explicit error messages (not terminal errors)
* Inactivity timeouts reached
### 6. Replay Journal
The service receives the retry request and restarts the execution of the handler. Whenever the handler performs an action on the Restate context (like calling another service, updating state, or setting a timer), the SDK checks the journal for the last recorded result. If it finds a previous result, it skips executing that action and uses the recorded result instead. This way, the handler resumes from exactly where it left off.
### 7. Complete Execution
Once the handler completes successfully, the Restate SDK proxies the result back to the client via the Restate Server. The server then marks the invocation as complete in the journal.
## Cross-service interactions
Note that inter-handler communication between two durable handlers goes through Restate (not direct service-to-service). The caller appends an invocation event to its journal, which Restate Server turns into an invocation for the target service, providing end-to-end idempotency.
## Request-response Deployment Targets
Some deployment targets don't support bidirectional streaming connections (HTTP/2), such as AWS Lambda. In these cases, the Restate SDK uses a request-response pattern (HTTP/1.1) instead.
In this case, the SDK will try to make as much progress as possible.
When it hits a point where it needs to wait for a response (like a service call, sleep, or run action), it will send the new journal entries to the Restate Server and suspend the execution.
The server will send a retry request once the response is available.
Execution then resumes by replaying the journal up to the suspension point.
# Sagas
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/guides/sagas
Implementing undo operations in case of failures, to keep your system consistent
A **Saga** is a design pattern for handling transactions that span multiple services. It breaks the process into a sequence of local operations, each with a corresponding **compensating action**. If a failure occurs partway through, these compensations are triggered to **undo** completed steps, ensuring your system stays consistent even when things go wrong.
## How does Restate help?
Restate makes implementing resilient sagas simple:
* **Durable Execution**: Restate guarantees completion and automatically retries from failure points. No manual state tracking or retry logic needed
* **Code-first approach**: Define sagas using regular code, no DSLs required
## Example
A travel booking workflow: book a flight, rent a car, then book a hotel. If any step fails (e.g. hotel full), we roll back previous steps to maintain consistency.
**Implementation:**
* Wrap business logic in a try-block, throw terminal errors for compensation cases
* Add compensations to a list for each step
* In catch block, run compensations in reverse order and rethrow
Note: Golang uses `defer` for compensations.
```ts TypeScript {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/patterns-use-cases/src/sagas/booking_workflow.ts?collapse_prequel?remove_comments"} theme={null}
const bookingWorkflow = restate.service({
name: "BookingWorkflow",
handlers: {
run: async (ctx: restate.Context, req: BookingRequest) => {
const { customerId, flight, car, hotel } = req;
const compensations = [];
try {
compensations.push(() => ctx.run("Cancel flight", () => flightClient.cancel(customerId)));
await ctx.run("Book flight", () => flightClient.book(customerId, flight));
compensations.push(() => ctx.run("Cancel car", () => carRentalClient.cancel(customerId)));
await ctx.run("Book car", () => carRentalClient.book(customerId, car));
compensations.push(() => ctx.run("Cancel hotel", () => hotelClient.cancel(customerId)));
await ctx.run("Book hotel", () => hotelClient.book(customerId, hotel));
} catch (e) {
if (e instanceof restate.TerminalError) {
for (const compensation of compensations.reverse()) {
await compensation();
}
}
throw e;
}
},
},
});
restate.serve({
services: [bookingWorkflow],
port: 9080,
});
```
```java Java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/patterns-use-cases/src/main/java/my/example/sagas/BookingWorkflow.java?collapse_prequel?remove_comments"} theme={null}
@Service
public class BookingWorkflow {
public record BookingRequest(
String customerId, FlightRequest flight, CarRequest car, HotelRequest hotel) {}
@Handler
public void run(Context ctx, BookingRequest req) throws TerminalException {
List compensations = new ArrayList<>();
try {
compensations.add(() -> ctx.run("Cancel flight", () -> FlightClient.cancel(req.customerId)));
ctx.run("Book flight", () -> FlightClient.book(req.customerId, req.flight()));
compensations.add(() -> ctx.run("Cancel car", () -> CarRentalClient.cancel(req.customerId)));
ctx.run("Book car", () -> CarRentalClient.book(req.customerId, req.car()));
compensations.add(() -> ctx.run("Cancel hotel", () -> HotelClient.cancel(req.customerId)));
ctx.run("Book hotel", () -> HotelClient.book(req.customerId, req.hotel()));
}
catch (TerminalException e) {
for (Runnable compensation : compensations) {
compensation.run();
}
throw e;
}
}
public static void main(String[] args) {
RestateHttpServer.listen(Endpoint.bind(new BookingWorkflow()));
}
}
```
```kotlin Kotlin {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/kotlin/patterns-use-cases/src/main/kotlin/my/example/sagas/BookingWorkflow.kt?collapse_prequel?remove_comments"} theme={null}
@Service
class BookingWorkflow {
@Handler
suspend fun run(ctx: Context, req: BookingRequest) {
val compensations = mutableListOf Unit>()
try {
compensations.add { ctx.runBlock("Cancel flight") { cancelFlight(req.customerId) } }
ctx.runBlock("Book flight") { bookFlight(req.customerId, req.flight) }
compensations.add { ctx.runBlock("Cancel car") { cancelCar(req.customerId) } }
ctx.runBlock("Book car") { bookCar(req.customerId, req.car) }
compensations.add { ctx.runBlock("Cancel hotel") { cancelHotel(req.customerId) } }
ctx.runBlock("Book hotel") { bookHotel(req.customerId, req.hotel) }
}
catch (e: TerminalException) {
compensations.reversed().forEach { it() }
throw e
}
}
}
fun main() {
RestateHttpServer.listen(endpoint { bind(BookingWorkflow()) })
}
```
```python Python {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/patterns-use-cases/sagas/app.py?collapse_prequel?remove_comments"} theme={null}
booking_workflow = restate.Service("BookingWorkflow")
@booking_workflow.handler()
async def run(ctx: restate.Context, req: BookingRequest):
compensations = []
try:
compensations.append(
lambda: ctx.run_typed("Cancel flight", flight_client.cancel, customer_id=req.customer_id)
)
await ctx.run_typed("Book flight", flight_client.book, customer_id=req.customer_id, flight=req.flight)
compensations.append(
lambda: ctx.run_typed("Cancel car", car_rental_client.cancel, customer_id=req.customer_id)
)
await ctx.run_typed("Book car", car_rental_client.book, customer_id=req.customer_id, car=req.car)
compensations.append(
lambda: ctx.run_typed("Cancel hotel", hotel_client.cancel, customer_id=req.customer_id)
)
await ctx.run_typed("Book hotel", hotel_client.book, customer_id=req.customer_id, hotel=req.hotel)
except TerminalError as e:
for compensation in reversed(compensations):
await compensation()
raise e
app = restate.app([booking_workflow])
if __name__ == "__main__":
import hypercorn
import asyncio
conf = hypercorn.Config()
conf.bind = ["0.0.0.0:9080"]
asyncio.run(hypercorn.asyncio.serve(app, conf))
```
```go Go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/patterns-use-cases/src/sagas/bookingworkflow.go?collapse_prequel?remove_comments"} theme={null}
type BookingWorkflow struct{}
func (BookingWorkflow) Run(ctx restate.Context, req BookingRequest) (err error) {
var compensations []func() (restate.Void, error)
defer func() {
if err != nil {
for _, compensation := range slices.Backward(compensations) {
if _, compErr := compensation(); compErr != nil {
err = compErr
}
}
}
}()
compensations = append(compensations, func() (restate.Void, error) {
return restate.Run(ctx,
func(ctx restate.RunContext) (restate.Void, error) {
return CancelFlight(req.CustomerId)
},
restate.WithName("Cancel flight"),
)
})
if _, err = restate.Run(ctx,
func(ctx restate.RunContext) (restate.Void, error) {
return BookFlight(req.CustomerId, req.Flight)
},
restate.WithName("Book flight"),
); err != nil {
return err
}
compensations = append(compensations, func() (restate.Void, error) {
return restate.Run(ctx,
func(ctx restate.RunContext) (restate.Void, error) {
return CancelCar(req.CustomerId)
},
restate.WithName("Cancel car"),
)
})
if _, err = restate.Run(ctx,
func(ctx restate.RunContext) (restate.Void, error) {
return BookCar(req.CustomerId, req.Car)
},
restate.WithName("Book car"),
); err != nil {
return err
}
compensations = append(compensations, func() (restate.Void, error) {
return restate.Run(ctx,
func(ctx restate.RunContext) (restate.Void, error) {
return CancelHotel(req.CustomerId)
},
restate.WithName("Cancel hotel"),
)
})
if _, err = restate.Run(ctx,
func(ctx restate.RunContext) (restate.Void, error) {
return BookHotel(req.CustomerId, req.Hotel)
},
restate.WithName("Book hotel"),
); err != nil {
return err
}
return nil
}
func main() {
if err := server.NewRestate().
Bind(restate.Reflect(BookingWorkflow{})).
Start(context.Background(), ":9080"); err != nil {
log.Fatal(err)
}
}
```
View on GitHub:
[TS](https://github.com/restatedev/examples/blob/main/typescript/patterns-use-cases/src/sagas/booking_workflow.ts) /
[Java](https://github.com/restatedev/examples/blob/main/java/patterns-use-cases/src/main/java/my/example/sagas/BookingWorkflow.java) /
[Kotlin](https://github.com/restatedev/examples/blob/main/kotlin/patterns-use-cases/src/main/kotlin/my/example/sagas/BookingWorkflow.kt) /
[Python](https://github.com/restatedev/examples/blob/main/python/patterns-use-cases/sagas/app.py) /
[Go](https://github.com/restatedev/examples/blob/main/go/patterns-use-cases/src/sagas/bookingworkflow.go)
This pattern is implementable with any of our SDKs. We are still working on translating all patterns to all SDK languages.
If you need help with a specific language, please reach out to us via [Discord](https://discord.com/invite/skW3AZ6uGd) or [Slack](https://join.slack.com/t/restatecommunity/shared_invite/zt-2v9gl005c-WBpr167o5XJZI1l7HWKImA).
## When to use Sagas
Restate automatically retries transient failures (network hiccups, temporary outages). For non-transient failures, sagas are essential:
1. **Business logic failures**: When failures are business decisions (e.g. "Hotel is full"), retrying won't help. Throw a terminal error to trigger compensations.
2. **User/system cancellations**: When you [cancel](/services/invocation/managing-invocations#cancel) long-running invocations, sagas undo previous operations to maintain consistency.
## Running the example
```bash TypeScript theme={null}
restate example typescript-patterns-use-cases && cd typescript-patterns-use-cases
```
```bash Java theme={null}
restate example java-patterns-use-cases && cd java-patterns-use-cases
```
```bash Kotlin theme={null}
restate example kotlin-patterns-use-cases && cd kotlin-patterns-use-cases
```
```bash Python theme={null}
restate example python-patterns-use-cases && cd python-patterns-use-cases
```
```bash Go theme={null}
restate example go-patterns-use-cases && cd go-patterns-use-cases
```
```bash theme={null}
restate-server
```
```bash TypeScript theme={null}
npm install
npx tsx watch ./src/sagas/booking_workflow.ts
```
```bash Java theme={null}
./gradlew -PmainClass=my.example.sagas.BookingWorkflow run
```
```bash Kotlin theme={null}
./gradlew -PmainClass=my.example.sagas.BookingWorkflowKt run
```
```bash Python theme={null}
python sagas/app.py
```
```bash Go theme={null}
go run ./src/sagas
```
```bash theme={null}
restate deployments register localhost:9080
```
```bash TypeScript theme={null}
curl localhost:8080/BookingWorkflow/run --json '{
"flight": {
"flightId": "12345",
"passengerName": "John Doe"
},
"car": {
"pickupLocation": "Airport",
"rentalDate": "2024-12-16"
},
"hotel": {
"arrivalDate": "2024-12-16",
"departureDate": "2024-12-20"
}
}'
```
```bash Java theme={null}
curl localhost:8080/BookingWorkflow/run --json '{
"flight": {
"flightId": "12345",
"passengerName": "John Doe"
},
"car": {
"pickupLocation": "Airport",
"rentalDate": "2024-12-16"
},
"hotel": {
"arrivalDate": "2024-12-16",
"departureDate": "2024-12-20"
}
}'
```
```bash Kotlin theme={null}
curl localhost:8080/BookingWorkflow/run --json '{
"flight": {
"flightId": "12345",
"passengerName": "John Doe"
},
"car": {
"pickupLocation": "Airport",
"rentalDate": "2024-12-16"
},
"hotel": {
"arrivalDate": "2024-12-16",
"departureDate": "2024-12-20"
}
}'
```
```bash Python theme={null}
curl localhost:8080/BookingWorkflow/run --json '{
"flight": {
"flightId": "12345",
"passengerName": "John Doe"
},
"car": {
"pickupLocation": "Airport",
"rentalDate": "2024-12-16"
},
"hotel": {
"arrivalDate": "2024-12-16",
"departureDate": "2024-12-20"
}
}'
```
```bash Go theme={null}
curl localhost:8080/BookingWorkflow/Run --json '{
"flight": {
"flightId": "12345",
"passengerName": "John Doe"
},
"car": {
"pickupLocation": "Airport",
"rentalDate": "2024-12-16"
},
"hotel": {
"arrivalDate": "2024-12-16",
"departureDate": "2024-12-20"
}
}'
```
See in the Restate UI (`localhost:9070`) how all steps were executed, and how the compensations were triggered because the hotel was full.
## Advanced: Idempotency and compensations
Sagas in Restate are flexible and powerful since they're implemented in user code. However, you need to make sure compensations are idempotent.
The example uses customer ID for idempotency, preventing duplicate bookings on retries. The API provider deduplicates requests based on this ID.
Different APIs require different approaches:
1. **Two-phase APIs**: First *reserve*, then *confirm* or *cancel*. Register the compensation after reservation, when you have the resource ID.
This type of API usually auto-cancels reservations after a timeout.
```ts TypeScript {"CODE_LOAD::ts/src/guides/sagas/booking_workflow.ts#twostep"} theme={null}
const bookingId = await ctx.run(() =>
flightClient.reserve(customerId, flight)
);
compensations.push(() => ctx.run(() => flightClient.cancel(bookingId)));
// ... do other work, like reserving a car, etc. ...
await ctx.run(() => flightClient.confirm(bookingId));
```
```python Python {"CODE_LOAD::python/src/guides/sagas/app.py#twostep"} theme={null}
booking_id = await ctx.run_typed(
"reserve", flight_client.reserve, customer_id=customer_id, flight=flight
)
compensations.append(
lambda: ctx.run_typed("cancel", flight_client.cancel, booking_id=booking_id)
)
# ... do other work, like reserving a car, etc. ...
await ctx.run_typed("confirm", flight_client.confirm, booking_id=booking_id)
```
```java Java {"CODE_LOAD::java/src/main/java/guides/sagas/BookingWorkflow.java#twostep"} theme={null}
String bookingId = ctx.run(String.class, () -> FlightClient.reserve(flight));
compensations.add(() -> ctx.run(() -> FlightClient.cancel(bookingId)));
// ... do other work, like reserving a car, etc. ...
ctx.run(() -> FlightClient.confirm(bookingId));
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/guides/sagas/BookingWorkflow.kt#twostep"} theme={null}
// For each action, we register a compensation that will be executed on failures
val bookingId = ctx.runBlock { reserveFlight(customerId, flight) }
compensations.add { ctx.runBlock { cancelFlight(bookingId) } }
// ... do other work, like reserving a car, etc. ...
compensations.add { ctx.runBlock { confirmFlight(bookingId) } }
```
```go Go {"CODE_LOAD::go/guides/sagas/bookingworkflow.go#twostep"} theme={null}
bookingId, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
return ReserveFlight(req.CustomerId, req.Flight)
})
if err != nil {
return err
}
compensations = append(compensations, func() (restate.Void, error) {
return restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return CancelFlight(bookingId)
})
})
// ... do other work, like reserving a car, etc. ...
if _, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return ConfirmFlight(bookingId)
}); err != nil {
return err
}
```
2. **One-shot APIs with idempotency key**: Generate idempotency key, persist in Restate, register compensation (e.g. `refund`), then do action (e.g. `charge`). Register compensation first in case action succeeded but confirmation was lost.
```ts TypeScript {"CODE_LOAD::ts/src/guides/sagas/booking_workflow.ts#idempotency"} theme={null}
const paymentId = ctx.rand.uuidv4();
compensations.push(() => ctx.run(() => paymentClient.refund(paymentId)));
await ctx.run(() => paymentClient.charge(paymentInfo, paymentId));
```
```python Python {"CODE_LOAD::python/src/guides/sagas/app.py#idempotency"} theme={null}
payment_id = ctx.uuid()
compensations.append(lambda: ctx.run_typed("refund", payment.refund, payment_id=payment_id))
await ctx.run_typed("charge", payment.charge, payment_info=payment_info, payment_id=payment_id)
```
```java Java {"CODE_LOAD::java/src/main/java/guides/sagas/BookingWorkflow.java#idempotency"} theme={null}
String paymentId = ctx.random().nextUUID().toString();
compensations.add(() -> ctx.run(() -> PaymentClient.refund(paymentId)));
ctx.run(() -> PaymentClient.charge(paymentInfo, paymentId));
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/guides/sagas/BookingWorkflow.kt#idempotency"} theme={null}
val paymentId = ctx.random().nextUUID().toString()
compensations.add { ctx.runBlock { refund(paymentId) } }
ctx.runBlock { charge(paymentInfo, paymentId) }
```
```go Go {"CODE_LOAD::go/guides/sagas/bookingworkflow.go#idempotency"} theme={null}
paymentID := restate.Rand(ctx).UUID().String()
// Register the refund as a compensation, using the idempotency key
compensations = append(compensations, func() (restate.Void, error) {
return restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return Refund(paymentID)
})
})
// Do the payment using the idempotency key
if _, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return Charge(paymentID, req.PaymentInfo)
}); err != nil {
return err
}
```
## Related resources
* [Error Handling guide](/guides/error-handling)
* [Cancellation of invocations](/services/invocation/managing-invocations#cancel)
* [Blog post: Graceful cancellations: How to keep your application and workflow state consistent 💪](https://restate.dev/blog/graceful-cancellations-how-to-keep-your-application-and-workflow-state-consistent/)
# XState
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/guides/xstate
Integrate Restate with XState to implement durable state machines.
Restate integrates with [XState](https://xstate.js.org/). Have a look at the [repo](https://github.com/restatedev/xstate) to learn more.
# Welcome to Restate!
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/index
Build innately resilient backends and AI agents
Restate is a lightweight runtime to turn AI agents, workflows, and backend services into durable processes. Focus on your logic, not failure mechanics.
Write normal code and let Restate handles resilience and consistency automatically.
## Key capabilities
**Durable execution**: Code automatically stores completed steps and resumes from where it left off when recovering from failures.
**Built-in state**: Maintain state beyond workflow executions and share it between functions with strong consistency guarantees.
**Reliable communication**: Call services sync or async with guaranteed execution and exactly-once semantics.
**Time-based coordination**: Sleep, schedule, and wait for external events with durable timers.
**Workflows**: Coordinate long-running processes, human approvals, listen to webhooks and other signals.
## Common use cases
Manage stateful AI agents with reliable tool usage and long-running conversations.
Build approval processes, multi-step operations, and business workflows that survive failures.
Coordinate calls across multiple services with automatic retries and failure handling.
Process events with exactly-once guarantees and automatic retry handling.
## First time here?
Build your first Restate service in minutes.
Understand the core building blocks.
Learn how to build common applications with Restate:
[AI agents](/tour/vercel-ai-agents) •
[Workflows](/tour/workflows) •
[Microservice orchestration](/tour/microservice-orchestration)
Chat with our AI assistant to get answers to your questions about Restate.
## Learning resources
A collection of examples that illustrate how to use Restate to solve common application challenges.
Learn how to do common tasks with Restate: patterns, integrations, deployment tutorials, ...
## Reference
Implement Restate applications in one of the available SDKs.
Deploy and operate services on your preferred platform.
[Deploy](/deploy/services/kubernetes) •
[Invoke](/services/invocation/http) •
[Versioning](/services/versioning) •
[Monitor & Inspect](/services/introspection)
Get started immediately with Restate Cloud, or host your own Restate server.
## Community
Join the Restate Discord or Slack communities.
Watch community meetings and talks about Restate.
Subscribe and attend our events.
# Installation
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/installation
Learn how to set up your local Restate development environment.
Set up Restate locally in minutes.
## Install Restate Server & CLI
Restate is a single self-contained binary. No external dependencies needed.
```shell theme={null}
brew install restatedev/tap/restate-server restatedev/tap/restate
```
Start the server:
```shell theme={null}
restate-server
```
Find the CLI at:
```shell theme={null}
restate --help
```
Download prebuilt binaries from the [releases page](https://github.com/restatedev/restate/releases/latest):
```shell MacOS-x64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=x86_64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell MacOS-arm64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=aarch64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell Linux-x64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=x86_64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
```shell Linux-arm64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=aarch64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
Start the server:
```shell theme={null}
restate-server
```
Find the CLI at:
```shell theme={null}
restate --help
```
```shell theme={null}
npm install --global @restatedev/restate-server@latest @restatedev/restate@latest
```
Start the server:
```shell theme={null}
restate-server
```
Find the CLI at:
```shell theme={null}
restate --help
```
Run the Restate Server:
```shell theme={null}
docker run --name restate_dev --rm \
-p 8080:8080 -p 9070:9070 -p 9071:9071 \
--add-host=host.docker.internal:host-gateway \
docker.restate.dev/restatedev/restate:latest
```
Run CLI commands:
```shell theme={null}
docker run -it --network=host \
docker.restate.dev/restatedev/restate-cli:latest \
invocations ls
```
Replace `invocations ls` with any CLI subcommand.
You can find the Restate UI running on port 9070 (`http://localhost:9070`) after starting the Restate Server.
Check out the [CLI](/references/cli-config) or [Server configuration options](/server/configuration).
Restate Server collects anonymized telemetry about versions and uptime via [Scarf](https://about.scarf.sh).\
We don't have access to your IP or any information about your cluster.
To disable, set `DO_NOT_TRACK=1`.
## Restate UI
The UI is bundled with the Restate Server and available at [http://localhost:9070](http://localhost:9070) when running locally. Use the UI to manage, debug, and configure your applications.
## Useful CLI Commands
With the CLI installed, try these commands:
```shell theme={null}
restate whoami
```
(If using Docker, use `http://host.docker.internal:9080`)
```shell theme={null}
restate deployments register localhost:9080
```
```shell theme={null}
restate invocation list
```
```shell theme={null}
# a single invocation
restate invocation cancel my_invocation_id
# cancel all invocations of a service, object, or handler
restate invocation cancel MyService
restate invocation cancel MyService/myHandler
restate invocation cancel MyObject/myObjectKey
restate invocation cancel MyObject/myObjectKey/myHandler
```
Use `restate invocation kill` to force kill.
```shell theme={null}
restate kv clear MyObject
restate kv clear MyObject/myObjectKey
```
Remove the `restate-data` directory to wipe all invocations, state, registered services, etc.
```shell theme={null}
rm -rf restate-data
```
See [SQL introspection docs](/services/introspection#inspecting-invocations) for examples.
Use `--json` for JSON output.
```shell theme={null}
restate sql "query"
```
See also the [introspection page](/services/introspection) for more CLI debugging commands.
## Advanced: Installing `restatectl`
`restatectl` is a command-line tool for managing Restate clusters. It provides commands for cluster management, introspection, and debugging.
This tool is specifically designed for system operators to manage Restate servers and is particularly useful in a cluster environment.
Install with:
```shell theme={null}
brew install restatedev/tap/restatectl
```
Then run:
```shell theme={null}
restatectl --help
```
Install restatectl by downloading the binary with `curl` from the [releases page](https://github.com/restatedev/restate/releases/latest), and make them executable:
```shell Linux-x64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=x86_64-unknown-linux-musl && \
curl -LO https://restate.gateway.scarf.sh/latest/restatectl-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restatectl-$RESTATE_PLATFORM.tar.xz --strip-components=1 restatectl-$RESTATE_PLATFORM/restatectl && \
chmod +x restatectl && \
# Move the binary to a directory in your PATH, for example ~/.local/bin:
mv restatectl $BIN
```
```shell Linux-arm64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=aarch64-unknown-linux-musl && \
curl -LO https://restate.gateway.scarf.sh/latest/restatectl-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restatectl-$RESTATE_PLATFORM.tar.xz --strip-components=1 restatectl-$RESTATE_PLATFORM/restatectl && \
chmod +x restatectl && \
# Move the binary to a directory in your PATH, for example ~/.local/bin:
mv restatectl $BIN
```
```shell MacOS-x64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=x86_64-apple-darwin && \
curl -LO https://restate.gateway.scarf.sh/latest/restatectl-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restatectl-$RESTATE_PLATFORM.tar.xz --strip-components=1 restatectl-$RESTATE_PLATFORM/restatectl && \
chmod +x restatectl && \
# Move the binary to a directory in your PATH, for example /usr/local/bin (needs sudo):
sudo mv restatectl $BIN
```
```shell MacOS-arm64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=aarch64-apple-darwin && \
curl -LO https://restate.gateway.scarf.sh/latest/restatectl-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restatectl-$RESTATE_PLATFORM.tar.xz --strip-components=1 restatectl-$RESTATE_PLATFORM/restatectl && \
chmod +x restatectl && \
# Move the binary to a directory in your PATH, for example /usr/local/bin (needs sudo):
sudo mv restatectl $BIN
```
Then run:
```shell theme={null}
restatectl --help
```
Install with:
```shell theme={null}
npm install --global @restatedev/restatectl@latest
```
Then run:
```shell theme={null}
restatectl --help
```
The server image contains the `restatectl` tool. To run `restatectl`, use the following command:
```shell theme={null}
docker run -it --network=host --entrypoint restatectl docker.restate.dev/restatedev/restate:latest nodes ls
```
You can also execute `restatectl` in a running server container using the following command:
```shell theme={null}
docker exec restate_dev restatectl nodes ls
```
Replace `restate_dev` with the name of a running container, and `nodes ls` with the subcommand you want to run.
`restatectl` requires direct access to nodes via their advertised addresses (default port: 5122). Ensure that restatectl commands can reach these addresses.
# Quickstart
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/quickstart
Develop and run your first Restate service
export const runtime_1 = "Shuttle"
export const runtime_0 = "The local Workers dev server"
export const GitHubLink = ({url}) =>
;
This guide takes you through your first steps with Restate.
We will run a simple Restate Greeter service that listens on port `9080` and responds with `You said hi to !` to a `greet` request.
Select your SDK:
Select your favorite runtime:
**Prerequisites**:
* [NodeJS](https://nodejs.org/en/) >= v20
Restate is a single self-contained binary. No external dependencies needed.
```shell theme={null}
brew install restatedev/tap/restate-server restatedev/tap/restate
```
Start the server:
```shell theme={null}
restate-server
```
Download prebuilt binaries from the [releases page](https://github.com/restatedev/restate/releases/latest):
```shell MacOS-x64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=x86_64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell MacOS-arm64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=aarch64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell Linux-x64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=x86_64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
```shell Linux-arm64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=aarch64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
Start the server:
```shell theme={null}
restate-server
```
```shell theme={null}
npm install --global @restatedev/restate-server@latest @restatedev/restate@latest
```
Start the server:
```shell theme={null}
restate-server
```
Run the Restate Server:
```shell theme={null}
docker run --name restate_dev --rm \
-p 8080:8080 -p 9070:9070 -p 9071:9071 \
--add-host=host.docker.internal:host-gateway \
docker.restate.dev/restatedev/restate:latest
```
Run CLI commands:
```shell theme={null}
docker run -it --network=host \
docker.restate.dev/restatedev/restate-cli:latest \
invocations ls
```
Replace `invocations ls` with any CLI subcommand.
You can find the Restate UI running on port 9070 (`http://localhost:9070`) after starting the Restate Server.
```shell CLI theme={null}
restate example typescript-hello-world &&
cd typescript-hello-world &&
npm install
```
```shell npx theme={null}
npx -y @restatedev/create-app@latest && cd restate-node-template &&
npm install
```
```shell theme={null}
npm run dev
```
Tell Restate where the service is running, so Restate can discover and register the services and handlers behind this endpoint.
You can do this via the UI (`http://localhost:9070`) or via:
```shell CLI theme={null}
restate deployments register http://localhost:9080
```
```shell curl theme={null}
curl localhost:9070/deployments --json '{"uri": "http://localhost:9080"}'
```
```shell CLI theme={null}
❯ SERVICES THAT WILL BE ADDED:
- Greeter
Type: Service
HANDLER INPUT OUTPUT
greet value of content-type 'application/json' value of content-type 'application/json'
✔ Are you sure you want to apply those changes? · yes
✅ DEPLOYMENT:
SERVICE REV
Greeter 1
```
```shell curl theme={null}
{
"id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"services": [
{
"name": "Greeter",
"handlers": [
{
"name": "greet",
"ty": "Shared",
"input_description": "one of [\"none\", \"value of content-type 'application/json'\"]",
"output_description": "value of content-type 'application/json'"
}
],
"ty": "Service",
"deployment_id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"revision": 1,
"public": true,
"idempotency_retention": "1day"
}
]
}
```
If you run Restate with Docker, register `http://host.docker.internal:9080` instead of `http://localhost:9080`.
When using [Restate Cloud](https://restate.dev/cloud), your service must be accessible over the public internet so Restate can invoke it.
If you want to develop with a local service, you can expose it using our [tunnel](/deploy/server/cloud/#registering-restate-services-with-your-environment) feature.
Invoke the service via the Restate UI playground: go to `http://localhost:9070`, click on your service and then on playground.
Or invoke via `curl`:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Sarah"}'
```
Expected output: `You said hi to Sarah!`
The invocation you just sent used Durable Execution to make sure the request ran till completion.
For each request, it sent a notification, slept for a second, and then sent a reminder.
```ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/templates/node/src/app.ts?collapse_prequel"} theme={null}
const greeter = restate.service({
name: "Greeter",
handlers: {
greet: restate.createServiceHandler(
{ input: serde.zod(Greeting), output: serde.zod(GreetingResponse) },
async (ctx: restate.Context, { name }) => {
// Durably execute a set of steps; resilient against failures
const greetingId = ctx.rand.uuidv4();
await ctx.run("Notification", () => sendNotification(greetingId, name));
await ctx.sleep({ seconds: 1 });
await ctx.run("Reminder", () => sendReminder(greetingId, name));
// Respond to caller
return { result: `You said hi to ${name}!` };
},
),
},
});
restate.serve({
services: [greeter],
port: 9080,
});
```
Send a request for `Alice` to see how the service behaves when it occasionally fails to send the reminder and notification:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Alice"}'
```
Expected output:
```shell theme={null}
You said hi to Alice!
```
You can see in the service logs and in the Restate UI how the request gets retried.
On a retry, it skipped the steps that already succeeded.
Even the sleep is durable and tracked by Restate.
If you kill/restart the service halfway through, the sleep will only last for what remained.
Restate persists the progress of the handler.
Letting you write code that is resilient to failures out of the box.
Have a look at the [Durable Execution page](/foundations/key-concepts#durable-execution) to learn more.
**Prerequisites**:
* [Bun](https://bun.sh/docs/installation)
* [npm CLI](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) >= 9.6.7
Restate is a single self-contained binary. No external dependencies needed.
```shell theme={null}
brew install restatedev/tap/restate-server restatedev/tap/restate
```
Start the server:
```shell theme={null}
restate-server
```
Download prebuilt binaries from the [releases page](https://github.com/restatedev/restate/releases/latest):
```shell MacOS-x64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=x86_64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell MacOS-arm64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=aarch64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell Linux-x64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=x86_64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
```shell Linux-arm64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=aarch64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
Start the server:
```shell theme={null}
restate-server
```
```shell theme={null}
npm install --global @restatedev/restate-server@latest @restatedev/restate@latest
```
Start the server:
```shell theme={null}
restate-server
```
Run the Restate Server:
```shell theme={null}
docker run --name restate_dev --rm \
-p 8080:8080 -p 9070:9070 -p 9071:9071 \
--add-host=host.docker.internal:host-gateway \
docker.restate.dev/restatedev/restate:latest
```
Run CLI commands:
```shell theme={null}
docker run -it --network=host \
docker.restate.dev/restatedev/restate-cli:latest \
invocations ls
```
Replace `invocations ls` with any CLI subcommand.
You can find the Restate UI running on port 9070 (`http://localhost:9070`) after starting the Restate Server.
```shell theme={null}
restate example typescript-hello-world-bun &&
cd typescript-hello-world-bun &&
npm install
```
```shell theme={null}
npm run dev
```
Tell Restate where the service is running, so Restate can discover and register the services and handlers behind this endpoint.
You can do this via the UI (`http://localhost:9070`) or via:
```shell CLI theme={null}
restate deployments register http://localhost:9080
```
```shell curl theme={null}
curl localhost:9070/deployments --json '{"uri": "http://localhost:9080"}'
```
```shell CLI theme={null}
❯ SERVICES THAT WILL BE ADDED:
- Greeter
Type: Service
HANDLER INPUT OUTPUT
greet value of content-type 'application/json' value of content-type 'application/json'
✔ Are you sure you want to apply those changes? · yes
✅ DEPLOYMENT:
SERVICE REV
Greeter 1
```
```shell curl theme={null}
{
"id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"services": [
{
"name": "Greeter",
"handlers": [
{
"name": "greet",
"ty": "Shared",
"input_description": "one of [\"none\", \"value of content-type 'application/json'\"]",
"output_description": "value of content-type 'application/json'"
}
],
"ty": "Service",
"deployment_id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"revision": 1,
"public": true,
"idempotency_retention": "1day"
}
]
}
```
If you run Restate with Docker, register `http://host.docker.internal:9080` instead of `http://localhost:9080`.
When using [Restate Cloud](https://restate.dev/cloud), your service must be accessible over the public internet so Restate can invoke it.
If you want to develop with a local service, you can expose it using our [tunnel](/deploy/server/cloud/#registering-restate-services-with-your-environment) feature.
Invoke the service via the Restate UI playground: go to `http://localhost:9070`, click on your service and then on playground.
Or invoke via `curl`:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Sarah"}'
```
Expected output: `You said hi to Sarah!`
The invocation you just sent used Durable Execution to make sure the request ran till completion.
For each request, it sent a notification, slept for a second, and then sent a reminder.
```ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/templates/bun/src/index.ts?collapse_prequel"} theme={null}
const greeter = restate.service({
name: "Greeter",
handlers: {
greet: restate.createServiceHandler(
{ input: serde.zod(Greeting), output: serde.zod(GreetingResponse) },
async (ctx: restate.Context, { name }) => {
// Durably execute a set of steps; resilient against failures
const greetingId = ctx.rand.uuidv4();
await ctx.run("Notification", () => sendNotification(greetingId, name));
await ctx.sleep({ seconds: 1 });
await ctx.run("Reminder", () => sendReminder(greetingId, name));
// Respond to caller
return { result: `You said hi to ${name}!` };
},
),
},
});
restate.serve({
services: [greeter],
port: 9080,
});
```
Send a request for `Alice` to see how the service behaves when it occasionally fails to send the reminder and notification:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Alice"}'
```
Expected output:
```shell theme={null}
You said hi to Alice!
```
You can see in the service logs and in the Restate UI how the request gets retried.
On a retry, it skipped the steps that already succeeded.
Even the sleep is durable and tracked by Restate.
If you kill/restart the service halfway through, the sleep will only last for what remained.
Restate persists the progress of the handler.
Letting you write code that is resilient to failures out of the box.
Have a look at the [Durable Execution page](/foundations/key-concepts#durable-execution) to learn more.
**Prerequisites**:
* [Deno](https://deno.land/#installation)
* [npm CLI](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) >= 9.6.7
Restate is a single self-contained binary. No external dependencies needed.
```shell theme={null}
brew install restatedev/tap/restate-server restatedev/tap/restate
```
Start the server:
```shell theme={null}
restate-server
```
Download prebuilt binaries from the [releases page](https://github.com/restatedev/restate/releases/latest):
```shell MacOS-x64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=x86_64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell MacOS-arm64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=aarch64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell Linux-x64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=x86_64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
```shell Linux-arm64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=aarch64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
Start the server:
```shell theme={null}
restate-server
```
```shell theme={null}
npm install --global @restatedev/restate-server@latest @restatedev/restate@latest
```
Start the server:
```shell theme={null}
restate-server
```
Run the Restate Server:
```shell theme={null}
docker run --name restate_dev --rm \
-p 8080:8080 -p 9070:9070 -p 9071:9071 \
--add-host=host.docker.internal:host-gateway \
docker.restate.dev/restatedev/restate:latest
```
Run CLI commands:
```shell theme={null}
docker run -it --network=host \
docker.restate.dev/restatedev/restate-cli:latest \
invocations ls
```
Replace `invocations ls` with any CLI subcommand.
You can find the Restate UI running on port 9070 (`http://localhost:9070`) after starting the Restate Server.
```shell theme={null}
restate example typescript-hello-world-deno &&
cd typescript-hello-world-deno
```
```shell theme={null}
deno task dev
```
Tell Restate where the service is running, so Restate can discover and register the services and handlers behind this endpoint.
You can do this via the UI (`http://localhost:9070`) or via:
```shell CLI theme={null}
restate deployments register http://localhost:9080
```
```shell curl theme={null}
curl localhost:9070/deployments --json '{"uri": "http://localhost:9080"}'
```
```shell CLI theme={null}
❯ SERVICES THAT WILL BE ADDED:
- Greeter
Type: Service
HANDLER INPUT OUTPUT
greet value of content-type 'application/json' value of content-type 'application/json'
✔ Are you sure you want to apply those changes? · yes
✅ DEPLOYMENT:
SERVICE REV
Greeter 1
```
```shell curl theme={null}
{
"id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"services": [
{
"name": "Greeter",
"handlers": [
{
"name": "greet",
"ty": "Shared",
"input_description": "one of [\"none\", \"value of content-type 'application/json'\"]",
"output_description": "value of content-type 'application/json'"
}
],
"ty": "Service",
"deployment_id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"revision": 1,
"public": true,
"idempotency_retention": "1day"
}
]
}
```
If you run Restate with Docker, register `http://host.docker.internal:9080` instead of `http://localhost:9080`.
When using [Restate Cloud](https://restate.dev/cloud), your service must be accessible over the public internet so Restate can invoke it.
If you want to develop with a local service, you can expose it using our [tunnel](/deploy/server/cloud/#registering-restate-services-with-your-environment) feature.
Invoke the service via the Restate UI playground: go to `http://localhost:9070`, click on your service and then on playground.
Or invoke via `curl`:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Sarah"}'
```
Expected output: `You said hi to Sarah!`
The invocation you just sent used Durable Execution to make sure the request ran till completion.
For each request, it sent a notification, slept for a second, and then sent a reminder.
```ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/templates/deno/main.ts?collapse_prequel"} theme={null}
export const greeter = restate.service({
name: "Greeter",
handlers: {
greet: restate.createServiceHandler(
{ input: serde.zod(Greeting), output: serde.zod(GreetingResponse) },
async (ctx: restate.Context, { name }) => {
// Durably execute a set of steps; resilient against failures
const greetingId = ctx.rand.uuidv4();
await ctx.run("Notification", () => sendNotification(greetingId, name));
await ctx.sleep({ seconds: 1 });
await ctx.run("Reminder", () => sendReminder(greetingId, name));
// Respond to caller
return { result: `You said hi to ${name}!` };
},
),
},
});
const handler = restate.createEndpointHandler({
services: [greeter],
bidirectional: true,
});
Deno.serve({ port: 9080 }, handler);
```
Send a request for `Alice` to see how the service behaves when it occasionally fails to send the reminder and notification:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Alice"}'
```
Expected output:
```shell theme={null}
You said hi to Alice!
```
You can see in the service logs and in the Restate UI how the request gets retried.
On a retry, it skipped the steps that already succeeded.
Even the sleep is durable and tracked by Restate.
If you kill/restart the service halfway through, the sleep will only last for what remained.
Restate persists the progress of the handler.
Letting you write code that is resilient to failures out of the box.
Have a look at the [Durable Execution page](/foundations/key-concepts#durable-execution) to learn more.
**Prerequisites**:
* [NodeJS](https://nodejs.org/en/) >= v20
Restate is a single self-contained binary. No external dependencies needed.
```shell theme={null}
brew install restatedev/tap/restate-server restatedev/tap/restate
```
Start the server:
```shell theme={null}
restate-server
```
Download prebuilt binaries from the [releases page](https://github.com/restatedev/restate/releases/latest):
```shell MacOS-x64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=x86_64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell MacOS-arm64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=aarch64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell Linux-x64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=x86_64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
```shell Linux-arm64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=aarch64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
Start the server:
```shell theme={null}
restate-server
```
```shell theme={null}
npm install --global @restatedev/restate-server@latest @restatedev/restate@latest
```
Start the server:
```shell theme={null}
restate-server
```
Run the Restate Server:
```shell theme={null}
docker run --name restate_dev --rm \
-p 8080:8080 -p 9070:9070 -p 9071:9071 \
--add-host=host.docker.internal:host-gateway \
docker.restate.dev/restatedev/restate:latest
```
Run CLI commands:
```shell theme={null}
docker run -it --network=host \
docker.restate.dev/restatedev/restate-cli:latest \
invocations ls
```
Replace `invocations ls` with any CLI subcommand.
You can find the Restate UI running on port 9070 (`http://localhost:9070`) after starting the Restate Server.
```shell theme={null}
restate example typescript-hello-world-cloudflare-worker &&
cd typescript-hello-world-cloudflare-worker &&
npm install
```
```shell theme={null}
npm run dev
```
Tell Restate where the service is running, so Restate can discover and register the services and handlers behind this endpoint.
You can do this via the UI (`http://localhost:9070`) or via:
```shell CLI theme={null}
restate deployments register http://localhost:9080 --use-http1.1
```
```shell curl theme={null}
curl localhost:9070/deployments --json '{"uri": "http://localhost:9080", "use_http_11": true}'
```
Expected output:
```shell CLI theme={null}
❯ SERVICES THAT WILL BE ADDED:
- Greeter
Type: Service
HANDLER INPUT OUTPUT
greet value of content-type 'application/json' value of content-type 'application/json'
✔ Are you sure you want to apply those changes? · yes
✅ DEPLOYMENT:
SERVICE REV
Greeter 1
```
```shell curl theme={null}
{
"id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"services": [
{
"name": "Greeter",
"handlers": [
{
"name": "greet",
"ty": "Shared",
"input_description": "one of [\"none\", \"value of content-type 'application/json'\"]",
"output_description": "value of content-type 'application/json'"
}
],
"ty": "Service",
"deployment_id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"revision": 1,
"public": true,
"idempotency_retention": "1day"
}
]
}
```
{runtime_0} does not support HTTP2, so we need to tell Restate to use HTTP1.1.
If you run Restate with Docker, use `http://host.docker.internal:9080` instead of `http://localhost:9080`.
When using [Restate Cloud](https://restate.dev/cloud), your service must be accessible over the public internet so Restate can invoke it.
If you want to develop with a local service, you can expose it using our [tunnel](/deploy/server/cloud/#registering-restate-services-with-your-environment) feature.
Invoke the service via the Restate UI playground: go to `http://localhost:9070`, click on your service and then on playground.
Or invoke via `curl`:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Sarah"}'
```
Expected output: `You said hi to Sarah!`
The invocation you just sent used Durable Execution to make sure the request ran till completion.
For each request, it sent a notification, slept for a second, and then sent a reminder.
```ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/templates/cloudflare-worker/src/index.ts?collapse_prequel"} theme={null}
const greeter = restate.service({
name: "Greeter",
handlers: {
greet: restate.createServiceHandler(
{ input: serde.zod(Greeting), output: serde.zod(GreetingResponse) },
async (ctx: restate.Context, { name }) => {
// Durably execute a set of steps; resilient against failures
const greetingId = ctx.rand.uuidv4();
await ctx.run("Notification", () => sendNotification(greetingId, name));
await ctx.sleep({ seconds: 1 });
await ctx.run("Reminder", () => sendReminder(greetingId, name));
// Respond to caller
return { result: `You said hi to ${name}!` };
},
),
},
});
export default {
fetch: restate.createEndpointHandler({ services: [greeter] })
};
```
Send a request for `Alice` to see how the service behaves when it occasionally fails to send the reminder and notification:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Alice"}'
```
Expected output:
```shell theme={null}
You said hi to Alice!
```
You can see in the service logs and in the Restate UI how the request gets retried.
On a retry, it skipped the steps that already succeeded.
Even the sleep is durable and tracked by Restate.
If you kill/restart the service halfway through, the sleep will only last for what remained.
Restate persists the progress of the handler.
Letting you write code that is resilient to failures out of the box.
Have a look at the [Durable Execution page](/foundations/key-concepts#durable-execution) to learn more.
**Prerequisites**:
* [NodeJS](https://nodejs.org/en/) >= v20
Restate is a single self-contained binary. No external dependencies needed.
```shell theme={null}
brew install restatedev/tap/restate-server restatedev/tap/restate
```
Start the server:
```shell theme={null}
restate-server
```
Download prebuilt binaries from the [releases page](https://github.com/restatedev/restate/releases/latest):
```shell MacOS-x64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=x86_64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell MacOS-arm64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=aarch64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell Linux-x64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=x86_64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
```shell Linux-arm64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=aarch64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
Start the server:
```shell theme={null}
restate-server
```
```shell theme={null}
npm install --global @restatedev/restate-server@latest @restatedev/restate@latest
```
Start the server:
```shell theme={null}
restate-server
```
Run the Restate Server:
```shell theme={null}
docker run --name restate_dev --rm \
-p 8080:8080 -p 9070:9070 -p 9071:9071 \
--add-host=host.docker.internal:host-gateway \
docker.restate.dev/restatedev/restate:latest
```
Run CLI commands:
```shell theme={null}
docker run -it --network=host \
docker.restate.dev/restatedev/restate-cli:latest \
invocations ls
```
Replace `invocations ls` with any CLI subcommand.
You can find the Restate UI running on port 9070 (`http://localhost:9070`) after starting the Restate Server.
```shell theme={null}
restate example typescript-hello-world-vercel &&
cd typescript-hello-world-vercel &&
npm install
```
```shell theme={null}
npm run dev
```
```shell CLI theme={null}
restate deployments register http://localhost:3000/restate --use-http1.1
```
```shell curl theme={null}
curl localhost:9070/deployments --json '{"uri": "http://localhost:3000/restate", "use_http_11": true}'
```
```shell CLI theme={null}
❯ SERVICES THAT WILL BE ADDED:
- Greeter
Type: Service
HANDLER INPUT OUTPUT
greet value of content-type 'application/json' value of content-type 'application/json'
✔ Are you sure you want to apply those changes? · yes
✅ DEPLOYMENT:
SERVICE REV
Greeter 1
```
```shell curl theme={null}
{
"id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"services": [
{
"name": "Greeter",
"handlers": [
{
"name": "greet",
"ty": "Shared",
"input_description": "one of [\"none\", \"value of content-type 'application/json'\"]",
"output_description": "value of content-type 'application/json'"
}
],
"ty": "Service",
"deployment_id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"revision": 1,
"public": true,
"idempotency_retention": "1day"
}
]
}
```
If you run Restate with Docker, use `http://host.docker.internal:3000/restate` instead of `http://localhost:3000/restate`.
When using [Restate Cloud](https://restate.dev/cloud), your service must be accessible over the public internet so Restate can invoke it.
If you want to develop with a local service, you can expose it using our [tunnel](/cloud/connecting-services#connecting-services-in-private-environments) feature.
Invoke the service via the Restate UI playground: go to `http://localhost:9070`, click on your service and then on playground.
Or invoke via `curl`:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Sarah"}'
```
Expected output: `You said hi to Sarah!`
The invocation you just sent used Durable Execution to make sure the request ran till completion.
For each request, it sent a notification, slept for a second, and then sent a reminder.
```ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/templates/vercel/src/restate/greeter.ts?collapse_prequel"} theme={null}
export const greeter = restate.service({
name: "Greeter",
handlers: {
greet: restate.createServiceHandler(
{ input: serde.zod(Greeting), output: serde.zod(GreetingResponse) },
async (ctx: restate.Context, { name }) => {
// Durably execute a set of steps; resilient against failures
const greetingId = ctx.rand.uuidv4();
await ctx.run("Notification", () => sendNotification(greetingId, name));
await ctx.sleep({ seconds: 1 });
await ctx.run("Reminder", () => sendReminder(greetingId, name));
// Respond to caller
return { result: `You said hi to ${name}!` };
},
),
},
});
export type Greeter = typeof greeter;
```
Send a request for `Alice` to see how the service behaves when it occasionally fails to send the reminder and notification:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Alice"}'
```
Expected output:
```shell theme={null}
You said hi to Alice!
```
You can see in the service logs and in the Restate UI how the request gets retried.
On a retry, it skipped the steps that already succeeded.
Even the sleep is durable and tracked by Restate.
If you kill/restart the service halfway through, the sleep will only last for what remained.
Restate persists the progress of the handler.
Letting you write code that is resilient to failures out of the box.
Have a look at the [Durable Execution page](/foundations/key-concepts#durable-execution) to learn more.
Select your favorite build tool and framework:
**Prerequisites**:
* [JDK](https://whichjdk.com/) >= 17
Restate is a single self-contained binary. No external dependencies needed.
```shell theme={null}
brew install restatedev/tap/restate-server restatedev/tap/restate
```
Start the server:
```shell theme={null}
restate-server
```
Download prebuilt binaries from the [releases page](https://github.com/restatedev/restate/releases/latest):
```shell MacOS-x64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=x86_64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell MacOS-arm64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=aarch64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell Linux-x64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=x86_64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
```shell Linux-arm64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=aarch64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
Start the server:
```shell theme={null}
restate-server
```
```shell theme={null}
npm install --global @restatedev/restate-server@latest @restatedev/restate@latest
```
Start the server:
```shell theme={null}
restate-server
```
Run the Restate Server:
```shell theme={null}
docker run --name restate_dev --rm \
-p 8080:8080 -p 9070:9070 -p 9071:9071 \
--add-host=host.docker.internal:host-gateway \
docker.restate.dev/restatedev/restate:latest
```
Run CLI commands:
```shell theme={null}
docker run -it --network=host \
docker.restate.dev/restatedev/restate-cli:latest \
invocations ls
```
Replace `invocations ls` with any CLI subcommand.
You can find the Restate UI running on port 9070 (`http://localhost:9070`) after starting the Restate Server.
```shell CLI theme={null}
restate example java-hello-world-maven-spring-boot &&
cd java-hello-world-maven-spring-boot
```
```shell wget theme={null}
wget https://github.com/restatedev/examples/releases/latest/download/java-hello-world-maven-spring-boot.zip &&
unzip java-hello-world-maven-spring-boot.zip -d java-hello-world-maven-spring-boot &&
rm java-hello-world-maven-spring-boot.zip && cd java-hello-world-maven-spring-boot
```
You are all set to start developing your service.
Open the project in an IDE, run your service and let it listen on port `9080` for requests:
```shell theme={null}
mvn compile spring-boot:run
```
Tell Restate where the service is running, so Restate can discover and register the services and handlers behind this endpoint.
You can do this via the UI (`http://localhost:9070`) or via:
```shell CLI theme={null}
restate deployments register http://localhost:9080
```
```shell curl theme={null}
curl localhost:9070/deployments --json '{"uri": "http://localhost:9080"}'
```
```shell CLI theme={null}
❯ SERVICES THAT WILL BE ADDED:
- Greeter
Type: Service
HANDLER INPUT OUTPUT
greet value of content-type 'application/json' value of content-type 'application/json'
✔ Are you sure you want to apply those changes? · yes
✅ DEPLOYMENT:
SERVICE REV
Greeter 1
```
```shell curl theme={null}
{
"id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"services": [
{
"name": "Greeter",
"handlers": [
{
"name": "greet",
"ty": "Shared",
"input_description": "one of [\"none\", \"value of content-type 'application/json'\"]",
"output_description": "value of content-type 'application/json'"
}
],
"ty": "Service",
"deployment_id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"revision": 1,
"public": true,
"idempotency_retention": "1day"
}
]
}
```
If you run Restate with Docker, register `http://host.docker.internal:9080` instead of `http://localhost:9080`.
When using [Restate Cloud](https://restate.dev/cloud), your service must be accessible over the public internet so Restate can invoke it.
If you want to develop with a local service, you can expose it using our [tunnel](/deploy/server/cloud/#registering-restate-services-with-your-environment) feature.
Invoke the service via the Restate UI playground: go to `http://localhost:9070`, click on your service and then on playground.
Or invoke via `curl`:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Sarah"}'
```
Expected output: `You said hi to Sarah!`
The invocation you just sent used Durable Execution to make sure the request ran till completion.
For each request, it sent a notification, slept for a second, and then sent a reminder.
```java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/templates/java-maven-spring-boot/src/main/java/com/example/restatestarter/Greeter.java?collapse_prequel"} theme={null}
@RestateService
public class Greeter {
@Value("${greetingPrefix}")
private String greetingPrefix;
public record Greeting(String name) {}
public record GreetingResponse(String message) {}
@Handler
public GreetingResponse greet(Context ctx, Greeting req) {
// Durably execute a set of steps; resilient against failures
String greetingId = ctx.random().nextUUID().toString();
ctx.run("Notification", () -> sendNotification(greetingId, req.name));
ctx.sleep(Duration.ofSeconds(1));
ctx.run("Reminder", () -> sendReminder(greetingId, req.name));
// Respond to caller
return new GreetingResponse("You said " + greetingPrefix + " to " + req.name + "!");
}
}
```
Send a request for `Alice` to see how the service behaves when it occasionally fails to send the reminder and notification:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Alice"}'
```
Expected output:
```shell theme={null}
You said hi to Alice!
```
You can see in the service logs and in the Restate UI how the request gets retried.
On a retry, it skipped the steps that already succeeded.
Even the sleep is durable and tracked by Restate.
If you kill/restart the service halfway through, the sleep will only last for what remained.
Restate persists the progress of the handler.
Letting you write code that is resilient to failures out of the box.
Have a look at the [Durable Execution page](/foundations/key-concepts#durable-execution) to learn more.
**Prerequisites**:
* [JDK](https://whichjdk.com/) >= 17
Restate is a single self-contained binary. No external dependencies needed.
```shell theme={null}
brew install restatedev/tap/restate-server restatedev/tap/restate
```
Start the server:
```shell theme={null}
restate-server
```
Download prebuilt binaries from the [releases page](https://github.com/restatedev/restate/releases/latest):
```shell MacOS-x64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=x86_64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell MacOS-arm64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=aarch64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell Linux-x64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=x86_64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
```shell Linux-arm64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=aarch64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
Start the server:
```shell theme={null}
restate-server
```
```shell theme={null}
npm install --global @restatedev/restate-server@latest @restatedev/restate@latest
```
Start the server:
```shell theme={null}
restate-server
```
Run the Restate Server:
```shell theme={null}
docker run --name restate_dev --rm \
-p 8080:8080 -p 9070:9070 -p 9071:9071 \
--add-host=host.docker.internal:host-gateway \
docker.restate.dev/restatedev/restate:latest
```
Run CLI commands:
```shell theme={null}
docker run -it --network=host \
docker.restate.dev/restatedev/restate-cli:latest \
invocations ls
```
Replace `invocations ls` with any CLI subcommand.
You can find the Restate UI running on port 9070 (`http://localhost:9070`) after starting the Restate Server.
```shell CLI theme={null}
restate example java-hello-world-maven-quarkus &&
cd java-hello-world-maven-quarkus
```
```shell wget theme={null}
wget https://github.com/restatedev/examples/releases/latest/download/java-hello-world-maven-quarkus.zip &&
unzip java-hello-world-maven-quarkus.zip -d java-hello-world-maven-quarkus &&
rm java-hello-world-maven-quarkus.zip && cd java-hello-world-maven-quarkus
```
You are all set to start developing your service.
Open the project in an IDE, run your service and let it listen on port `9080` for requests:
```shell theme={null}
quarkus dev
```
Tell Restate where the service is running, so Restate can discover and register the services and handlers behind this endpoint.
You can do this via the UI (`http://localhost:9070`) or via:
```shell CLI theme={null}
restate deployments register http://localhost:9080
```
```shell curl theme={null}
curl localhost:9070/deployments --json '{"uri": "http://localhost:9080"}'
```
```shell CLI theme={null}
❯ SERVICES THAT WILL BE ADDED:
- Greeter
Type: Service
HANDLER INPUT OUTPUT
greet value of content-type 'application/json' value of content-type 'application/json'
✔ Are you sure you want to apply those changes? · yes
✅ DEPLOYMENT:
SERVICE REV
Greeter 1
```
```shell curl theme={null}
{
"id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"services": [
{
"name": "Greeter",
"handlers": [
{
"name": "greet",
"ty": "Shared",
"input_description": "one of [\"none\", \"value of content-type 'application/json'\"]",
"output_description": "value of content-type 'application/json'"
}
],
"ty": "Service",
"deployment_id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"revision": 1,
"public": true,
"idempotency_retention": "1day"
}
]
}
```
If you run Restate with Docker, register `http://host.docker.internal:9080` instead of `http://localhost:9080`.
When using [Restate Cloud](https://restate.dev/cloud), your service must be accessible over the public internet so Restate can invoke it.
If you want to develop with a local service, you can expose it using our [tunnel](/deploy/server/cloud/#registering-restate-services-with-your-environment) feature.
Invoke the service via the Restate UI playground: go to `http://localhost:9070`, click on your service and then on playground.
Or invoke via `curl`:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Sarah"}'
```
Expected output: `You said hi to Sarah!`
The invocation you just sent used Durable Execution to make sure the request ran till completion.
For each request, it sent a notification, slept for a second, and then sent a reminder.
```java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/templates/java-maven-quarkus/src/main/java/org/acme/Greeter.java?collapse_prequel"} theme={null}
@Service
public class Greeter {
@ConfigProperty(name = "greetingPrefix") String greetingPrefix;
record Greeting(String name) {}
record GreetingResponse(String message) {}
@Handler
public GreetingResponse greet(Context ctx, Greeting req) {
// Durably execute a set of steps; resilient against failures
String greetingId = ctx.random().nextUUID().toString();
ctx.run("Notification", () -> sendNotification(greetingId, req.name));
ctx.sleep(Duration.ofSeconds(1));
ctx.run("Reminder", () -> sendReminder(greetingId, req.name));
// Respond to caller
return new GreetingResponse("You said hi to " + req.name + "!");
}
}
```
Send a request for `Alice` to see how the service behaves when it occasionally fails to send the reminder and notification:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Alice"}'
```
Expected output:
```shell theme={null}
You said hi to Alice!
```
You can see in the service logs and in the Restate UI how the request gets retried.
On a retry, it skipped the steps that already succeeded.
Even the sleep is durable and tracked by Restate.
If you kill/restart the service halfway through, the sleep will only last for what remained.
Restate persists the progress of the handler.
Letting you write code that is resilient to failures out of the box.
Have a look at the [Durable Execution page](/foundations/key-concepts#durable-execution) to learn more.
**Prerequisites**:
* [JDK](https://whichjdk.com/) >= 17
Restate is a single self-contained binary. No external dependencies needed.
```shell theme={null}
brew install restatedev/tap/restate-server restatedev/tap/restate
```
Start the server:
```shell theme={null}
restate-server
```
Download prebuilt binaries from the [releases page](https://github.com/restatedev/restate/releases/latest):
```shell MacOS-x64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=x86_64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell MacOS-arm64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=aarch64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell Linux-x64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=x86_64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
```shell Linux-arm64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=aarch64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
Start the server:
```shell theme={null}
restate-server
```
```shell theme={null}
npm install --global @restatedev/restate-server@latest @restatedev/restate@latest
```
Start the server:
```shell theme={null}
restate-server
```
Run the Restate Server:
```shell theme={null}
docker run --name restate_dev --rm \
-p 8080:8080 -p 9070:9070 -p 9071:9071 \
--add-host=host.docker.internal:host-gateway \
docker.restate.dev/restatedev/restate:latest
```
Run CLI commands:
```shell theme={null}
docker run -it --network=host \
docker.restate.dev/restatedev/restate-cli:latest \
invocations ls
```
Replace `invocations ls` with any CLI subcommand.
You can find the Restate UI running on port 9070 (`http://localhost:9070`) after starting the Restate Server.
```shell CLI theme={null}
restate example java-hello-world-maven &&
cd java-hello-world-maven
```
```shell wget theme={null}
wget https://github.com/restatedev/examples/releases/latest/download/java-hello-world-maven.zip &&
unzip java-hello-world-maven.zip -d java-hello-world-maven &&
rm java-hello-world-maven.zip && cd java-hello-world-maven
```
You are all set to start developing your service.
Open the project in an IDE, run your service and let it listen on port `9080` for requests:
```shell theme={null}
mvn compile exec:java
```
Tell Restate where the service is running, so Restate can discover and register the services and handlers behind this endpoint.
You can do this via the UI (`http://localhost:9070`) or via:
```shell CLI theme={null}
restate deployments register http://localhost:9080
```
```shell curl theme={null}
curl localhost:9070/deployments --json '{"uri": "http://localhost:9080"}'
```
```shell CLI theme={null}
❯ SERVICES THAT WILL BE ADDED:
- Greeter
Type: Service
HANDLER INPUT OUTPUT
greet value of content-type 'application/json' value of content-type 'application/json'
✔ Are you sure you want to apply those changes? · yes
✅ DEPLOYMENT:
SERVICE REV
Greeter 1
```
```shell curl theme={null}
{
"id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"services": [
{
"name": "Greeter",
"handlers": [
{
"name": "greet",
"ty": "Shared",
"input_description": "one of [\"none\", \"value of content-type 'application/json'\"]",
"output_description": "value of content-type 'application/json'"
}
],
"ty": "Service",
"deployment_id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"revision": 1,
"public": true,
"idempotency_retention": "1day"
}
]
}
```
If you run Restate with Docker, register `http://host.docker.internal:9080` instead of `http://localhost:9080`.
When using [Restate Cloud](https://restate.dev/cloud), your service must be accessible over the public internet so Restate can invoke it.
If you want to develop with a local service, you can expose it using our [tunnel](/deploy/server/cloud/#registering-restate-services-with-your-environment) feature.
Invoke the service via the Restate UI playground: go to `http://localhost:9070`, click on your service and then on playground.
Or invoke via `curl`:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Sarah"}'
```
Expected output: `You said hi to Sarah!`
The invocation you just sent used Durable Execution to make sure the request ran till completion.
For each request, it sent a notification, slept for a second, and then sent a reminder.
```java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/templates/java-maven/src/main/java/my/example/Greeter.java?collapse_prequel"} theme={null}
@Service
public class Greeter {
record Greeting(String name) {}
record GreetingResponse(String message) {}
@Handler
public GreetingResponse greet(Context ctx, Greeting req) {
// Durably execute a set of steps; resilient against failures
String greetingId = ctx.random().nextUUID().toString();
ctx.run("Notification", () -> sendNotification(greetingId, req.name));
ctx.sleep(Duration.ofSeconds(1));
ctx.run("Reminder", () -> sendReminder(greetingId, req.name));
// Respond to caller
return new GreetingResponse("You said hi to " + req.name + "!");
}
public static void main(String[] args) {
RestateHttpServer.listen(Endpoint.bind(new Greeter()));
}
}
```
Send a request for `Alice` to see how the service behaves when it occasionally fails to send the reminder and notification:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Alice"}'
```
Expected output:
```shell theme={null}
You said hi to Alice!
```
You can see in the service logs and in the Restate UI how the request gets retried.
On a retry, it skipped the steps that already succeeded.
Even the sleep is durable and tracked by Restate.
If you kill/restart the service halfway through, the sleep will only last for what remained.
Restate persists the progress of the handler.
Letting you write code that is resilient to failures out of the box.
Have a look at the [Durable Execution page](/foundations/key-concepts#durable-execution) to learn more.
**Prerequisites**:
* [JDK](https://whichjdk.com/) >= 17
Restate is a single self-contained binary. No external dependencies needed.
```shell theme={null}
brew install restatedev/tap/restate-server restatedev/tap/restate
```
Start the server:
```shell theme={null}
restate-server
```
Download prebuilt binaries from the [releases page](https://github.com/restatedev/restate/releases/latest):
```shell MacOS-x64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=x86_64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell MacOS-arm64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=aarch64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell Linux-x64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=x86_64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
```shell Linux-arm64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=aarch64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
Start the server:
```shell theme={null}
restate-server
```
```shell theme={null}
npm install --global @restatedev/restate-server@latest @restatedev/restate@latest
```
Start the server:
```shell theme={null}
restate-server
```
Run the Restate Server:
```shell theme={null}
docker run --name restate_dev --rm \
-p 8080:8080 -p 9070:9070 -p 9071:9071 \
--add-host=host.docker.internal:host-gateway \
docker.restate.dev/restatedev/restate:latest
```
Run CLI commands:
```shell theme={null}
docker run -it --network=host \
docker.restate.dev/restatedev/restate-cli:latest \
invocations ls
```
Replace `invocations ls` with any CLI subcommand.
You can find the Restate UI running on port 9070 (`http://localhost:9070`) after starting the Restate Server.
```shell CLI theme={null}
restate example java-hello-world-gradle &&
cd java-hello-world-gradle
```
```shell wget theme={null}
wget https://github.com/restatedev/examples/releases/latest/download/java-hello-world-gradle.zip &&
unzip java-hello-world-gradle.zip -d java-hello-world-gradle &&
rm java-hello-world-gradle.zip && cd java-hello-world-gradle
```
You are all set to start developing your service.
Open the project in an IDE, run your service and let it listen on port `9080` for requests:
```shell theme={null}
./gradlew run
```
Tell Restate where the service is running, so Restate can discover and register the services and handlers behind this endpoint.
You can do this via the UI (`http://localhost:9070`) or via:
```shell CLI theme={null}
restate deployments register http://localhost:9080
```
```shell curl theme={null}
curl localhost:9070/deployments --json '{"uri": "http://localhost:9080"}'
```
```shell CLI theme={null}
❯ SERVICES THAT WILL BE ADDED:
- Greeter
Type: Service
HANDLER INPUT OUTPUT
greet value of content-type 'application/json' value of content-type 'application/json'
✔ Are you sure you want to apply those changes? · yes
✅ DEPLOYMENT:
SERVICE REV
Greeter 1
```
```shell curl theme={null}
{
"id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"services": [
{
"name": "Greeter",
"handlers": [
{
"name": "greet",
"ty": "Shared",
"input_description": "one of [\"none\", \"value of content-type 'application/json'\"]",
"output_description": "value of content-type 'application/json'"
}
],
"ty": "Service",
"deployment_id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"revision": 1,
"public": true,
"idempotency_retention": "1day"
}
]
}
```
If you run Restate with Docker, register `http://host.docker.internal:9080` instead of `http://localhost:9080`.
When using [Restate Cloud](https://restate.dev/cloud), your service must be accessible over the public internet so Restate can invoke it.
If you want to develop with a local service, you can expose it using our [tunnel](/deploy/server/cloud/#registering-restate-services-with-your-environment) feature.
Invoke the service via the Restate UI playground: go to `http://localhost:9070`, click on your service and then on playground.
Or invoke via `curl`:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Sarah"}'
```
Expected output: `You said hi to Sarah!`
The invocation you just sent used Durable Execution to make sure the request ran till completion.
For each request, it sent a notification, slept for a second, and then sent a reminder.
```java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/templates/java-gradle/src/main/java/my/example/Greeter.java?collapse_prequel"} theme={null}
@Service
public class Greeter {
record Greeting(String name) {}
record GreetingResponse(String message) {}
@Handler
public GreetingResponse greet(Context ctx, Greeting req) {
// Durably execute a set of steps; resilient against failures
String greetingId = ctx.random().nextUUID().toString();
ctx.run("Notification", () -> sendNotification(greetingId, req.name));
ctx.sleep(Duration.ofSeconds(1));
ctx.run("Reminder", () -> sendReminder(greetingId, req.name));
// Respond to caller
return new GreetingResponse("You said hi to " + req.name + "!");
}
public static void main(String[] args) {
RestateHttpServer.listen(Endpoint.bind(new Greeter()));
}
}
```
Send a request for `Alice` to see how the service behaves when it occasionally fails to send the reminder and notification:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Alice"}'
```
Expected output:
```shell theme={null}
You said hi to Alice!
```
You can see in the service logs and in the Restate UI how the request gets retried.
On a retry, it skipped the steps that already succeeded.
Even the sleep is durable and tracked by Restate.
If you kill/restart the service halfway through, the sleep will only last for what remained.
Restate persists the progress of the handler.
Letting you write code that is resilient to failures out of the box.
Have a look at the [Durable Execution page](/foundations/key-concepts#durable-execution) to learn more.
Select your favorite framework:
**Prerequisites**:
* [JDK](https://whichjdk.com/) >= 17
Restate is a single self-contained binary. No external dependencies needed.
```shell theme={null}
brew install restatedev/tap/restate-server restatedev/tap/restate
```
Start the server:
```shell theme={null}
restate-server
```
Download prebuilt binaries from the [releases page](https://github.com/restatedev/restate/releases/latest):
```shell MacOS-x64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=x86_64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell MacOS-arm64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=aarch64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell Linux-x64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=x86_64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
```shell Linux-arm64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=aarch64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
Start the server:
```shell theme={null}
restate-server
```
```shell theme={null}
npm install --global @restatedev/restate-server@latest @restatedev/restate@latest
```
Start the server:
```shell theme={null}
restate-server
```
Run the Restate Server:
```shell theme={null}
docker run --name restate_dev --rm \
-p 8080:8080 -p 9070:9070 -p 9071:9071 \
--add-host=host.docker.internal:host-gateway \
docker.restate.dev/restatedev/restate:latest
```
Run CLI commands:
```shell theme={null}
docker run -it --network=host \
docker.restate.dev/restatedev/restate-cli:latest \
invocations ls
```
Replace `invocations ls` with any CLI subcommand.
You can find the Restate UI running on port 9070 (`http://localhost:9070`) after starting the Restate Server.
```shell CLI theme={null}
restate example kotlin-hello-world-gradle-spring-boot &&
cd kotlin-hello-world-gradle-spring-boot
```
```shell wget theme={null}
wget https://github.com/restatedev/examples/releases/latest/download/kotlin-hello-world-gradle-spring-boot.zip &&
unzip kotlin-hello-world-gradle-spring-boot.zip -d kotlin-hello-world-gradle-spring-boot &&
rm kotlin-hello-world-gradle-spring-boot.zip && cd kotlin-hello-world-gradle-spring-boot
```
You are all set to start developing your service.
Open the project in an IDE and configure it to build with Gradle.
Run your service and let it listen on port `9080` for requests:
```shell theme={null}
./gradlew bootRun
```
Tell Restate where the service is running, so Restate can discover and register the services and handlers behind this endpoint.
You can do this via the UI (`http://localhost:9070`) or via:
```shell CLI theme={null}
restate deployments register http://localhost:9080
```
```shell curl theme={null}
curl localhost:9070/deployments --json '{"uri": "http://localhost:9080"}'
```
```shell CLI theme={null}
❯ SERVICES THAT WILL BE ADDED:
- Greeter
Type: Service
HANDLER INPUT OUTPUT
greet value of content-type 'application/json' value of content-type 'application/json'
✔ Are you sure you want to apply those changes? · yes
✅ DEPLOYMENT:
SERVICE REV
Greeter 1
```
```shell curl theme={null}
{
"id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"services": [
{
"name": "Greeter",
"handlers": [
{
"name": "greet",
"ty": "Shared",
"input_description": "one of [\"none\", \"value of content-type 'application/json'\"]",
"output_description": "value of content-type 'application/json'"
}
],
"ty": "Service",
"deployment_id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"revision": 1,
"public": true,
"idempotency_retention": "1day"
}
]
}
```
If you run Restate with Docker, register `http://host.docker.internal:9080` instead of `http://localhost:9080`.
When using [Restate Cloud](https://restate.dev/cloud), your service must be accessible over the public internet so Restate can invoke it.
If you want to develop with a local service, you can expose it using our [tunnel](/deploy/server/cloud/#registering-restate-services-with-your-environment) feature.
Invoke the service via the Restate UI playground: go to `http://localhost:9070`, click on your service and then on playground.
Or invoke via `curl`:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Sarah"}'
```
Expected output: `You said hi to Sarah!`
The invocation you just sent used Durable Execution to make sure the request ran till completion.
For each request, it sent a notification, slept for a second, and then sent a reminder.
```kotlin {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/kotlin/templates/kotlin-gradle-spring-boot/src/main/kotlin/com/example/restatestarter/Greeter.kt?collapse_prequel"} theme={null}
@RestateService
class Greeter {
@Value("\${greetingPrefix}")
lateinit var greetingPrefix: String
@Serializable
data class Greeting(val name: String)
@Serializable
data class GreetingResponse(val message: String)
@Handler
suspend fun greet(ctx: Context, req: Greeting): GreetingResponse {
// Durably execute a set of steps; resilient against failures
val greetingId = ctx.random().nextUUID().toString()
ctx.runBlock("Notification") { sendNotification(greetingId, req.name) }
ctx.sleep(1.seconds)
ctx.runBlock("Reminder") { sendReminder(greetingId, req.name) }
// Respond to caller
return GreetingResponse("You said $greetingPrefix to ${req.name}!")
}
}
```
Send a request for `Alice` to see how the service behaves when it occasionally fails to send the reminder and notification:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Alice"}'
```
Expected output:
```shell theme={null}
You said hi to Alice!
```
You can see in the service logs and in the Restate UI how the request gets retried.
On a retry, it skipped the steps that already succeeded.
Even the sleep is durable and tracked by Restate.
If you kill/restart the service halfway through, the sleep will only last for what remained.
Restate persists the progress of the handler.
Letting you write code that is resilient to failures out of the box.
Have a look at the [Durable Execution page](/foundations/key-concepts#durable-execution) to learn more.
**Prerequisites**:
* [JDK](https://whichjdk.com/) >= 17
Restate is a single self-contained binary. No external dependencies needed.
```shell theme={null}
brew install restatedev/tap/restate-server restatedev/tap/restate
```
Start the server:
```shell theme={null}
restate-server
```
Download prebuilt binaries from the [releases page](https://github.com/restatedev/restate/releases/latest):
```shell MacOS-x64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=x86_64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell MacOS-arm64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=aarch64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell Linux-x64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=x86_64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
```shell Linux-arm64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=aarch64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
Start the server:
```shell theme={null}
restate-server
```
```shell theme={null}
npm install --global @restatedev/restate-server@latest @restatedev/restate@latest
```
Start the server:
```shell theme={null}
restate-server
```
Run the Restate Server:
```shell theme={null}
docker run --name restate_dev --rm \
-p 8080:8080 -p 9070:9070 -p 9071:9071 \
--add-host=host.docker.internal:host-gateway \
docker.restate.dev/restatedev/restate:latest
```
Run CLI commands:
```shell theme={null}
docker run -it --network=host \
docker.restate.dev/restatedev/restate-cli:latest \
invocations ls
```
Replace `invocations ls` with any CLI subcommand.
You can find the Restate UI running on port 9070 (`http://localhost:9070`) after starting the Restate Server.
```shell CLI theme={null}
restate example kotlin-hello-world-gradle &&
cd kotlin-hello-world-gradle
```
```shell wget theme={null}
wget https://github.com/restatedev/examples/releases/latest/download/kotlin-hello-world-gradle.zip &&
unzip kotlin-hello-world-gradle.zip -d kotlin-hello-world-gradle &&
rm kotlin-hello-world-gradle.zip && cd kotlin-hello-world-gradle
```
You are all set to start developing your service.
Open the project in an IDE and configure it to build with Gradle.
Run your service and let it listen on port `9080` for requests:
```shell theme={null}
./gradlew run
```
Tell Restate where the service is running, so Restate can discover and register the services and handlers behind this endpoint.
You can do this via the UI (`http://localhost:9070`) or via:
```shell CLI theme={null}
restate deployments register http://localhost:9080
```
```shell curl theme={null}
curl localhost:9070/deployments --json '{"uri": "http://localhost:9080"}'
```
```shell CLI theme={null}
❯ SERVICES THAT WILL BE ADDED:
- Greeter
Type: Service
HANDLER INPUT OUTPUT
greet value of content-type 'application/json' value of content-type 'application/json'
✔ Are you sure you want to apply those changes? · yes
✅ DEPLOYMENT:
SERVICE REV
Greeter 1
```
```shell curl theme={null}
{
"id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"services": [
{
"name": "Greeter",
"handlers": [
{
"name": "greet",
"ty": "Shared",
"input_description": "one of [\"none\", \"value of content-type 'application/json'\"]",
"output_description": "value of content-type 'application/json'"
}
],
"ty": "Service",
"deployment_id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"revision": 1,
"public": true,
"idempotency_retention": "1day"
}
]
}
```
If you run Restate with Docker, register `http://host.docker.internal:9080` instead of `http://localhost:9080`.
When using [Restate Cloud](https://restate.dev/cloud), your service must be accessible over the public internet so Restate can invoke it.
If you want to develop with a local service, you can expose it using our [tunnel](/deploy/server/cloud/#registering-restate-services-with-your-environment) feature.
Invoke the service via the Restate UI playground: go to `http://localhost:9070`, click on your service and then on playground.
Or invoke via `curl`:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Sarah"}'
```
Expected output: `You said hi to Sarah!`
The invocation you just sent used Durable Execution to make sure the request ran till completion.
For each request, it sent a notification, slept for a second, and then sent a reminder.
```kotlin {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/kotlin/templates/kotlin-gradle/src/main/kotlin/my/example/Greeter.kt?collapse_prequel"} theme={null}
@Service
class Greeter {
@Serializable
data class Greeting(val name: String)
@Serializable
data class GreetingResponse(val message: String)
@Handler
suspend fun greet(ctx: Context, req: Greeting): GreetingResponse {
// Durably execute a set of steps; resilient against failures
val greetingId = ctx.random().nextUUID().toString()
ctx.runBlock("Notification") { sendNotification(greetingId, req.name) }
ctx.sleep(1.seconds)
ctx.runBlock("Reminder") { sendReminder(greetingId, req.name) }
// Respond to caller
return GreetingResponse("You said hi to ${req.name}!")
}
}
fun main() {
RestateHttpServer.listen(endpoint {
bind(Greeter())
})
}
```
Send a request for `Alice` to see how the service behaves when it occasionally fails to send the reminder and notification:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Alice"}'
```
Expected output:
```shell theme={null}
You said hi to Alice!
```
You can see in the service logs and in the Restate UI how the request gets retried.
On a retry, it skipped the steps that already succeeded.
Even the sleep is durable and tracked by Restate.
If you kill/restart the service halfway through, the sleep will only last for what remained.
Restate persists the progress of the handler.
Letting you write code that is resilient to failures out of the box.
Have a look at the [Durable Execution page](/foundations/key-concepts#durable-execution) to learn more.
**Prerequisites**:
* Go: >= 1.21.0
Restate is a single self-contained binary. No external dependencies needed.
```shell theme={null}
brew install restatedev/tap/restate-server restatedev/tap/restate
```
Start the server:
```shell theme={null}
restate-server
```
Download prebuilt binaries from the [releases page](https://github.com/restatedev/restate/releases/latest):
```shell MacOS-x64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=x86_64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell MacOS-arm64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=aarch64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell Linux-x64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=x86_64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
```shell Linux-arm64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=aarch64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
Start the server:
```shell theme={null}
restate-server
```
```shell theme={null}
npm install --global @restatedev/restate-server@latest @restatedev/restate@latest
```
Start the server:
```shell theme={null}
restate-server
```
Run the Restate Server:
```shell theme={null}
docker run --name restate_dev --rm \
-p 8080:8080 -p 9070:9070 -p 9071:9071 \
--add-host=host.docker.internal:host-gateway \
docker.restate.dev/restatedev/restate:latest
```
Run CLI commands:
```shell theme={null}
docker run -it --network=host \
docker.restate.dev/restatedev/restate-cli:latest \
invocations ls
```
Replace `invocations ls` with any CLI subcommand.
You can find the Restate UI running on port 9070 (`http://localhost:9070`) after starting the Restate Server.
```shell CLI theme={null}
restate example go-hello-world &&
cd go-hello-world
```
```shell wget theme={null}
wget https://github.com/restatedev/examples/releases/latest/download/go-hello-world.zip &&
unzip go-hello-world.zip -d go-hello-world &&
rm go-hello-world.zip && cd go-hello-world
```
Now, start developing your service in `greeter.go`. Run it with:
```shell theme={null}
go run .
```
it will listen on port 9080 for requests.
Tell Restate where the service is running, so Restate can discover and register the services and handlers behind this endpoint.
You can do this via the UI (`http://localhost:9070`) or via:
```shell CLI theme={null}
restate deployments register http://localhost:9080
```
```shell curl theme={null}
curl localhost:9070/deployments --json '{"uri": "http://localhost:9080"}'
```
```shell CLI theme={null}
❯ SERVICES THAT WILL BE ADDED:
- Greeter
Type: Service
HANDLER INPUT OUTPUT
Greet value of content-type 'application/json' value of content-type 'application/json'
✔ Are you sure you want to apply those changes? · yes
✅ DEPLOYMENT:
SERVICE REV
Greeter 1
```
```shell curl theme={null}
{
"id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"services": [
{
"name": "Greeter",
"handlers": [
{
"name": "Greet",
"ty": "Shared",
"input_description": "one of [\"none\", \"value of content-type 'application/json'\"]",
"output_description": "value of content-type 'application/json'"
}
],
"ty": "Service",
"deployment_id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"revision": 1,
"public": true,
"idempotency_retention": "1day"
}
]
}
```
If you run Restate with Docker, use `http://host.docker.internal:9080` instead of `http://localhost:9080`.
Invoke the service via the Restate UI playground: go to `http://localhost:9070`, click on your service and then on playground.
Or invoke via `curl`:
```shell theme={null}
curl localhost:8080/Greeter/Greet --json '"Sarah"'
```
Expected output:
```
You said hi to Sarah!
```
The invocation you just sent used Durable Execution to make sure the request ran till completion.
For each request, it sent a notification, slept for a second, and then sent a reminder.
```go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/templates/go/greeter.go?collapse_prequel"} theme={null}
type Greeter struct{}
func (Greeter) Greet(ctx restate.Context, name string) (string, error) {
// Durably execute a set of steps; resilient against failures
greetingId := restate.Rand(ctx).UUID().String()
if _, err := restate.Run(ctx,
func(ctx restate.RunContext) (restate.Void, error) {
return SendNotification(greetingId, name)
},
restate.WithName("Notification"),
); err != nil {
return "", err
}
if err := restate.Sleep(ctx, 1*time.Second); err != nil {
return "", err
}
if _, err := restate.Run(ctx,
func(ctx restate.RunContext) (restate.Void, error) {
return SendReminder(greetingId, name)
},
restate.WithName("Reminder"),
); err != nil {
return "", err
}
// Respond to caller
return "You said hi to " + name + "!", nil
}
```
Send a request for `Alice` to see how the service behaves when it occasionally fails to send the reminder and notification:
```shell theme={null}
curl localhost:8080/Greeter/Greet --json '"Alice"'
```
Expected output:
```
You said hi to Alice!
```
You can see in the service logs and in the Restate UI how the request gets retried.
On a retry, it skipped the steps that already succeeded.
Even the sleep is durable and tracked by Restate.
If you kill/restart the service halfway through, the sleep will only last for what remained.
Restate persists the progress of the handler.
Letting you write code that is resilient to failures out of the box.
Have a look at the [Durable Execution page](/foundations/key-concepts#durable-execution) to learn more.
**Prerequisites**:
* Python >= v3.11
* [uv](https://docs.astral.sh/uv/getting-started/installation/)
Restate is a single self-contained binary. No external dependencies needed.
```shell theme={null}
brew install restatedev/tap/restate-server restatedev/tap/restate
```
Start the server:
```shell theme={null}
restate-server
```
Download prebuilt binaries from the [releases page](https://github.com/restatedev/restate/releases/latest):
```shell MacOS-x64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=x86_64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell MacOS-arm64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=aarch64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell Linux-x64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=x86_64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
```shell Linux-arm64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=aarch64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
Start the server:
```shell theme={null}
restate-server
```
```shell theme={null}
npm install --global @restatedev/restate-server@latest @restatedev/restate@latest
```
Start the server:
```shell theme={null}
restate-server
```
Run the Restate Server:
```shell theme={null}
docker run --name restate_dev --rm \
-p 8080:8080 -p 9070:9070 -p 9071:9071 \
--add-host=host.docker.internal:host-gateway \
docker.restate.dev/restatedev/restate:latest
```
Run CLI commands:
```shell theme={null}
docker run -it --network=host \
docker.restate.dev/restatedev/restate-cli:latest \
invocations ls
```
Replace `invocations ls` with any CLI subcommand.
You can find the Restate UI running on port 9070 (`http://localhost:9070`) after starting the Restate Server.
```shell CLI theme={null}
restate example python-hello-world &&
cd python-hello-world
```
```shell wget theme={null}
wget https://github.com/restatedev/examples/releases/latest/download/python-hello-world.zip &&
unzip python-hello-world.zip -d python-hello-world &&
rm python-hello-world.zip && cd python-hello-world
```
Run it and let it listen on port 9080 for requests:
```shell theme={null}
uv run .
```
Tell Restate where the service is running, so Restate can discover and register the services and handlers behind this endpoint.
You can do this via the UI (`http://localhost:9070`) or via:
```shell CLI theme={null}
restate deployments register http://localhost:9080
```
```shell curl theme={null}
curl localhost:9070/deployments --json '{"uri": "http://localhost:9080"}'
```
```shell CLI theme={null}
❯ SERVICES THAT WILL BE ADDED:
- Greeter
Type: Service
HANDLER INPUT OUTPUT
greet value of content-type 'application/json' value of content-type 'application/json'
✔ Are you sure you want to apply those changes? · yes
✅ DEPLOYMENT:
SERVICE REV
Greeter 1
```
```shell curl theme={null}
{
"id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"services": [
{
"name": "Greeter",
"handlers": [
{
"name": "greet",
"ty": "Shared",
"input_description": "one of [\"none\", \"value of content-type 'application/json'\"]",
"output_description": "value of content-type 'application/json'"
}
],
"ty": "Service",
"deployment_id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"revision": 1,
"public": true,
"idempotency_retention": "1day"
}
]
}
```
If you run Restate with Docker, register `http://host.docker.internal:9080` instead of `http://localhost:9080`.
When using [Restate Cloud](https://restate.dev/cloud), your service must be accessible over the public internet so Restate can invoke it.
If you want to develop with a local service, you can expose it using our [tunnel](/deploy/server/cloud/#registering-restate-services-with-your-environment) feature.
Invoke the service via the Restate UI playground: go to `http://localhost:9070`, click on your service and then on playground.
Or invoke via `curl`:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Sarah"}'
```
Expected output: `You said hi to Sarah!`
The invocation you just sent used Durable Execution to make sure the request ran till completion.
For each request, it sent a notification, slept for a second, and then sent a reminder.
```python {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/templates/python/app/greeter.py?collapse_prequel"} theme={null}
greeter = restate.Service("Greeter")
@greeter.handler()
async def greet(ctx: restate.Context, req: GreetingRequest) -> Greeting:
# Durably execute a set of steps; resilient against failures
greeting_id = str(ctx.uuid())
await ctx.run_typed("notification", send_notification, greeting_id=greeting_id, name=req.name)
await ctx.sleep(timedelta(seconds=1))
await ctx.run_typed("reminder", send_reminder, greeting_id=greeting_id, name=req.name)
# Respond to caller
return Greeting(message=f"You said hi to {req.name}!")
```
Send a request for `Alice` to see how the service behaves when it occasionally fails to send the reminder and notification:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Alice"}'
```
Expected output:
```shell theme={null}
You said hi to Alice!
```
You can see in the service logs and in the Restate UI how the request gets retried.
On a retry, it skipped the steps that already succeeded.
Even the sleep is durable and tracked by Restate.
If you kill/restart the service halfway through, the sleep will only last for what remained.
Restate persists the progress of the handler.
Letting you write code that is resilient to failures out of the box.
Have a look at the [Durable Execution page](/foundations/key-concepts#durable-execution) to learn more.
Select your favorite runtime:
**Prerequisites**:
* [Rust](https://rustup.rs/)
Restate is a single self-contained binary. No external dependencies needed.
```shell theme={null}
brew install restatedev/tap/restate-server restatedev/tap/restate
```
Start the server:
```shell theme={null}
restate-server
```
Download prebuilt binaries from the [releases page](https://github.com/restatedev/restate/releases/latest):
```shell MacOS-x64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=x86_64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell MacOS-arm64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=aarch64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell Linux-x64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=x86_64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
```shell Linux-arm64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=aarch64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
Start the server:
```shell theme={null}
restate-server
```
```shell theme={null}
npm install --global @restatedev/restate-server@latest @restatedev/restate@latest
```
Start the server:
```shell theme={null}
restate-server
```
Run the Restate Server:
```shell theme={null}
docker run --name restate_dev --rm \
-p 8080:8080 -p 9070:9070 -p 9071:9071 \
--add-host=host.docker.internal:host-gateway \
docker.restate.dev/restatedev/restate:latest
```
Run CLI commands:
```shell theme={null}
docker run -it --network=host \
docker.restate.dev/restatedev/restate-cli:latest \
invocations ls
```
Replace `invocations ls` with any CLI subcommand.
You can find the Restate UI running on port 9070 (`http://localhost:9070`) after starting the Restate Server.
```shell CLI theme={null}
restate example rust-hello-world &&
cd rust-hello-world
```
```shell wget theme={null}
wget https://github.com/restatedev/examples/releases/latest/download/rust-hello-world.zip &&
unzip rust-hello-world.zip -d rust-hello-world &&
rm rust-hello-world.zip && cd rust-hello-world
```
```shell theme={null}
cargo run
```
Tell Restate where the service is running, so Restate can discover and register the services and handlers behind this endpoint.
You can do this via the UI (`http://localhost:9070`) or via:
```shell CLI theme={null}
restate deployments register http://localhost:9080
```
```shell curl theme={null}
curl localhost:9070/deployments --json '{"uri": "http://localhost:9080"}'
```
```shell CLI theme={null}
❯ SERVICES THAT WILL BE ADDED:
- Greeter
Type: Service
HANDLER INPUT OUTPUT
greet value of content-type 'application/json' value of content-type 'application/json'
✔ Are you sure you want to apply those changes? · yes
✅ DEPLOYMENT:
SERVICE REV
Greeter 1
```
```shell curl theme={null}
{
"id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"services": [
{
"name": "Greeter",
"handlers": [
{
"name": "greet",
"ty": "Shared",
"input_description": "one of [\"none\", \"value of content-type 'application/json'\"]",
"output_description": "value of content-type 'application/json'"
}
],
"ty": "Service",
"deployment_id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"revision": 1,
"public": true,
"idempotency_retention": "1day"
}
]
}
```
If you run Restate with Docker, register `http://host.docker.internal:9080` instead of `http://localhost:9080`.
When using [Restate Cloud](https://restate.dev/cloud), your service must be accessible over the public internet so Restate can invoke it.
If you want to develop with a local service, you can expose it using our [tunnel](/deploy/server/cloud/#registering-restate-services-with-your-environment) feature.
Invoke the service via the Restate UI playground: go to `http://localhost:9070`, click on your service and then on playground.
Or invoke via `curl`:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '"Sarah"'
```
Expected output:
```log theme={null}
You said hi to Sarah!
```
The invocation you just sent used Durable Execution to make sure the request ran till completion.
For each request, it sent a notification, slept for a second, and then sent a reminder.
```rust {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/rust/templates/rust/src/main.rs?collapse_prequel"} theme={null}
#[restate_sdk::service]
trait Greeter {
async fn greet(name: String) -> Result;
}
struct GreeterImpl;
impl Greeter for GreeterImpl {
async fn greet(&self, mut ctx: Context<'_>, name: String) -> Result {
// Durably execute a set of steps; resilient against failures
let greeting_id = ctx.rand_uuid().to_string();
ctx.run(|| send_notification(&greeting_id, &name))
.name("notification")
.await?;
ctx.sleep(Duration::from_secs(1)).await?;
ctx.run(|| send_reminder(&greeting_id, &name))
.name("reminder")
.await?;
// Respond to caller
Ok(format!("You said hi to {name}"))
}
}
#[tokio::main]
async fn main() {
// To enable logging
tracing_subscriber::fmt::init();
HttpServer::new(Endpoint::builder().bind(GreeterImpl.serve()).build())
.listen_and_serve("0.0.0.0:9080".parse().unwrap())
.await;
}
```
Send a request for `Alice` to see how the service behaves when it occasionally fails to send the reminder and notification:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '"Alice"'
```
Example output:
```shell theme={null}
You said hi to Alice!
```
You can see in the service logs and in the Restate UI how the request gets retried.
On a retry, it skipped the steps that already succeeded.
Even the sleep is durable and tracked by Restate.
If you kill/restart the service halfway through, the sleep will only last for what remained.
Restate persists the progress of the handler.
Letting you write code that is resilient to failures out of the box.
Have a look at the [Durable Execution page](/foundations/key-concepts#durable-execution) to learn more.
**Prerequisites**:
* [Rust](https://rustup.rs/)
* [Shuttle](https://docs.shuttle.dev/getting-started/installation)
Restate is a single self-contained binary. No external dependencies needed.
```shell theme={null}
brew install restatedev/tap/restate-server restatedev/tap/restate
```
Start the server:
```shell theme={null}
restate-server
```
Download prebuilt binaries from the [releases page](https://github.com/restatedev/restate/releases/latest):
```shell MacOS-x64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=x86_64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell MacOS-arm64 theme={null}
BIN=/usr/local/bin && RESTATE_PLATFORM=aarch64-apple-darwin && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
sudo mv restate $BIN && \
sudo mv restate-server $BIN
```
```shell Linux-x64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=x86_64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
```shell Linux-arm64 theme={null}
BIN=$HOME/.local/bin && RESTATE_PLATFORM=aarch64-unknown-linux-musl && \
curl -L --remote-name-all https://restate.gateway.scarf.sh/latest/restate-{server,cli}-$RESTATE_PLATFORM.tar.xz && \
tar -xvf restate-server-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-server-$RESTATE_PLATFORM/restate-server && \
tar -xvf restate-cli-$RESTATE_PLATFORM.tar.xz --strip-components=1 restate-cli-$RESTATE_PLATFORM/restate && \
chmod +x restate restate-server && \
mv restate $BIN && \
mv restate-server $BIN
```
Start the server:
```shell theme={null}
restate-server
```
```shell theme={null}
npm install --global @restatedev/restate-server@latest @restatedev/restate@latest
```
Start the server:
```shell theme={null}
restate-server
```
Run the Restate Server:
```shell theme={null}
docker run --name restate_dev --rm \
-p 8080:8080 -p 9070:9070 -p 9071:9071 \
--add-host=host.docker.internal:host-gateway \
docker.restate.dev/restatedev/restate:latest
```
Run CLI commands:
```shell theme={null}
docker run -it --network=host \
docker.restate.dev/restatedev/restate-cli:latest \
invocations ls
```
Replace `invocations ls` with any CLI subcommand.
You can find the Restate UI running on port 9070 (`http://localhost:9070`) after starting the Restate Server.
```shell CLI theme={null}
restate example rust-hello-world-shuttle &&
cd rust-hello-world-shuttle
```
```shell wget theme={null}
wget https://github.com/restatedev/examples/releases/latest/download/rust-hello-world-shuttle.zip &&
unzip rust-hello-world-shuttle.zip -d rust-hello-world-shuttle &&
rm rust-hello-world-shuttle.zip && cd rust-hello-world-shuttle
```
```shell theme={null}
cargo shuttle run --port 9080
```
Tell Restate where the service is running, so Restate can discover and register the services and handlers behind this endpoint.
You can do this via the UI (`http://localhost:9070`) or via:
```shell CLI theme={null}
restate deployments register http://localhost:9080 --use-http1.1
```
```shell curl theme={null}
curl localhost:9070/deployments --json '{"uri": "http://localhost:9080", "use_http_11": true}'
```
Expected output:
```shell CLI theme={null}
❯ SERVICES THAT WILL BE ADDED:
- Greeter
Type: Service
HANDLER INPUT OUTPUT
greet value of content-type 'application/json' value of content-type 'application/json'
✔ Are you sure you want to apply those changes? · yes
✅ DEPLOYMENT:
SERVICE REV
Greeter 1
```
```shell curl theme={null}
{
"id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"services": [
{
"name": "Greeter",
"handlers": [
{
"name": "greet",
"ty": "Shared",
"input_description": "one of [\"none\", \"value of content-type 'application/json'\"]",
"output_description": "value of content-type 'application/json'"
}
],
"ty": "Service",
"deployment_id": "dp_17sztQp4gnEC1L0OCFM9aEh",
"revision": 1,
"public": true,
"idempotency_retention": "1day"
}
]
}
```
{runtime_1} does not support HTTP2, so we need to tell Restate to use HTTP1.1.
If you run Restate with Docker, use `http://host.docker.internal:9080` instead of `http://localhost:9080`.
When using [Restate Cloud](https://restate.dev/cloud), your service must be accessible over the public internet so Restate can invoke it.
If you want to develop with a local service, you can expose it using our [tunnel](/deploy/server/cloud/#registering-restate-services-with-your-environment) feature.
Invoke the service via the Restate UI playground: go to `http://localhost:9070`, click on your service and then on playground.
Or invoke via `curl`:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '{"name": "Sarah"}'
```
Expected output: `You said hi to Sarah!`
The invocation you just sent used Durable Execution to make sure the request ran till completion.
For each request, it sent a notification, slept for a second, and then sent a reminder.
```rust {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/rust/templates/rust-shuttle/src/main.rs?collapse_prequel"} theme={null}
#[restate_sdk::service]
trait Greeter {
async fn greet(name: String) -> Result;
}
struct GreeterImpl;
impl Greeter for GreeterImpl {
async fn greet(&self, mut ctx: Context<'_>, name: String) -> Result {
// Durably execute a set of steps; resilient against failures
let greeting_id = ctx.rand_uuid().to_string();
ctx.run(|| send_notification(&greeting_id, &name))
.name("notification")
.await?;
ctx.sleep(Duration::from_secs(1)).await?;
ctx.run(|| send_reminder(&greeting_id, &name))
.name("reminder")
.await?;
// Respond to caller
Ok(format!("You said hi to {name}"))
}
}
#[shuttle_runtime::main]
async fn main() -> Result {
Ok(RestateShuttleEndpoint::new(
Endpoint::builder().bind(GreeterImpl.serve()).build(),
))
}
```
Send a request for `Alice` to see how the service behaves when it occasionally fails to send the reminder and notification:
```shell theme={null}
curl localhost:8080/Greeter/greet --json '"Alice"'
```
Example output:
```shell theme={null}
You said hi to Alice!
```
You can see in the service logs and in the Restate UI how the request gets retried.
On a retry, it skipped the steps that already succeeded.
Even the sleep is durable and tracked by Restate.
If you kill/restart the service halfway through, the sleep will only last for what remained.
Restate persists the progress of the handler.
Letting you write code that is resilient to failures out of the box.
Have a look at the [Durable Execution page](/foundations/key-concepts#durable-execution) to learn more.
# Restate Architecture
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/references/architecture
Restate architecture and distributed deployment concepts
Restate is designed to be extremely simple to get started with by delivering all the functionality in a single binary with minimal upfront configuration needs. In particular, when starting out by running Restate on a single node, you don't need to understand its internal architecture in a great level of detail.
As you begin to plan for more complex deployment scenarios, you will benefit from having a deeper understanding of the various components and how they fit together to support scalable and resilient clusters. The goal of this section is to introduce the terminology we use throughout the server documentation and inform the choices involved in configuring Restate clusters.
## Overview
This section explains how Restate is built today: a partitioned, log-centric runtime where events are synchronously replicated between Restate server nodes, and partition-processor state is periodically snapshotted to S3 for bounded, fast recovery. The description focuses on the current implementation and omits optional storage trade-offs.
## Components
At a high level, Restate interposes a log-first runtime between clients and your handlers, with clear separation of ingress, durability, execution, and control.
### Ingress
Ingress is the front door for client and internal calls, identifies the target service/handler and partition (via workflow ID, virtual-object key, or idempotency key), and forwards the call to the current leader of that partition. Ingress is leader-aware; when leadership changes due to failover or rebalancing, routing updates automatically without client involvement.
### Durable Log (“Bifrost”)
The log is the primary durability layer. Each partition has a single sequencer/leader that orders events and replicates them to peer replicas on other Restate nodes. A write is committed when a quorum of replicas acknowledges the append. Restate uses a segmented virtual log: the active segment receives appends; reconfiguration seals the active segment and atomically publishes a new segment as the head. Segmentation enables clean and fast leadership changes, placement updates, and other reconfiguration without copying data.
### Partition Processor
Every partition has one processor leader (and optional followers). The processor tails the log, invokes your handler code via a bidirectional stream, and maintains a materialized state cache in an embedded RocksDB. This cache holds journals, idempotency metadata, key-scoped state for virtual objects, and timer indices—everything required for low-latency execution. Followers track the same state and can become leader quickly on failure.
### Control Plane
The control plane holds cluster metadata (configurations, partition placement, epochs, segment descriptors) behind a consensus interface (built-in Raft). A cluster controller manages log nodes and processors and initiates failover when health checks fail. Strong consensus is confined to this metadata layer, and the data path “borrows” consensus in the form of a leader/epoch configuration which the control plane revokes and re-assignes upon failover, rebalancing, and other reconfigurations.
## Durability and storage model
The system treats the replicated log as ground truth and uses S3-backed snapshots to bound recovery time without introducing a second source of truth.
### Hot-path durability
An operation “happens” when the partition leader appends its record to the log and receives quorum acks. That commit point defines the durable order of invocations, steps, state updates, messages, timers, and completions.
### Materializing state for fast access
The processor leader maintains a full cache of the partition’s materialized state in RocksDB for fast random reads and updates during execution. This cache is derivative—it can always be rebuilt from the log—and is not a second source of truth. In typical configurations, partitions also have processor followers, which maintain a materialization of the partition state, for fast failover.
### Recovery bounded by snapshots
Processors create periodic snapshots of RocksDB and upload them to S3 . On restart or takeover, a fresh partition processor can download the latest snapshot and replays the log suffix since the snapshot’s sequence number. After a snapshot is durably stored on S3, the log can trim entries up to the snapshot point to cap local storage and replay cost.
Partition processors pull the snapshots only when they were not previously leader or follower and need to bootstrap a new copy of the partition state.
## Partitioned scale-out and addressability
Restate scales by sharding orchestration and state by key, keeping hot paths partition-local and cross-partition work explicit.
### Keyed routing
Workflow IDs, virtual-object keys, or idempotency keys deterministically hash to a partition. Non-keyed invocations are placed for locality by the ingress. The invocation ID encodes the partition, enabling efficient subsequent routing and lookups.
### Co-sharding of orchestration and state
Each partition owns both orchestration (invocation lifecycle, journaling, timers) and the state cache for its keys. Steps, state updates, and timers for a key execute entirely within one partition—no cross-partition coordination is needed for the hot path.
### Cross-partition actions
When a handler targets a different key (e.g., sends a message or performs an RPC to another service keyed on a different partition), the event is recorded in the origin partition’s log and delivered exactly once to the destination partition via an internal shuffler. Delivery is addressed and deduplicated by sequence numbers; the receiving partition treats it as a normal log-first operation.
### Elastic operations
While the number of partitions is configured at cluster creation today, the addressing scheme and segment abstraction are designed so the system can migrate partitions or split key ranges in future versions without violating ordering or idempotency guarantees.
## Write path and step lifecycle
To make the mechanics concrete, the flow below traces an end-to-end invocation of processPayment keyed by idempotency key K.
A client invokes processPayment with key K. The ingress hashes K to select the target partition and enqueues the invocation to that partition’s log.
The partition’s processor leader consumes the enqueue event, checks its local idempotency state for K, and sees that K is not present for processPayment. It atomically records K (idempotency) and transitions the invocation to RUNNING, then opens a bidirectional stream to the target service endpoint and pushes the initial invoke journal entry to the handler.
As the service executes, it streams back a step result event (e.g., from `ctx.run`). The processor appends this step journal entry to the log. The moment this append is replicated to quorum defines “the step happened.” From then on, the step will be recovered on retries and won’t be re-executed.
When the processor subsequently reads the committed step event back from the log (confirming it still holds leadership), it adds the entry to the invocation’s journal state (and any relevant indexes) and acks the handler so the user code can proceed with the next step.
The same pattern applies to state updates, timers, inter-service RPC/messages, or durable promises/futures: each action is added to the log first, and upon reading the committed record, the processor applies it (e.g., updates key-scoped state, registers/fires a timer, routes an RPC to another partition via the internal shuffler, or resolves a promise).
When the handler finishes, the processor appends a Result event to the log. After reading that event back, it marks the invocation COMPLETE and returns the result to the client (or emits the completion signal for async flows).
If execution fails at any point (process crash, stream loss, user exception), the processor dispatches a new attempt and attaches the full journal so far. Attempts carry monotonically increasing epochs; the processor rejects any events from superseded epochs (late messages from older attempts), preventing split-brain effects. This bookkeeping is partition-local and efficient because invocations are sticky to a single partition with a strong leader.
Note: Duplicate client calls with the same idempotency key K resolve to the same invocation: the processor returns the already-committed result from the journal without re-executing steps.
## Failover and reconfiguration
Leader loss or placement changes trigger a sealed-segment handover and epoch fencing that keep ordering and exactly-once properties intact.
### Detection and triggering
The cluster controller heartbeats sequencers and processors. Partition processors and log servers additionally gossip their detection of peer failures for faster failover handling.
### Log failover
On reconfiguration, the controller seals the active segment (a quorum of replicas refuses further appends), determines the authoritative tail of the log segment, elects a new sequencer/replica set, and performs a metadata CAS to publish the new segment as head. Appenders and readers consult metadata and continue on the new head.
### Processor failover
A follower (or a restarted processor) is promoted, obtains a new epoch, appends an epoch-bump record. The new leader ensures it can resume the log’s event stream (possibly restoring the latest S3 snapshot, if it was not previously a follower). Any late appends from superseded leaders or stale handler attempts (carrying lower epochs) are fenced at the epoch boundary and ignored.
### Routing continuity
Ingress consults the updated control-plane metadata and routes directly to the new leaders. Clients do not need to reconnect to different endpoints or re-negotiate sessions; failover is transparent at the API boundary.
## Nodes and roles
You'll see many mentions of the terms server and node throughout this documentation. Generally, we use the term "server" to refer to a running instance of the `restate-server` binary. This binary can host multiple functions. When you start a single-node Restate server, for example when doing some local development or testing, you are hosting all the essential features in a single process. These include accepting incoming requests, durably recording events, processing work (delegating invocations to services, handling key-value operations), as well as maintaining metadata used internally by the system.
At its simplest, running a cluster is not that different - multiple nodes cooperate to share the responsibilities we mentioned earlier. This is accomplished by having multiple copies of the server process running on separate machines, although it is possible to create test clusters on a single machine. **Nodes** are therefore distinct instances of the Restate server within a cluster.
Restate clusters are designed to scale out in support of large deployments. As you add more machines, it becomes wasteful to replicate all the functionality across all the machines in a cluster, since not all features need to scale out at the same rate. **Roles** control which features run on any given node, enabling specialization within the cluster.
Here is an overview of the different roles that can run on a node:
* Metadata server: the source of truth for cluster-wide information
* Ingress: the entry point for external requests
* Log server: responsible for durably persisting the log
* Worker: houses the partition processors
### Metadata store
The Restate metadata store is part of the control plane and is the internal source of truth for node membership and responsibilities. It is essential to the correctness of the overall system: In a cluster this service enables distributed consensus about other components' configuration. All nodes in a Restate cluster must be able to access the metadata store, though not all members of the cluster need to be part of hosting it. Restate includes a built-in Raft-based metadata store which is hosted on all nodes running the `metadata-server` role.
The metadata store is designed to support relatively low volumes of read and write operations (at least compared to other parts of Restate), with the highest level of integrity and availability.
### Ingress
External requests enter the Restate cluster via the HTTP ingress component, which runs on nodes assigned the `http-ingress` role. Compared to other roles, the HTTP ingress role does not involve long-lived state and it can move around relatively freely, since it only handles ongoing client connections.
### Log servers
Log server nodes running the `log-server` role are responsible for durably persisting the log. If the log is the equivalent of a WAL, then partition stores are the materializations that enable efficient reads of the events (invocation journals, key-value data) that have been recorded. Depending on the configured **log replication** requirements, Restate will replicate log records to multiple log servers to persist a given log, and this will change over time to support maintenance and resizing of the cluster.
### Workers
Nodes assigned the `worker` role run the partition processors, which are the Restate components responsible for maintaining the partition store.
Partition processors can operate in either leader or follower mode.
Only a single leader for a given partition can be active at a time, and this is the sole processor that handles invocations to deployed services.
Followers keep up with the log without taking action, and are ready to take over in the event that the partition's leader becomes unavailable.
The overall number of processors per partition is configurable via the **partition replication** configuration option.
Partition processors replicate their state by following and applying the log for their partition.
If a processor needs to stop, for example for scheduled maintenance, it will typically catch up on the records it missed by reading them from the cluster's log servers once it comes back online.
Occasionally, a worker node might lose a disk - or you might need to grow your cluster by adding fresh nodes to it.
In these cases, it's far more efficient to obtain a **snapshot** of the partition state from a recent point in time than to replay all the missing log events.
Restate clusters can be configured to use an external **object store** as the snapshot repository, allowing partition processors to skip ahead in the log.
This also enables us to **trim logs** which might otherwise grow unboundedly.
## Other reading material
* [Blog post: Building a modern Durable Execution Engine from First Principles](https://restate.dev/blog/building-a-modern-durable-execution-engine-from-first-principles/)
* [Blog post: Distributed Restate - a first look](https://restate.dev/blog/distributed-restate-a-first-look/)
* [Blog post: Every System is a Log](https://restate.dev/blog/every-system-is-a-log-avoiding-coordination-in-distributed-applications/)
# CLI Configuration
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/references/cli-config
Restate CLI configuration options.
You can use the CLI to interact with Restate, and manage your services, deployments and invocations.
Have a look at the [CLI installation docs](/installation).
There are 2 ways to configure the CLI: via environment variables or via a configuration file.
## Using environment variables
You can specify the following environment variables:
* `RESTATE_HOST`: The hostname/IP address of the server. Default is `localhost`.
* `RESTATE_HOST_SCHEME`: Default is `http`.
* `RESTATE_ADMIN_URL`: To specify the full URL of the admin server (scheme+host+port).
* `RESTATE_AUTH_TOKEN`: Set if authentication is required.
For example, to specify the hostname `myhost` and the host scheme `https`, pass environment variables as follows:
```shell theme={null}
RESTATE_HOST=myhost RESTATE_HOST_SCHEME=https restate
```
You can find the full list of configuration variables in the [CLI GitHub repo](https://github.com/restatedev/restate/blob/main/cli/src/cli_env.rs).
You can also specify the configuration in a `.env` file. The CLI will look for a `.env` file in its current directory.
For example, to connect to a Restate admin server running at `http://myhost:9070`, save the following in a `.env` file:
```shell .env theme={null}
RESTATE_ADMIN_URL=http://myhost:9070
```
## Using the config file
By default, the CLI will treat `$HOME/.config/restate` as its config directory.
This is configurable with `$RESTATE_CLI_CONFIG_HOME`. Restate will look for file
named `config.toml` inside this directory. You can edit this file with
`restate config edit`, or view its contents with `restate config view`.
The config file has a native concept of 'enviroments' which are sets of
configuration intended to specify different instances of Restate. You can
list your configured environments:
```bash theme={null}
> restate config list-environments
CURRENT NAME ADMIN_BASE_URL
* local http://localhost:9070/
```
By default, the CLI uses the `local` environment which is configured to point
at a Restate instance running on your local machine. Additional environments
can be specified as new blocks in `config.toml`:
```toml config.toml theme={null}
[myhost]
ingress_base_url = "http://myhost:8080"
admin_base_url = "http://myhost:9070"
bearer_token = "..."
```
The title of the block is the name of the environment. You can switch
between environments in various ways:
1. With an argument: `restate -e myhost whoami`
2. With an environment variable: `RESTATE_ENVIRONMENT=myhost restate whoami`
3. With the command `restate config use-environment myhost`. This will write
the name of the environment to the CLI config directory, so that it's used by
default for all CLI commands. You can switch back to `local` with
`restate config use-environment local`.
# Error Codes
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/references/errors
Descriptions of error codes emitted by Restate components
This page contains the list of error codes emitted by Restate components.
## META0003
Cannot reach the service endpoint to execute discovery. Make sure:
* The provided `URI`/`ARN` is correct
* The deployment is up and running
* Restate can reach the deployment through the configured `URI`/`ARN`
* If additional authentication is required, make sure it's configured through `additional_headers`
## META0004
Cannot register the provided deployment, because it conflicts with the uri of an already registered deployment.
In Restate deployments have a unique uri/arn and are immutable, thus it's not possible to discover the same deployment twice.
Make sure, when updating a deployment, to assign it a new uri/arn.
You can force the override using the `"force": true` field in the discover request, but beware that this can lead in-flight invocations to an unrecoverable error state.
See the [versioning documentation](https://docs.restate.dev/operate/versioning) for more information.
## META0005
Cannot propagate deployment/service metadata to Restate services. If you see this error when starting Restate, this might indicate a corrupted Meta storage.
We recommend wiping the Meta storage and recreating it by registering deployments in the same order they were registered before.
## META0006
Cannot register the newly discovered service revision in the provided deployment, because it conflicts with an already existing service revision.
When implementing a new service revision, make sure that:
* The service type is the same as the previous revision.
* The new revision contains at least all the handlers of the previous revision.
See the [versioning documentation](https://docs.restate.dev/operate/versioning) for more information.
## META0009
The provided subscription is invalid. Subscriptions should have:
* A `source` field in the format of `kafka:///`. When registering, the Kafka cluster should be configured in the Restate configuration.
* A `sink` field in the format of `service:///`. When registering, service and handler should be available already in the registry, meaning they have been previously registered.
* Additional constraints may apply depending on the sink service type.
Please look at the Kafka documentation (for [TypeScript](https://docs.restate.dev/develop/ts/kafka) and [Java](https://docs.restate.dev/develop/java/kafka)) for more details on subscriptions and event handlers.
## META0010
Trying to open meta storage directory, configured via `meta.storage_path`, which contains incompatible data. This indicates that your data was written with a different Restate version than you are running right now.
Suggestions:
* Up/Downgrade your Restate server to the requested version.
* Migrate your data to the requested version by running the migration scripts.
* Wipe your meta storage directory to start afresh via `rm -rf //local-metadata-store`.
* Configure a different meta storage directory via `meta.storage_path`.
## META0011
Non-empty meta storage directory, configured via `meta.storage_path`, is missing the version file. This indicates data corruption or that the data has been written with an incompatible Restate version \< 0.8.
Suggestions:
* Wipe your meta storage directory to start afresh via `rm -rf //local-metadata-store`.
* Configure a different meta storage directory via `meta.storage_path`.
* Downgrade your Restate server to {'<='} 0.7.
## META0012
Trying to register a service endpoint whose supported service protocol versions is incompatible with the server. This indicates that you have to upgrade your server to make it work together with the deployed SDK.
Suggestions:
* Check the compatibility matrix between SDK and server versions
* Try upgrading to a server version which is compatible with your SDK
* Try using an SDK version which is compatible with your server
## META0013
Received a bad service discovery response from the specified service endpoint. This indicates that you are trying to register a service endpoint with an incompatible server.
Suggestions:
* Check the compatibility matrix between SDK and server versions
* Either deploy a server version which is compatible with your SDK
* Or use an SDK version which is compatible with your server
## META0014
Service discovery response failed, and the server may have responded in HTTP1.1.
This can happen when discovering locally running dev servers from Faas platforms
eg `wrangler dev`. FaaS platforms in generally will support HTTP2, however, so
this is only a local development concern.
You can try to discover the endpoint with `--use-http1.1` when working
with these local dev servers. This should not be needed in production.
## META0015
The service discovery response suggested that the SDK is serving in
bidirectional protocol mode, but discovery is going over a protocol that does
not support it (currently only Lambda).
Lambda endpoints do not support the bidirectional protocol mode and should be
configured to announce themselves as being in request-response mode upon
discovery.
## META0016
Cannot update the provided deployment with the discovered metadata, because the new metadata is insufficiently similar to the old.
When updating a deployment, make sure that:
* All services have the same type as they did in the previous deployment.
* All services contain at least all the handlers that they did in the previous deployment.
* The updated deployment contains at least all the services that it previously did.
* The updated deployment has exactly the same supported protocol versions, which generally means you want to use the same SDK minor version.
See the [versioning documentation](https://docs.restate.dev/operate/versioning) for more information.
## META0017
Cannot force-add the provided URI/ARN as a new deployment, because two or more existing deployments use this URI.
Generally Restate enforces that there is only one deployment for a given destination (a HTTP URI or Lambda ARN). This means
that redicovering the same destination requires a `force` flag. Adding a deployment in force mode instructs Restate to replace the existing deployment
with that destination, with the discovered metadata. This relies on there being an unambiguous deployment to replace.
When using the `PUT /deployments/{deployment_id}` API to update a deployment in place, it is possible to create two deployments that have the same destination.
This is intended to be a temporary measure to fix failing invocations on a draining deployment. While in this state, it is not possible to force deploy that same destination.
Instead, one of the two deployments must be deleted (`DELETE /deployments/{deployment_id}`) so that there is an unambiguous deployment to replace.
See the [versioning documentation](https://docs.restate.dev/operate/versioning) for more information.
## RT0001
The invocation response stream was aborted due to the timeout configured in `worker.invoker.abort_timeout`.
This timeout is fired when Restate has an open invocation, and it's waiting only for response messages, but no message is seen for the configured time.
Suggestions:
* Check for bugs in your code. Most likely no message was sent to Restate because your code is blocked and/or reached a deadlock.
* If your code is supposed to not send any message to Restate for longer than the configured timeout, because for example is doing a blocking operation that takes a long time, change the configuration accordingly.
## RT0002
Cannot start Restate because the configuration cannot be parsed. Check the configuration file and the environment variables provided.
For a complete list of configuration options, and a sample configuration, check [https://docs.restate.dev/operate/configuration](https://docs.restate.dev/operate/configuration)
## RT0003
The invocation failed because Restate received a message from a service larger than the `worker.invoker.message_size_limit`.
Suggestions:
* Check in your code whether there is a case where a very large message can be generated, such as a state entry being too large, a request payload being too large, etc.
* Increase the limit by tuning the `worker.invoker.message_size_limit` config entry, eventually tuning the memory of your operating system/machine where Restate is running.
## RT0004
Failed starting process because it could not bind to configured address.
This happens usually if another process has already bound to this address.
Suggestions:
* Select an address that is free.
* Stop the process that has bound to the specified address.
* Make sure you have the permissions to bind to the configured port. Some operating systems require admin/root privileges to bind to ports lower than 1024.
## RT0005
Failed opening RocksDB, because the db file is currently locked.
This happens usually if another process still holds the lock.
Suggestions:
* Check no other Restate process is running and using the same db file.
* Configure a different RocksDB storage directory via `worker.storage_rocksdb.path`.
## RT0006
A generic error occurred while invoking the service.
We suggest checking the service/deployment logs as well to get any hint on the error cause.
## RT0007
A retry-able error was received from the handler while processing the invocation.
Restate will soon retry executing the invocation, replaying from the point where it left.
Suggestions:
* Check the service logs to get more info about the error cause, like the stacktrace.
* Look at the error handling docs for more info about error handling in services (e.g. [https://docs.restate.dev/develop/ts/error-handling](https://docs.restate.dev/develop/ts/error-handling) or [https://docs.restate.dev/develop/java/error-handling](https://docs.restate.dev/develop/java/error-handling)).
## RT0009
Trying to open worker storage directory, configured via `worker.storage_rocksdb.path`, which contains no storage format version information. This indicates data corruption or that the data has been written with an incompatible Restate version \< 0.8.
Suggestions:
* Wipe your meta storage directory to start afresh via `rm -rf //db`.
* Configure a different worker storage directory via `worker.storage_rocksdb.path`.
* Downgrade your Restate server to \< 0.8.
## RT0010
Network error when interacting with the service endpoint.
This can be caused by a variety of reasons including:
* The service is (temporarily) down
* The service is (temporarily) not reachable over the network
* Your network security setup blocks Restate from reaching the service
* A config error where the registered service endpoint and the actually deployed service endpoint differ
## RT0011
No deployment found for the given service.
This might indicate that the service and/or the associated deployment was removed from the schema registry before starting to process the invocation.
Check whether the deployment still exists using `restate deployments list` or by looking in the UI.
## RT0012
Protocol violation error.
This can be caused by an incompatible runtime and SDK version, or by an SDK bug.
If the error persists, please file a bug report here: [https://github.com/restatedev/restate/issues](https://github.com/restatedev/restate/issues).
## RT0013
The service endpoint does not support any of the supported service protocol versions of the server.
Please make sure that the service endpoint's SDK and the Restate server are compatible.
Suggestions:
* Register a service endpoint which uses an SDK which is compatible with the used server
* Upgrade the server to a version which is compatible with the used SDK
## RT0014
The server cannot resume an in-flight invocation which has been started with a now incompatible service protocol version.
Restate does not support upgrading service protocols yet.
Suggestions:
* Downgrade the server to a version which is compatible with the used service protocol version
* Kill the affected invocation via the CLI.
## RT0015
The server can't establish an invocation stream because the SDK does not support the service protocol version negotiated during discovery.
This indicates that the SDK was updated to a new version that dropped support for old service protocol versions, but no re-registration was performed.
Suggestions:
* For in-flight invocations, downgrade the SDK version back to the previous version.
* For new invocations, register a new deployment with a new endpoint as described here: [https://docs.restate.dev/operate/versioning#deploying-new-service-versions](https://docs.restate.dev/operate/versioning#deploying-new-service-versions).
* Make sure the new SDK is compatible with this runtime version, for more info check out [https://docs.restate.dev/operate/upgrading#service-compatibility](https://docs.restate.dev/operate/upgrading#service-compatibility).
## RT0016
Journal mismatch detected when replaying the invocation: the handler generated a sequence of journal entries (thus context operations) that doesn't exactly match the recorded journal.
This indicates that either the service code was changed (e.g. the service container image updated) without registering a new version of the service deployment, or some code within the handler is non-deterministic.
To fix it:
* Update the deployment in place, fixing the bug, as described in [https://docs.restate.dev/operate/versioning/#updating-deployments-in-place](https://docs.restate.dev/operate/versioning/#updating-deployments-in-place)
* If you can afford to lose partial progress, then kill the invocation as described in [https://docs.restate.dev/operate/invocation/#killing-invocations](https://docs.restate.dev/operate/invocation/#killing-invocations)
Some common mistakes that lead to non-deterministic errors are:
* Branch the execution flow based on some non-deterministic information, such as the elapsed time between now and another timestamp, or the result of an HTTP request that was not recorded using the `ctx.run` feature.
* A parameter passed to a `Context` operation is non-deterministic, for example setting a state key using a random value or the current date-time.
* Execute a sequence of `Context` operations, such as calling other services, while iterating over a data structure with non-deterministic iteration order (such as sets/maps/dictionaries).
For more info about determinism and journaling of non-deterministic operations, check out [https://docs.restate.dev/get\_started/tour/#journaling-actions](https://docs.restate.dev/get_started/tour/#journaling-actions).
## RT0017
The entry cannot be processed due to a failed precondition.
This entry, and all the subsequent received entries, have been discarded and Restate will retry executing the invocation from the last recorded entry.
Entry preconditions are usually checked by the SDK.
If the error persists, please file a bug report here: [https://github.com/restatedev/restate/issues](https://github.com/restatedev/restate/issues).
## RT0018
The request submitted through the `Context` API to the given service handler cannot be processed, because the service handler doesn't exist.
Make sure the service/handler is registered, by using `restate svc ls` or through the UI:
* If the service/handler is correctly registered, you can ignore this error as it's a transient error, due to internal propagation of the cluster metadata.
* If the service/handler is not registered, you must register it in order for this invocation to progress.
## RT0019
The service replied with Content Too Large/Payload Too Large.
If you deploy on a serverless platform like Vercel, this might indicate that you're hitting the payload size limit imposed by service providers.
Suggestions:
* Check in your code whether there is a case where a very large message can be generated, such as a state being too large, a `ctx.run` result being too large, etc.
* Ask the service provider to increase the limits, if possible.
# Go API
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/references/gopkg
# Javadocs
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/references/javadocs
# KotlinDocs
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/references/ktdocs
# Restate Server Configuration
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/references/server-config
Reference of the configuration options for Restate Server.
## Default configuration
The following is the default configuration. It does not include all possible configuration options, since some can be conflicting. Take a look at the configuration reference below for a full list of options.
Note that configuration defaults might change across server releases, if you want to make sure you use stable values, use an explicit configuration file an pass the path via `--config-path=` as described above.
```toml restate.toml expandable {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/docs-restate/refs/heads/main/docs/schemas/restate.toml"} theme={null}
roles = [
"worker",
"admin",
"metadata-server",
"log-server",
"http-ingress",
]
cluster-name = "localcluster"
auto-provision = true
advertised-address = "http://127.0.0.1:5122/"
default-num-partitions = 24
default-replication = 1
shutdown-timeout = "1m"
tracing-filter = "info"
log-filter = "warn,restate=info"
log-format = "pretty"
log-disable-ansi-codes = false
tokio-console-bind-address = "0.0.0.0:6669"
no-proxy = []
connect-timeout = "10s"
request-compression-threshold = "4.0 MiB"
disable-prometheus = false
rocksdb-total-memory-size = "6.0 GiB"
rocksdb-total-memtables-ratio = 0.5
rocksdb-high-priority-bg-threads = 2
rocksdb-write-stall-threshold = "3s"
rocksdb-enable-stall-on-memory-limit = false
rocksdb-perf-level = "enable-count"
metadata-update-interval = "10s"
metadata-fetch-from-peer-timeout = "3s"
initialization-timeout = "5m"
disable-telemetry = false
gossip-tick-interval = "100ms"
gossip-failure-threshold = 10
gossip-num-peers = 2
gossip-fd-stability-threshold = 3
gossip-suspect-interval = "5s"
gossip-loneliness-threshold = 30
gossip-extras-exchange-frequency = 10
gossip-time-skew-threshold = "1s"
default-journal-retention = "1d"
[metadata-client]
type = "replicated"
addresses = ["http://127.0.0.1:5122/"]
connect-timeout = "3s"
keep-alive-interval = "5s"
keep-alive-timeout = "5s"
[metadata-client.backoff-policy]
type = "exponential"
initial-interval = "100ms"
factor = 1.399999976158142
max-attempts = 10
max-interval = "1s"
[http-keep-alive-options]
interval = "40s"
timeout = "20s"
[network-error-retry-policy]
type = "exponential"
initial-interval = "10ms"
factor = 2.0
max-attempts = 15
max-interval = "5s"
[worker]
internal-queue-length = 1000
cleanup-interval = "1h"
max-command-batch-size = 32
[worker.storage]
rocksdb-disable-wal = true
rocksdb-memory-ratio = 0.49000000953674316
[worker.invoker]
inactivity-timeout = "1m"
abort-timeout = "1m"
message-size-warning = "10.0 MiB"
in-memory-queue-length-limit = 66049
concurrent-invocations-limit = 1000
[worker.snapshots.object-store-retry-policy]
type = "exponential"
initial-interval = "100ms"
factor = 2.0
max-attempts = 10
max-interval = "10s"
[admin]
bind-address = "0.0.0.0:9070"
heartbeat-interval = "1s 500ms"
log-trim-check-interval = "1h"
disable-web-ui = false
disable-cluster-controller = false
[admin.query-engine]
memory-size = "4.0 GiB"
[ingress]
bind-address = "0.0.0.0:8080"
kafka-clusters = []
[bifrost]
default-provider = "replicated"
seal-retry-interval = "2s"
auto-recovery-interval = "15s"
append-retry-min-interval = "10ms"
append-retry-max-interval = "1s"
record-cache-memory-size = "250.0 MiB"
disable-auto-improvement = false
[bifrost.local]
rocksdb-disable-wal = false
rocksdb-memory-ratio = 0.5
rocksdb-disable-wal-fsync = false
writer-batch-commit-count = 5000
writer-batch-commit-duration = "0s"
[bifrost.replicated-loglet]
maximum-inflight-records = 1000
sequencer-inactivity-timeout = "15s"
log-server-rpc-timeout = "2s"
readahead-records = 20
read-batch-size = "32.0 KiB"
readahead-trigger-ratio = 0.5
[bifrost.replicated-loglet.sequencer-retry-policy]
type = "exponential"
initial-interval = "250ms"
factor = 2.0
max-interval = "5s"
[bifrost.replicated-loglet.log-server-retry-policy]
type = "exponential"
initial-interval = "250ms"
factor = 2.0
max-attempts = 3
max-interval = "2s"
[bifrost.read-retry-policy]
type = "exponential"
initial-interval = "50ms"
factor = 2.0
max-attempts = 50
max-interval = "1s"
[metadata-server]
request-queue-length = 32
rocksdb-memory-ratio = 0.009999999776482582
rocksdb-disable-wal = false
raft-election-tick = 10
raft-heartbeat-tick = 2
raft-tick-interval = "100ms"
status-update-interval = "5s"
log-trim-threshold = 1000
auto-join = true
[networking]
connect-timeout = "3s"
handshake-timeout = "3s"
http2-keep-alive-interval = "1s"
http2-keep-alive-timeout = "3s"
http2-adaptive-window = true
disable-compression = false
data-stream-window-size = "2.0 MiB"
[networking.connect-retry-policy]
type = "exponential"
initial-interval = "250ms"
factor = 2.0
max-attempts = 10
max-interval = "3s"
[log-server]
rocksdb-disable-wal = false
rocksdb-memory-ratio = 0.5
rocksdb-disable-wal-fsync = false
rocksdb-max-sub-compactions = 0
writer-batch-commit-count = 5000
incoming-network-queue-length = 1000
```
## Configuration Reference
Worker options
Internal queue for partition processor communication
The number of timers in memory limit is used to bound the amount of timers loaded in memory. If this limit is set, when exceeding it, the timers farther in the future will be spilled to disk.
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Storage options
How many partitions to divide memory across?
By default this uses the value defined in `default-num-partitions` in the common section of the config.
The memory budget for rocksdb memtables in bytes
The total is divided evenly across partitions. The divisor is defined in `num-partitions-to-share-memory-budget`. If this value is set, it overrides the ratio defined in `rocksdb-memory-ratio`.
Non-zero human-readable bytes
The memory budget for rocksdb memtables as ratio
This defines the total memory for rocksdb as a ratio of all memory available to memtables (See `rocksdb-total-memtables-ratio` in common). The budget is then divided evenly across partitions. The divisor is defined in `num-partitions-to-share-memory-budget`
Files will be opened in "direct I/O" mode which means that data r/w from the disk will not be cached or buffered. The hardware buffer of the devices may however still be used. Memory mapped files are not impacted by these parameters.
Use O\_DIRECT for writes in background flush and compactions.
The default depends on the different rocksdb use-cases at Restate.
Supports hot-reloading (Partial / Bifrost only)
Disable rocksdb statistics collection
Default: False (statistics enabled)
Default: the number of CPU cores on this node.
If non-zero, we perform bigger reads when doing compaction. If you're running RocksDB on spinning disks, you should set this to at least 2MB. That way RocksDB's compaction is doing sequential instead of random reads.
Non-zero human-readable bytes
StatsLevel can be used to reduce statistics overhead by skipping certain types of stats in the stats collection process.
Default: "except-detailed-timers"
Disable all metrics
Disable timer stats, and skip histogram stats
Skip timer stats
Collect all stats except time inside mutex lock AND time spent on compression.
Collect all stats except the counters requiring to get time inside the mutex lock.
Collect all stats, including measuring duration of mutex operations. If getting time is expensive on the platform to run, it can reduce scalability to more threads, especially for writes.
Verbosity of the LOG.
Default: "error"
Verbosity of the LOG.
Number of info LOG files to keep
Default: 1
Max size of info LOG file
Default: 64MB
Non-zero human-readable bytes
Uncompressed block size
Default: 64KiB
Non-zero human-readable bytes
Invoker options
This is **deprecated** and will be removed in the next Restate releases.
Please refer to `default-retry-policy` for the new configuration options.
Definition of a retry policy
No retry strategy.
Retry with a fixed delay strategy.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Number of maximum attempts before giving up. Infinite retries if unset.
Retry with an exponential strategy. The next retry is computed as `min(last_retry_interval * factor, max_interval)`.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
The factor to use to compute the next retry attempt.
Number of maximum attempts before giving up. Infinite retries if unset.
Maximum interval between retries.
Can be configured using the [`jiff::fmt::friendly`](https://docs.rs/jiff/latest/jiff/fmt/friendly/index.html) format or ISO8601, for example `5 hours`.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Non-zero human-readable bytes
Threshold to fail the invocation in case protocol messages coming from a service are larger than the specified amount.
Non-zero human-readable bytes
Temporary directory to use for the invoker temporary files. If empty, the system temporary directory will be used instead.
Defines the threshold after which queues invocations will spill to disk at the path defined in `tmp-dir`. In other words, this is the number of invocations that can be kept in memory before spilling to disk. This is a per-partition limit.
Number of concurrent invocations that can be processed by the invoker.
Configures throttling for service invocations at the node level. This throttling mechanism uses a token bucket algorithm to control the rate at which invocations can be processed, helping to prevent resource exhaustion and maintain system stability under high load.
The throttling limit is shared across all partitions running on this node, providing a global rate limit for the entire node rather than per-partition limits. When `unset`, no throttling is applied and invocations are processed without throttling.
Throttling options per invoker.
The rate at which the tokens are replenished.
Syntax: `/` where `` is `s|sec|second`, `m|min|minute`, or `h|hr|hour`. unit defaults to per second if not specified.
The maximum number of tokens the bucket can hold. Default to the rate value if not specified.
Configures rate limiting for service actions at the node level. This throttling mechanism uses a token bucket algorithm to control the rate at which actions can be processed, helping to prevent resource exhaustion and maintain system stability under high load.
The throttling limit is shared across all partitions running on this node, providing a global rate limit for the entire node rather than per-partition limits. When `unset`, no throttling is applied and actions are processed without throttling.
Throttling options per invoker.
The rate at which the tokens are replenished.
Syntax: `/` where `` is `s|sec|second`, `m|min|minute`, or `h|hr|hour`. unit defaults to per second if not specified.
The maximum number of tokens the bucket can hold. Default to the rate value if not specified.
The maximum number of commands a partition processor will apply in a batch. The larger this value is, the higher the throughput and latency are.
Partition store snapshotting settings. At a minimum, set `destination` and `snapshot-interval-num-records` to enable snapshotting. For a complete example, see [Snapshots](https://docs.restate.dev/operate/snapshots).
Base URL for cluster snapshots. Supports `s3://` and `file://` protocol scheme. S3-compatible object stores must support ETag-based conditional writes.
Default: `None`
Number of log records that trigger a snapshot to be created.
As snapshots are created asynchronously, the actual number of new records that will trigger a snapshot will vary. The counter for the subsequent snapshot begins from the LSN at which the previous snapshot export was initiated. Only leader Partition Processors will take snapshots for a given partition.
This setting does not influence explicitly requested snapshots triggered using `restatectl`.
Default: `None` - automatic snapshots are disabled
Definition of a retry policy
No retry strategy.
Retry with a fixed delay strategy.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Number of maximum attempts before giving up. Infinite retries if unset.
Retry with an exponential strategy. The next retry is computed as `min(last_retry_interval * factor, max_interval)`.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
The factor to use to compute the next retry attempt.
Number of maximum attempts before giving up. Infinite retries if unset.
Maximum interval between retries.
Can be configured using the [`jiff::fmt::friendly`](https://docs.rs/jiff/latest/jiff/fmt/friendly/index.html) format or ISO8601, for example `5 hours`.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
The AWS configuration profile to use for S3 object store destinations. If you use named profiles in your AWS configuration, you can replace all the other settings with a single profile reference. See the \[AWS documentation on profiles] ([https://docs.aws.amazon.com/sdkref/latest/guide/file-format.html](https://docs.aws.amazon.com/sdkref/latest/guide/file-format.html)) for more.
AWS region to use with S3 object store destinations. This may be inferred from the environment, for example the current region when running in EC2. Because of the request signing algorithm this must have a value. For Minio, you can generally set this to any string, such as `us-east-1`.
Username for Minio, or consult the service documentation for other S3-compatible stores.
Password for Minio, or consult the service documentation for other S3-compatible stores.
This is only needed with short-term STS session credentials.
When you use Amazon S3, this is typically inferred from the region and there is no need to set it. With other object stores, you will have to provide an appropriate HTTP(S) endpoint. If *not* using HTTPS, also set `aws-allow-http` to `true`.
Allow plain HTTP to be used with the object store endpoint. Required when the endpoint URL that isn't using HTTPS.
Admin server options
Address to bind for the Admin APIs.
Optional advertised Admin API endpoint.
Concurrency limit for the Admin APIs. Default is unlimited.
Storage query engine options
Non-zero human-readable bytes
The path to spill to
The degree of parallelism to use for query execution (Defaults to the number of available cores).
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Disable serving the Restate Web UI on the admin port. Default is `false`.
Ingress options
The address to bind for the ingress.
Local concurrency limit to use to limit the amount of concurrent requests. If exceeded, the ingress will reply immediately with an appropriate status code. Default is unlimited.
Configuration options to connect to a Kafka cluster.
Cluster name (Used to identify subscriptions).
Initial list of brokers (host or host:port).
Ingress endpoint that the Web UI should use to interact with.
Bifrost options
An enum with the list of supported loglet providers.
A local rocksdb-backed loglet.
Replicated loglets are restate's native log replication system. This requires `log-server` role to run on enough nodes in the cluster.
Configuration of local loglet provider
Maximum number of inflight records sequencer can accept
Once this maximum is hit, sequencer will induce back pressure on clients. This controls the total number of records regardless of how many batches.
Note that this will be increased to fit the biggest batch of records being enqueued.
Definition of a retry policy
No retry strategy.
Retry with a fixed delay strategy.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Number of maximum attempts before giving up. Infinite retries if unset.
Retry with an exponential strategy. The next retry is computed as `min(last_retry_interval * factor, max_interval)`.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
The factor to use to compute the next retry attempt.
Number of maximum attempts before giving up. Infinite retries if unset.
Maximum interval between retries.
Can be configured using the [`jiff::fmt::friendly`](https://docs.rs/jiff/latest/jiff/fmt/friendly/index.html) format or ISO8601, for example `5 hours`.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Definition of a retry policy
No retry strategy.
Retry with a fixed delay strategy.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Number of maximum attempts before giving up. Infinite retries if unset.
Retry with an exponential strategy. The next retry is computed as `min(last_retry_interval * factor, max_interval)`.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
The factor to use to compute the next retry attempt.
Number of maximum attempts before giving up. Infinite retries if unset.
Maximum interval between retries.
Can be configured using the [`jiff::fmt::friendly`](https://docs.rs/jiff/latest/jiff/fmt/friendly/index.html) format or ISO8601, for example `5 hours`.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Maximum number of records to prefetch from log servers
The number of records bifrost will attempt to prefetch from replicated loglet's log-servers for every loglet reader (e.g. partition processor). Note that this mainly impacts readers that are not co-located with the loglet sequencer (i.e. partition processor followers).
Non-zero human-readable bytes
Trigger to prefetch more records
When read-ahead is used (readahead-records), this value (percentage in float) will determine when readers should trigger a prefetch for another batch to fill up the buffer. For instance, if this value is 0.3, then bifrost will trigger a prefetch when 30% or more of the read-ahead slots become available (e.g. partition processor consumed records and freed up enough slots).
The higher the value is, the longer bifrost will wait before it triggers the next fetch, potentially fetching more records as a result.
To illustrate, if readahead-records is set to 100 and readahead-trigger-ratio is 1.0. Then bifrost will prefetch up to 100 records from log-servers and will not trigger the next prefetch unless the consumer consumes 100% of this buffer. This means that bifrost will read in batches but will not do while the consumer is still reading the previous batch.
Value must be between 0 and 1. It will be clamped at `1.0`.
Definition of a retry policy
No retry strategy.
Retry with a fixed delay strategy.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Number of maximum attempts before giving up. Infinite retries if unset.
Retry with an exponential strategy. The next retry is computed as `min(last_retry_interval * factor, max_interval)`.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
The factor to use to compute the next retry attempt.
Number of maximum attempts before giving up. Infinite retries if unset.
Maximum interval between retries.
Can be configured using the [`jiff::fmt::friendly`](https://docs.rs/jiff/latest/jiff/fmt/friendly/index.html) format or ISO8601, for example `5 hours`.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Human-readable bytes
When enabled, automatic improvement periodically checks with the loglet provider if the loglet configuration can be improved by performing a reconfiguration.
This allows the log to pick up replication property changes, apply better placement of replicas, or for other reasons.
Metadata store options
Limit number of in-flight requests
Number of in-flight metadata store requests.
The memory budget for rocksdb memtables in bytes
If this value is set, it overrides the ratio defined in `rocksdb-memory-ratio`.
Non-zero human-readable bytes
The memory budget for rocksdb memtables as ratio
This defines the total memory for rocksdb as a ratio of all memory available to memtables (See `rocksdb-total-memtables-ratio` in common).
Auto join the metadata cluster when being started
Defines whether this node should auto join the metadata store cluster when being started for the first time.
Files will be opened in "direct I/O" mode which means that data r/w from the disk will not be cached or buffered. The hardware buffer of the devices may however still be used. Memory mapped files are not impacted by these parameters.
Use O\_DIRECT for writes in background flush and compactions.
The default depends on the different rocksdb use-cases at Restate.
Supports hot-reloading (Partial / Bifrost only)
Disable rocksdb statistics collection
Default: False (statistics enabled)
Default: the number of CPU cores on this node.
If non-zero, we perform bigger reads when doing compaction. If you're running RocksDB on spinning disks, you should set this to at least 2MB. That way RocksDB's compaction is doing sequential instead of random reads.
Non-zero human-readable bytes
StatsLevel can be used to reduce statistics overhead by skipping certain types of stats in the stats collection process.
Default: "except-detailed-timers"
Disable all metrics
Disable timer stats, and skip histogram stats
Skip timer stats
Collect all stats except time inside mutex lock AND time spent on compression.
Collect all stats except the counters requiring to get time inside the mutex lock.
Collect all stats, including measuring duration of mutex operations. If getting time is expensive on the platform to run, it can reduce scalability to more threads, especially for writes.
Verbosity of the LOG.
Default: "error"
Verbosity of the LOG.
Number of info LOG files to keep
Default: 1
Max size of info LOG file
Default: 64MB
Non-zero human-readable bytes
Uncompressed block size
Default: 64KiB
Non-zero human-readable bytes
The number of ticks before triggering an election
The number of ticks before triggering an election. The value must be larger than `raft_heartbeat_tick`. It's recommended to set `raft_election_tick = 10 * raft_heartbeat_tick`. Decrease this value if you want to react faster to failed leaders. Note, decreasing this value too much can lead to cluster instabilities due to falsely detecting dead leaders.
The number of ticks before sending a heartbeat
A leader sends heartbeat messages to maintain its leadership every heartbeat ticks. Decrease this value to send heartbeats more often.
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
The threshold for trimming the raft log. The log will be trimmed if the number of apply entries exceeds this threshold. The default value is `1000`.
Common network configuration options for communicating with Restate cluster nodes. Note that similar keys are present in other config sections, such as in Service Client options.
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Definition of a retry policy
No retry strategy.
Retry with a fixed delay strategy.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Number of maximum attempts before giving up. Infinite retries if unset.
Retry with an exponential strategy. The next retry is computed as `min(last_retry_interval * factor, max_interval)`.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
The factor to use to compute the next retry attempt.
Number of maximum attempts before giving up. Infinite retries if unset.
Maximum interval between retries.
Can be configured using the [`jiff::fmt::friendly`](https://docs.rs/jiff/latest/jiff/fmt/friendly/index.html) format or ISO8601, for example `5 hours`.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
HTTP/2 Adaptive Window
Disables Zstd compression for internal gRPC network connections
Non-zero human-readable bytes
Configuration is only used on nodes running with `log-server` role.
The memory budget for rocksdb memtables in bytes
If this value is set, it overrides the ratio defined in `rocksdb-memory-ratio`.
Non-zero human-readable bytes
The memory budget for rocksdb memtables as ratio
This defines the total memory for rocksdb as a ratio of all memory available to the log-server.
(See `rocksdb-total-memtables-ratio` in common).
Disable fsync of WAL on every batch
The maximum number of subcompactions to run in parallel.
Setting this to 1 means no sub-compactions are allowed (i.e. only 1 thread will do the compaction).
Default is 0 which maps to floor(number of CPU cores / 2)
Human-readable bytes
Trigger a commit when the batch size exceeds this threshold.
Set to 0 or 1 to commit the write batch on every command.
The number of messages that can queue up on input network stream while request processor is busy.
Files will be opened in "direct I/O" mode which means that data r/w from the disk will not be cached or buffered. The hardware buffer of the devices may however still be used. Memory mapped files are not impacted by these parameters.
Use O\_DIRECT for writes in background flush and compactions.
The default depends on the different rocksdb use-cases at Restate.
Supports hot-reloading (Partial / Bifrost only)
Disable rocksdb statistics collection
Default: False (statistics enabled)
Default: the number of CPU cores on this node.
If non-zero, we perform bigger reads when doing compaction. If you're running RocksDB on spinning disks, you should set this to at least 2MB. That way RocksDB's compaction is doing sequential instead of random reads.
Non-zero human-readable bytes
StatsLevel can be used to reduce statistics overhead by skipping certain types of stats in the stats collection process.
Default: "except-detailed-timers"
Disable all metrics
Disable timer stats, and skip histogram stats
Skip timer stats
Collect all stats except time inside mutex lock AND time spent on compression.
Collect all stats except the counters requiring to get time inside the mutex lock.
Collect all stats, including measuring duration of mutex operations. If getting time is expensive on the platform to run, it can reduce scalability to more threads, especially for writes.
Verbosity of the LOG.
Default: "error"
Verbosity of the LOG.
Number of info LOG files to keep
Default: 1
Max size of info LOG file
Default: 64MB
Non-zero human-readable bytes
Uncompressed block size
Default: 64KiB
Non-zero human-readable bytes
Defines the roles which this Restate node should run, by default the node starts with all roles.
A worker runs partition processor (journal, state, and drives invocations)
Admin runs cluster controller and user-facing admin APIs
Serves the metadata store
Serves a log-server for replicated loglets
Serves HTTP ingress requests
Unique name for this node in the cluster. The node must not change unless it's started with empty local store. It defaults to the node's hostname.
\[PREVIEW FEATURE] Setting the location allows Restate to form a tree-like cluster topology. The value is written in the format of "region\[.zone]" to assign this node to a specific region, or to a zone within a region.
The value of region and zone is arbitrary but whitespace and `.` are disallowed.
NOTE: It's *strongly* recommended to not change the node's location string after its initial registration. Changing the location may result in data loss or data inconsistency if `log-server` is enabled on this node.
When this value is not set, the node is considered to be in the *default* location. The *default* location means that the node is not assigned to any specific region or zone.
## Examples - `us-west` -- the node is in the `us-west` region. - `us-west.a1` -- the node is in the `us-west` region and in the `a1` zone. - \`\` -- \[default] the node is in the default location
If set, the node insists on acquiring this node ID.
A unique identifier for the cluster. All nodes in the same cluster should have the same.
If true, then this node is allowed to automatically provision as a new cluster. This node *must* have an admin role and a new nodes configuration will be created that includes this node.
auto-provision is allowed by default in development mode and is disabled if restate-server runs with `--production` flag to prevent cluster nodes from forming their own clusters, rather than forming a single cluster.
Use `restatectl` to provision the cluster/node if automatic provisioning is disabled.
This can also be explicitly disabled by setting this value to false.
Default: true
The working directory which this Restate node should use for relative paths. The default is `restate-data` under the current working directory.
The metadata client type to store metadata
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Definition of a retry policy
No retry strategy.
Retry with a fixed delay strategy.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Number of maximum attempts before giving up. Infinite retries if unset.
Retry with an exponential strategy. The next retry is computed as `min(last_retry_interval * factor, max_interval)`.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
The factor to use to compute the next retry attempt.
Number of maximum attempts before giving up. Infinite retries if unset.
Maximum interval between retries.
Can be configured using the [`jiff::fmt::friendly`](https://docs.rs/jiff/latest/jiff/fmt/friendly/index.html) format or ISO8601, for example `5 hours`.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Store metadata on the replicated metadata store that runs on nodes with the metadata-server role.
Restate metadata server address list
Store metadata on an external etcd cluster.
The addresses are formatted as `host:port`
Etcd cluster node address list
Store metadata on an external object store.
This location will be used to persist cluster metadata. Takes the form of a URL with `s3://` as the protocol and bucket name as the authority, plus an optional prefix specified as the path component.
Example: `s3://bucket/prefix`
Definition of a retry policy
No retry strategy.
Retry with a fixed delay strategy.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Number of maximum attempts before giving up. Infinite retries if unset.
Retry with an exponential strategy. The next retry is computed as `min(last_retry_interval * factor, max_interval)`.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
The factor to use to compute the next retry attempt.
Number of maximum attempts before giving up. Infinite retries if unset.
Maximum interval between retries.
Can be configured using the [`jiff::fmt::friendly`](https://docs.rs/jiff/latest/jiff/fmt/friendly/index.html) format or ISO8601, for example `5 hours`.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
The AWS configuration profile to use for S3 object store destinations. If you use named profiles in your AWS configuration, you can replace all the other settings with a single profile reference. See the \[AWS documentation on profiles] ([https://docs.aws.amazon.com/sdkref/latest/guide/file-format.html](https://docs.aws.amazon.com/sdkref/latest/guide/file-format.html)) for more.
AWS region to use with S3 object store destinations. This may be inferred from the environment, for example the current region when running in EC2. Because of the request signing algorithm this must have a value. For Minio, you can generally set this to any string, such as `us-east-1`.
Username for Minio, or consult the service documentation for other S3-compatible stores.
Password for Minio, or consult the service documentation for other S3-compatible stores.
This is only needed with short-term STS session credentials.
When you use Amazon S3, this is typically inferred from the region and there is no need to set it. With other object stores, you will have to provide an appropriate HTTP(S) endpoint. If *not* using HTTPS, also set `aws-allow-http` to `true`.
Allow plain HTTP to be used with the object store endpoint. Required when the endpoint URL that isn't using HTTPS.
Address to bind for the Node server. Derived from the advertised address, defaulting to `0.0.0.0:$PORT` (where the port will be inferred from the URL scheme).
Address that other nodes will use to connect to this node. Default is `http://127.0.0.1:5122/`
Number of partitions that will be provisioned during initial cluster provisioning. partitions are the logical shards used to process messages.
Cannot be higher than `65535` (You should almost never need as many partitions anyway)
NOTE 1: This config entry only impacts the initial number of partitions, the value of this entry is ignored for provisioned nodes/clusters.
NOTE 2: This will be renamed to `default-num-partitions` by default as of v1.3+
Default: 24
Configures the global default replication factor to be used by the the system.
Note that this value only impacts the cluster initial provisioning and will not be respected after the cluster has been provisioned.
To update existing clusters use the `restatectl` utility.
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Size of the default thread pool used to perform internal tasks. If not set, it defaults to the number of CPU cores.
Log filter configuration. Can be overridden by the `RUST_LOG` environment variable. Check the [`RUST_LOG` documentation](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html) for more details how to configure it.
Log format
Enables verbose logging. Not recommended in production.
Enables compact logging.
Enables json logging. You can use a json log collector to ingest these logs and further process them.
Disable ANSI terminal codes for logs. This is useful when the log collector doesn't support processing ANSI terminal codes.
Address to bind for the tokio-console tracing subscriber. If unset and restate-server is built with tokio-console support, it'll listen on `0.0.0.0:6669`.
Disable prometheus metric recording and reporting. Default is `false`.
Storage high priority thread pool
This configures the restate-managed storage thread pool for performing high-priority or latency-sensitive storage tasks when the IO operation cannot be performed on in-memory caches.
Storage low priority thread pool
This configures the restate-managed storage thread pool for performing low-priority or latency-insensitive storage tasks.
Non-zero human-readable bytes
The memory size used across all memtables (ratio between 0 to 1.0). This limits how much memory memtables can eat up from the value in rocksdb-total-memory-limit. When set to 0, memtables can take all available memory up to the value specified in rocksdb-total-memory-limit. This value will be sanitized to 1.0 if outside the valid bounds.
The number of threads to reserve to Rocksdb background tasks. Defaults to the number of cores on the machine.
The number of threads to reserve to high priority Rocksdb background tasks.
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Note if automatic memory budgeting is enabled, it should be safe to allow rocksdb to stall if it hits the limit. However, if rocksdb stall kicked in, it's unlikely that the system will recover from this without intervention.
Disable perf stats
Enables only count stats
Count stats and enable time stats except for mutexes
Other than time, also measure CPU time counters. Still don't measure time (neither wall time nor CPU time) for mutexes
Enables count and time stats
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Definition of a retry policy
No retry strategy.
Retry with a fixed delay strategy.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Number of maximum attempts before giving up. Infinite retries if unset.
Retry with an exponential strategy. The next retry is computed as `min(last_retry_interval * factor, max_interval)`.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
The factor to use to compute the next retry attempt.
Number of maximum attempts before giving up. Infinite retries if unset.
Maximum interval between retries.
Can be configured using the [`jiff::fmt::friendly`](https://docs.rs/jiff/latest/jiff/fmt/friendly/index.html) format or ISO8601, for example `5 hours`.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Restate uses Scarf to collect anonymous usage data to help us understand how the software is being used. You can set this flag to true to disable this collection. It can also be set with the environment variable DO\_NOT\_TRACK=1.
This is a shortcut to set both \[`Self::tracing_runtime_endpoint`], and \[`Self::tracing_services_endpoint`].
Specify the tracing endpoint to send runtime traces to. Traces will be exported using [OTLP gRPC](https://opentelemetry.io/docs/specs/otlp/#otlpgrpc) through [opentelemetry\_otlp](https://docs.rs/opentelemetry-otlp/0.12.0/opentelemetry_otlp/).
To configure the sampling, please refer to the [opentelemetry autoconfigure docs](https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md#sampler).
Overrides \[`Self::tracing_endpoint`] for runtime traces
Specify the tracing endpoint to send runtime traces to. Traces will be exported using [OTLP gRPC](https://opentelemetry.io/docs/specs/otlp/#otlpgrpc) through [opentelemetry\_otlp](https://docs.rs/opentelemetry-otlp/0.12.0/opentelemetry_otlp/).
To configure the sampling, please refer to the [opentelemetry autoconfigure docs](https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md#sampler).
Overrides \[`Self::tracing_endpoint`] for services traces
Specify the tracing endpoint to send services traces to. Traces will be exported using [OTLP gRPC](https://opentelemetry.io/docs/specs/otlp/#otlpgrpc) through [opentelemetry\_otlp](https://docs.rs/opentelemetry-otlp/0.12.0/opentelemetry_otlp/).
To configure the sampling, please refer to the [opentelemetry autoconfigure docs](https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md#sampler).
If set, an exporter will be configured to write traces to files using the Jaeger JSON format. Each trace file will start with the `trace` prefix.
If unset, no traces will be written to file.
It can be used to export traces in a structured format without configuring a Jaeger agent.
To inspect the traces, open the Jaeger UI and use the Upload JSON feature to load and inspect them.
Distributed tracing exporter filter. Check the [`RUST_LOG` documentation](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html) for more details how to configure it.
Specify additional headers you want the system to send to the tracing endpoint (e.g. authentication headers).
A path to a file, such as "/var/secrets/key.pem", which contains exactly one ed25519 private key in PEM format. Such a file can be generated with `openssl genpkey -algorithm ed25519`. If provided, this key will be used to attach JWTs to requests from this client which SDKs may optionally verify, proving that the caller is a particular Restate instance.
This file is currently only read on client creation, but this may change in future. Parsed public keys will be logged at INFO level in the same format that SDKs expect.
Headers that should be applied to all outgoing requests (HTTP and Lambda). Defaults to `x-restate-cluster-name: `.
Configuration for the HTTP/2 keep-alive mechanism, using PING frames.
Please note: most gateways don't propagate the HTTP/2 keep-alive between downstream and upstream hosts. In those environments, you need to make sure the gateway can detect a broken connection to the upstream deployment(s).
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
A URI, such as `http://127.0.0.1:10001`, of a server to which all invocations should be sent, with the `Host` header set to the deployment URI. HTTPS proxy URIs are supported, but only HTTP endpoint traffic will be proxied currently. Can be overridden by the `HTTP_PROXY` environment variable.
HTTP authorities eg `localhost`, `restate.dev`, `127.0.0.1` that should not be proxied by the http\_proxy. Ports are ignored. Subdomains are also matched. An entry “\*” matches all hostnames. Can be overridden by the `NO_PROXY` environment variable, which supports comma separated values.
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Sets the initial maximum of locally initiated (send) streams.
This value will be overwritten by the value included in the initial SETTINGS frame received from the peer as part of a \[connection preface].
Default: None
**NOTE**: Setting this value to None (default) users the default recommended value from HTTP2 specs
Name of the AWS profile to select. Defaults to 'AWS\_PROFILE' env var, or otherwise the `default` profile.
An external ID to apply to any AssumeRole operations taken by this client. [https://docs.aws.amazon.com/IAM/latest/UserGuide/id\_roles\_create\_for-user\_externalid.html](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html) Can be overridden by the `AWS_EXTERNAL_ID` environment variable.
Request minimum size to enable compression. The request size includes the total of the journal replay and its framing using Restate service protocol, without accounting for the json envelope and the base 64 encoding.
Default: 4MB (The default AWS Lambda Limit is 6MB, 4MB roughly accounts for +33% of Base64 and the json envelope).
Human-readable bytes
Files will be opened in "direct I/O" mode which means that data r/w from the disk will not be cached or buffered. The hardware buffer of the devices may however still be used. Memory mapped files are not impacted by these parameters.
Use O\_DIRECT for writes in background flush and compactions.
The default depends on the different rocksdb use-cases at Restate.
Supports hot-reloading (Partial / Bifrost only)
Disable rocksdb statistics collection
Default: False (statistics enabled)
Default: the number of CPU cores on this node.
If non-zero, we perform bigger reads when doing compaction. If you're running RocksDB on spinning disks, you should set this to at least 2MB. That way RocksDB's compaction is doing sequential instead of random reads.
Non-zero human-readable bytes
StatsLevel can be used to reduce statistics overhead by skipping certain types of stats in the stats collection process.
Default: "except-detailed-timers"
Disable all metrics
Disable timer stats, and skip histogram stats
Skip timer stats
Collect all stats except time inside mutex lock AND time spent on compression.
Collect all stats except the counters requiring to get time inside the mutex lock.
Collect all stats, including measuring duration of mutex operations. If getting time is expensive on the platform to run, it can reduce scalability to more threads, especially for writes.
Verbosity of the LOG.
Default: "error"
Verbosity of the LOG.
Number of info LOG files to keep
Default: 1
Max size of info LOG file
Default: 64MB
Non-zero human-readable bytes
Uncompressed block size
Default: 64KiB
Non-zero human-readable bytes
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Specifies how many gossip intervals of inactivity need to pass before considering a node as dead.
On every gossip interval, how many peers each node attempts to gossip with. The default is optimized for small clusters (less than 5). On larger clusters, if gossip overhead is noticeable, consider reducing this value to 1.
Gossips before failure detector is stable
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
How many intervals need to pass without receiving any gossip messages before considering this node as potentially isolated/dead. This threshold is used in the case where the node can still send gossip messages but did not receive any. This can rarely happen in asymmetric network partitions.
In this case, the node will advertise itself as dead in the gossip messages it sends out.
Note: this threshold does not apply to a cluster that's configured with a single node.
In addition to basic health/liveness information, the gossip protocol is used to exchange extra information about the roles hosted by this node. For instance, which partitions are currently running, their configuration versions, and the durable LSN of the corresponding partition databases. This information is sent every Nth gossip message. This setting controls the frequency of this exchange. For instance, `10` means that every 10th gossip message will contain the extra information about.
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Maximum journal retention duration that can be configured. When discovering a service deployment, or when modifying the journal retention using the Admin API, the given value will be clamped.
Unset means no limit.
Duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
The default retry policy to use for invocations.
The retry policy can be customized on a service/handler basis, using the respective SDK APIs.
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
The factor to use to compute the next retry attempt. Default: `2.0`.
Unlimited retries.
Bounded number of retries.
Pause the invocation when max attempts are reached.
Kill the invocation when max attempts are reached.
Non-zero duration string in either jiff human friendly or ISO8601 format. Check [https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing](https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing) for more details.
Maximum max attempts configurable in an invocation retry policy. When discovering a service deployment with configured retry policies, or when modifying the invocation retry policy using the Admin API, the given value will be clamped.
`None` means no limit, that is infinite retries is enabled.
# SQL Introspection API
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/references/sql-introspection
API reference for inspecting the invocation status and service state.
This page contains the reference of the introspection tables.
To learn how to access the introspection interface, check out the [introspection documentation](/services/introspection).
## Table: `state`
| Column name | Type | Description |
| --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `partition_key` | `UInt64` | Internal column that is used for partitioning the services invocations. Can be ignored. |
| `service_name` | `Utf8` | The name of the invoked service. |
| `service_key` | `Utf8` | The key of the Virtual Object. |
| `key` | `Utf8` | The `utf8` state key. |
| `key_length` | `UInt64` | The byte length of the state key. If you are writing a query that only needs to know the length, reading this field will be much more efficient than reading octet\_length(key). |
| `value_utf8` | `Utf8` | Only contains meaningful values when a service stores state as `utf8`. This is the case for services that serialize state using JSON (default for Typescript SDK, Java/Kotlin SDK if using JsonSerdes). |
| `value` | `Binary` | A binary, uninterpreted representation of the value. You can use the more specific column `value_utf8` if the value is a string. |
| `value_length` | `UInt64` | The byte length of the value. If you are writing a query that only needs to know the length, reading this field will be much more efficient than reading length(value). |
## Table: `sys_journal`
| Column name | Type | Description |
| ----------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `partition_key` | `UInt64` | Internal column that is used for partitioning the services invocations. Can be ignored. |
| `id` | `Utf8` | [Invocation ID](/operate/invocation#invocation-identifier). |
| `index` | `UInt32` | The index of this journal entry. |
| `entry_type` | `Utf8` | The entry type. You can check all the available entry types in [`entries.rs`](https://github.com/restatedev/restate/blob/main/crates/types/src/journal/entries.rs). |
| `name` | `Utf8` | The name of the entry supplied by the user, if any. |
| `completed` | `Boolean` | Indicates whether this journal entry has been completed; this is only valid for some entry types. |
| `invoked_id` | `Utf8` | If this entry represents an outbound invocation, indicates the ID of that invocation. |
| `invoked_target` | `Utf8` | If this entry represents an outbound invocation, indicates the invocation Target. Format for plain services: `ServiceName/HandlerName`, e.g. `Greeter/greet`. Format for virtual objects/workflows: `VirtualObjectName/Key/HandlerName`, e.g. `Greeter/Francesco/greet`. |
| `sleep_wakeup_at` | `TimestampMillisecond` | If this entry represents a sleep, indicates wakeup time. |
| `promise_name` | `Utf8` | If this entry is a promise related entry (GetPromise, PeekPromise, CompletePromise), indicates the promise name. |
| `raw` | `Binary` | Raw binary representation of the entry. Check the [service protocol](https://github.com/restatedev/service-protocol) for more details to decode it. |
| `version` | `UInt32` | The journal version. |
| `entry_json` | `Utf8` | The entry serialized as a JSON string. Filled only if journal version is 2. |
| `entry_lite_json` | `Utf8` | The EntryLite projection serialized as a JSON string. Filled only if journal version is 2. |
| `appended_at` | `TimestampMillisecond` | When the entry was appended to the journal. Filled only if journal version is 2. |
## Table: `sys_journal_events`
| Column name | Type | Description |
| --------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `partition_key` | `UInt64` | Internal column that is used for partitioning the services invocations. Can be ignored. |
| `id` | `Utf8` | [Invocation ID](/operate/invocation#invocation-identifier). |
| `after_journal_entry_index` | `UInt32` | The journal index after which this event happened. This can be used to establish a total order between events and journal entries. |
| `appended_at` | `TimestampMillisecond` | When the entry was appended to the journal. |
| `event_type` | `Utf8` | The event type. |
| `event_json` | `Utf8` | The event serialized as a JSON string. |
## Table: `sys_keyed_service_status`
| Column name | Type | Description |
| --------------- | -------- | --------------------------------------------------------------------------------------- |
| `partition_key` | `UInt64` | Internal column that is used for partitioning the services invocations. Can be ignored. |
| `service_name` | `Utf8` | The name of the invoked virtual object/workflow. |
| `service_key` | `Utf8` | The key of the virtual object/workflow. |
| `invocation_id` | `Utf8` | [Invocation ID](/operate/invocation#invocation-identifier). |
## Table: `sys_inbox`
| Column name | Type | Description |
| ----------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `partition_key` | `UInt64` | Internal column that is used for partitioning the services invocations. Can be ignored. |
| `service_name` | `Utf8` | The name of the invoked virtual object/workflow. |
| `service_key` | `Utf8` | The key of the virtual object/workflow. |
| `id` | `Utf8` | [Invocation ID](/operate/invocation#invocation-identifier). |
| `sequence_number` | `UInt64` | Sequence number in the inbox. |
| `created_at` | `TimestampMillisecond` | Timestamp indicating the start of this invocation. DEPRECATED: you should not use this field anymore, but join with the sys\_invocation table |
## Table: `sys_idempotency`
| Column name | Type | Description |
| ----------------- | -------- | --------------------------------------------------------------------------------------- |
| `partition_key` | `UInt64` | Internal column that is used for partitioning the services invocations. Can be ignored. |
| `service_name` | `Utf8` | The name of the invoked service. |
| `service_key` | `Utf8` | The key of the virtual object or the workflow ID. Null for regular services. |
| `service_handler` | `Utf8` | The invoked handler. |
| `idempotency_key` | `Utf8` | The user provided idempotency key. |
| `invocation_id` | `Utf8` | [Invocation ID](/operate/invocation#invocation-identifier). |
## Table: `sys_promise`
| Column name | Type | Description |
| ------------------------------- | --------- | --------------------------------------------------------------------------------------- |
| `partition_key` | `UInt64` | Internal column that is used for partitioning the services invocations. Can be ignored. |
| `service_name` | `Utf8` | The name of the workflow service. |
| `service_key` | `Utf8` | The workflow ID. |
| `key` | `Utf8` | The promise key. |
| `completed` | `Boolean` | True if the promise was completed. |
| `completion_success_value` | `Binary` | The completion success, if any. |
| `completion_success_value_utf8` | `Utf8` | The completion success as UTF-8 string, if any. |
| `completion_failure` | `Utf8` | The completion failure, if any. |
## Table: `sys_service`
| Column name | Type | Description |
| --------------- | --------- | ---------------------------------------------------------------------- |
| `name` | `Utf8` | The name of the registered user service. |
| `revision` | `UInt64` | The latest deployed revision. |
| `public` | `Boolean` | Whether the service is accessible through the ingress endpoint or not. |
| `ty` | `Utf8` | The service type. Either `service` or `virtual_object` or `workflow`. |
| `deployment_id` | `Utf8` | The ID of the latest deployment |
## Table: `sys_deployment`
| Column name | Type | Description |
| ------------------------------ | ---------------------- | ----------------------------------------------------------- |
| `id` | `Utf8` | The ID of the service deployment. |
| `ty` | `Utf8` | The type of the endpoint. Either `http` or `lambda`. |
| `endpoint` | `Utf8` | The address of the endpoint. Either HTTP URL or Lambda ARN. |
| `created_at` | `TimestampMillisecond` | Timestamp indicating the deployment registration time. |
| `min_service_protocol_version` | `UInt32` | Minimum supported protocol version. |
| `max_service_protocol_version` | `UInt32` | Maximum supported protocol version. |
## Table: `sys_invocation`
| Column name | Type | Description |
| ------------------------------------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id` | `Utf8` | [Invocation ID](/operate/invocation#invocation-identifier). |
| `target` | `Utf8` | Invocation Target. Format for plain services: `ServiceName/HandlerName`, e.g. `Greeter/greet`. Format for virtual objects/workflows: `VirtualObjectName/Key/HandlerName`, e.g. `Greeter/Francesco/greet`. |
| `target_service_name` | `Utf8` | The name of the invoked service. |
| `target_service_key` | `Utf8` | The key of the virtual object or the workflow ID. Null for regular services. |
| `target_handler_name` | `Utf8` | The invoked handler. |
| `target_service_ty` | `Utf8` | The service type. Either `service` or `virtual_object` or `workflow`. |
| `idempotency_key` | `Utf8` | Idempotency key, if any. |
| `invoked_by` | `Utf8` | Either: \* `ingress` if the invocation was created externally. \* `service` if the invocation was created by another Restate service. \* `subscription` if the invocation was created by a subscription (e.g. Kafka). \* `restart_as_new` if the invocation was created by restarting an old invocation as new. |
| `invoked_by_service_name` | `Utf8` | The name of caller service if `invoked_by = 'service'`. |
| `invoked_by_id` | `Utf8` | The caller [Invocation ID](/operate/invocation#invocation-identifier) if `invoked_by = 'service'`. |
| `invoked_by_subscription_id` | `Utf8` | The subscription id if `invoked_by = 'subscription'`. |
| `invoked_by_target` | `Utf8` | The caller invocation target if `invoked_by = 'service'`. |
| `restarted_from` | `Utf8` | The original invocation id if `invoked_by = 'restart_as_new'`. |
| `pinned_deployment_id` | `Utf8` | The ID of the service deployment that started processing this invocation, and will continue to do so (e.g. for retries). This gets set after the first journal entry has been stored for this invocation. |
| `pinned_service_protocol_version` | `UInt32` | The negotiated protocol version used for this invocation. This gets set after the first journal entry has been stored for this invocation. |
| `trace_id` | `Utf8` | The ID of the trace that is assigned to this invocation. Only relevant when tracing is enabled. |
| `journal_size` | `UInt32` | The number of journal entries durably logged for this invocation. |
| `journal_commands_size` | `UInt32` | The number of commands generated by this invocation, stored in the journal. Only relevant when pinned\_service\_protocol\_version >= 4. |
| `created_at` | `TimestampMillisecond` | Timestamp indicating the start of this invocation. |
| `created_using_restate_version` | `Utf8` | restate-server version in use when this invocation was created. |
| `modified_at` | `TimestampMillisecond` | Timestamp indicating the last invocation status transition. For example, last time the status changed from `invoked` to `suspended`. |
| `inboxed_at` | `TimestampMillisecond` | Timestamp indicating when the invocation was inboxed, if ever. |
| `scheduled_at` | `TimestampMillisecond` | Timestamp indicating when the invocation was scheduled, if ever. |
| `scheduled_start_at` | `TimestampMillisecond` | If the invocation was scheduled, indicates the timestamp when the invocation should start. |
| `running_at` | `TimestampMillisecond` | Timestamp indicating when the invocation first transitioned to running, if ever. |
| `completed_at` | `TimestampMillisecond` | Timestamp indicating when the invocation was completed, if ever. |
| `completion_retention` | `DurationMillisecond` | For how long the metadata of this invocation, including its result, is retained after completion. |
| `journal_retention` | `DurationMillisecond` | For how long the journal is retained after completion. |
| `retry_count` | `UInt64` | The number of invocation attempts since the current leader started executing it. Increments on start, so a value greater than 1 means a failure occurred. Note: the value is not a global attempt counter across invocation suspensions and leadership changes. |
| `last_start_at` | `TimestampMillisecond` | Timestamp indicating the start of the most recent attempt of this invocation. |
| `next_retry_at` | `TimestampMillisecond` | Timestamp indicating the start of the next attempt of this invocation. |
| `last_attempt_deployment_id` | `Utf8` | The ID of the service deployment that executed the most recent attempt of this invocation; this is set before a journal entry is stored, but can change later. |
| `last_attempt_server` | `Utf8` | Server/SDK version, e.g. `restate-sdk-java/1.0.1` |
| `last_failure` | `Utf8` | An error message describing the most recent failed attempt of this invocation, if any. |
| `last_failure_error_code` | `Utf8` | The error code of the most recent failed attempt of this invocation, if any. |
| `last_failure_related_entry_index` | `UInt64` | The index of the journal entry that caused the failure, if any. It may be out-of-bound of the currently stored entries in `sys_journal`. DEPRECATED: you should not use this field anymore, but last\_failure\_related\_command\_index instead. |
| `last_failure_related_entry_name` | `Utf8` | The name of the journal entry that caused the failure, if any. DEPRECATED: you should not use this field anymore, but last\_failure\_related\_command\_name instead. |
| `last_failure_related_entry_type` | `Utf8` | The type of the journal entry that caused the failure, if any. You can check all the available entry types in [`entries.rs`](https://github.com/restatedev/restate/blob/main/crates/types/src/journal/entries.rs). DEPRECATED: you should not use this field anymore, but last\_failure\_related\_command\_type instead. |
| `last_failure_related_command_index` | `UInt64` | The index of the command in the journal that caused the failure, if any. It may be out-of-bound of the currently stored commands in `sys_journal`. |
| `last_failure_related_command_name` | `Utf8` | The name of the command that caused the failure, if any. |
| `last_failure_related_command_type` | `Utf8` | The type of the command that caused the failure, if any. You can check all the available command types in [`entries.rs`](https://github.com/restatedev/restate/blob/main/crates/types/src/journal_v2/command.rs). |
| `status` | `Utf8` | Either `pending` or `scheduled` or `ready` or `running` or `paused` or `backing-off` or `suspended` or `completed`. |
| `completion_result` | `Utf8` | If `status = 'completed'`, this contains either `success` or `failure` |
| `completion_failure` | `Utf8` | If `status = 'completed' AND completion_result = 'failure'`, this contains the error cause |
# Typescript API
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/references/tsdocs
# Clusters
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/server/clusters
Operating Restate clusters
This page helps with operating [Restate clusters](/deploy/server/cluster).
To understand the terminology used on this page, it might be helpful to read through the [architecture reference](/references/architecture).
## Controlling clusters with `restatectl`
Restate includes a command line utility tool to connect to and control running Restate servers called `restatectl`.
This tool is specifically designed for system operators to manage Restate servers and is particularly useful in a cluster environment.
Follow the [installation instructions](/installation#advanced%3A-installing-restatectl) to get `restatectl` set up on your machine.
The `restatectl` tool communicates with Restate at the advertised address specified in the [server configuration](/references/server-config) - by default TCP port 5122.
```shell theme={null}
restatectl status
```
Optionally, specify the addresses via `--addresses http://localhost:5122/`:
Check out the [cluster deployment documentation](/server/deploy/cluster)
```shell theme={null}
restatectl nodes list
```
Output:
```shell theme={null}
Node Configuration (v21)
NODE GEN NAME ADDRESS ROLES
N1 6 node-1 http://127.0.0.1:5122/ admin | log-server | metadata-server | worker
N2 4 node-2 http://127.0.0.1:6122/ admin | log-server | metadata-server | worker
N3 6 node-3 http://127.0.0.1:7122/ log-server | metadata-server | worker
```
View the current log configuration (provider, replication, and nodeset) and the effective logs per partition:
```shell theme={null}
restatectl logs list
```
```shell theme={null}
restatectl logs describe
```
Output:
```shell theme={null}
Log chain v8
└ Logs Provider: replicated
├ Log replication: {node: 2}
└ Nodeset size: 0
L-ID FROM-LSN KIND LOGLET-ID REPLICATION SEQUENCER NODESET
0 61 Replicated 0_5 {node: 2} N1:6 [N1, N2, N3]
1 4 Replicated 1_4 {node: 2} N1:6 [N1, N2, N3]
2 4 Replicated 2_4 {node: 2} N1:6 [N1, N2, N3]
3 5 Replicated 3_5 {node: 2} N1:6 [N1, N2, N3]
4 4 Replicated 4_4 {node: 2} N1:6 [N1, N2, N3]
5 5 Replicated 5_5 {node: 2} N1:6 [N1, N2, N3]
6 6 Replicated 6_6 {node: 2} N1:6 [N1, N2, N3]
7 4 Replicated 7_4 {node: 2} N1:6 [N1, N2, N3]
8 4 Replicated 8_4 {node: 2} N1:6 [N1, N2, N3]
```
```shell theme={null}
restatectl partitions list
```
Output:
```shell theme={null}
Alive partition processors (nodes config v21, partition table v21)
ID NODE MODE STATUS EPOCH APPLIED DURABLE ARCHIVED LSN-LAG UPDATED
0 N1:6 Leader Active N1:6 61 - - 0 1 second and 170 ms ago
0 N2:4 Follower Active N1:6 61 - - 0 1 second and 64 ms ago
1 N1:6 Leader Active N1:6 4 - - 0 801 ms ago
1 N2:4 Follower Active N1:6 4 - - 0 779 ms ago
2 N1:6 Leader Active N1:6 4 - - 0 600 ms ago
2 N2:4 Follower Active N1:6 4 - - 0 1 second and 108 ms ago
3 N1:6 Leader Active N1:6 5 - - 0 1 second and 369 ms ago
3 N2:4 Follower Active N1:6 5 - - 0 1 second and 306 ms ago
4 N1:6 Leader Active N1:6 4 - - 0 651 ms ago
4 N2:4 Follower Active N1:6 4 - - 0 1 second and 169 ms ago
5 N1:6 Leader Active N1:6 5 - - 0 567 ms ago
5 N2:4 Follower Active N1:6 5 - - 0 1 second and 382 ms ago
6 N1:6 Leader Active N1:6 6 - - 0 804 ms ago
6 N2:4 Follower Active N1:6 6 - - 0 1 second and 145 ms ago
7 N1:6 Leader Active N1:6 4 - - 0 1 second and 79 ms ago
7 N2:4 Follower Active N1:6 4 - - 0 974 ms ago
8 N1:6 Leader Active N1:6 4 - - 0 1 second and 71 ms ago
8 N2:4 Follower Active N1:6 4 - - 0 717 ms ago
☠️ Dead nodes
NODE LAST-SEEN
N3 11 minutes, 40 seconds and 995 ms ago
```
```shell theme={null}
restatectl config get
```
Output:
```shell theme={null}
⚙️ Cluster Configuration
├ Number of partitions: 8
├ Partition replication: *
└ Logs Provider: replicated
├ Log replication: {node: 1}
└ Nodeset size: 0
```
```shell theme={null}
restatectl config set --help # check options
restatectl config set --log-replication 2 # increase log replication
```
Output:
```shell theme={null}
⚙️ Cluster Configuration
├ Number of partitions: 8
├ Partition replication: *
└ Logs Provider: replicated
- ├ Log replication: {node: 1}
+ ├ Log replication: {node: 2}
└ Nodeset size: 0
? Apply changes? (y/n) › yes
```
## Growing the cluster
You can expand an existing cluster by adding new nodes after it has been started.
A Restate cluster can initially be started with a single node.
Follow the [cluster deployment instructions](/server/deploy/cluster) and ensure that:
* It uses the replicated loglet. If you use local loglet, check [this migration guide](/guides/local-to-replicated).
* `default-replication` is set to 1
* [Snapshotting is enabled](/server/snapshots), to ensure that newly added nodes are fully utilized by the system.
```toml theme={null}
# Replicating data to one node: cluster cannot tolerate node failures
default-replication = 1
```
Launch a new node with the same `cluster-name` and specify at least one existing node's address in `metadata-client.addresses`.
This allows the new node to discover the metadata servers and join the cluster.
Update the cluster’s replication settings to take advantage of the additional nodes and improve fault tolerance.
Increase log replication to your desired number. For example, to replicate to two nodes:
```shell theme={null}
restatectl config set --log-replication 2 --partition-replication 2
```
```shell theme={null}
⚙️ Cluster Configuration
├ Number of partitions: 4
-├ Partition replication: {node: 1}
+├ Partition replication: {node: 2}
└ Logs Provider: replicated
- ├ Log replication: {node: 1}
+ ├ Log replication: {node: 2}
└ Nodeset size: 0
? Apply changes? (y/n) › no
```
Then list the logs:
```shell theme={null}
restatectl logs list
```
```shell theme={null}
Log chain v5
└ Logs Provider: replicated
├ Log replication: {node: 2}
└ Nodeset size: 0
L-ID FROM-LSN KIND LOGLET-ID REPLICATION SEQUENCER NODESET
0 2 Replicated 0_2 {node: 2} N1:2 [N1, N2, N3]
1 2 Replicated 1_2 {node: 2} N1:2 [N1, N2, N3]
2 2 Replicated 2_2 {node: 2} N1:2 [N1, N2, N3]
3 2 Replicated 3_2 {node: 2} N1:2 [N1, N2, N3]
4 2 Replicated 4_2 {node: 2} N1:2 [N1, N2, N3]
5 2 Replicated 5_2 {node: 2} N1:2 [N1, N2, N3]
6 2 Replicated 6_2 {node: 2} N1:2 [N1, N2, N3]
7 2 Replicated 7_2 {node: 2} N1:2 [N1, N2, N3]
```
You might need to re-run the command a few times until all logs reflect the updated replication setting.
If the update takes longer than expected, check the node logs for errors or warnings.
## Managing the Replicated Loglet
You can manage the [replicated loglet](/references/architecture#durable-log-“bifrost”) via:
```shell theme={null}
restatectl replicated-loglet
```
When you use the replicated loglet, which is required for distributed operation, the Restate [control plane](/references/architecture#control-plane) selects nodes on which to replicate the log according to the specified log replication.
Each [`log-server` node](/references/architecture#log-servers) in the cluster has a storage state which determines how the control plane may use this node. The `set-storage-state` tool allows you to manually override this state as operational needs dictate.
New log servers come up in the `provisioning` state and will automatically transition to `read-write`. The `read-write` state means that the node is considered both healthy to read from and accept writes, that is it may be selected as a nodeset member for new loglet segments.
### View storage state of log server
You can view the current storage state of log servers in your cluster using the `list-servers` sub-command.
```shell theme={null}
restatectl replicated-loglet list-servers
```
```shell theme={null}
Node configuration v12
Log chain v3
NODE GEN STORAGE-STATE HISTORICAL LOGLETS ACTIVE LOGLETS
N1 N1:3 read-write 4 2
N2 N2:2 read-write 4 2
N3 N3:2 read-write 4 2
```
Other valid storage include `data-loss`, `read-only`, and `disabled`. Nodes may transition to `data-loss` if they detect that some previously written data is not available. This does not necessarily imply corruption, only that such nodes may not participate in some quorum checks. Such nodes may transition back to `read-write` if they can be repaired.
The `read-only` and `disabled` states are of particular interest to operators. Log servers in `read-only` storage state may continue to serve both reads and writes, but will no longer be selected as participants in new segments' nodesets. The control plane will reconfigure existing logs to move away from such nodes.
### Manually update the log server state
**Danger of data loss**:
`set-storage-state` is a low-level command that allows you to directly set log servers' storage-state. Changing this can lead to cluster unavailability or data loss.
Use the `set-storage-state` sub-command to manually update the log server state, for example to prevent log servers from being included in new nodesets. Consider the following example:
```shell theme={null}
restatectl replicated-loglet set-storage-state --node 1 --storage-state read-only
```
Output:
```shell theme={null}
Node N1 storage-state updated from read-write to read-only
```
The cluster controller reconfigures the log nodeset to exclude `N1`. Depending on the configured log replication level, you may see a warning about compromised availability or, if insufficient log servers are available to achieve the minimum required replication, the log will stop accepting writes altogether.
The `restatectl` checks whether it is possible to create new node sets after marking a given node or set of nodes as read-only.
Examine the logs using `restatectl logs describe`.
## Troubleshooting Clusters
If a misconfigured Restate node with the log server role attempts to join a cluster where the node id is already in use, you will observe that the newly started node aborts with an error:
```log wrap theme={null}
ERROR restate_core::task_center: Shutting down: task 4 failed with: Node cannot start a log-server on N3, it has detected that it has lost its data. storage-state is `data-loss`
```
Restarting the existing node that previously owned this id will also cause it to stop with the same message. Follow these steps to return the initial log server into service without losing its stored log segments.
First, prevent the misconfigured node from starting again until the configuration has been corrected. If this was a brand new node, there should be no data stored on it, and you may delete it altogether.
The reused node id has been marked as having `data-loss`. This precaution that tells the Restate control plane to avoid selecting this node as member of new log nodesets. You can view the current status using the [`restatectl replicated-loglet` tool](#managing-the-replicated-loglet):
```shell theme={null}
restatectl replicated-loglet servers
```
```log theme={null}
Node configuration v21
Log chain v6
NODE GEN STORAGE-STATE HISTORICAL LOGLETS ACTIVE LOGLETS
N1 N1:5 read-write 8 2
N2 N2:4 read-write 8 2
N3 N3:6 data-loss 6 0
```
You should also observe that the control plane is now avoiding using this node for log storage. This will result in reduced fault tolerance or even unavailability, depending on the configured minimum log replication:
```shell theme={null}
restatectl logs list
```
```log theme={null}
Logs v3
└ Logs Provider: replicated
├ Log replication: {node: 2}
└ Nodeset size: 0
L-ID FROM-LSN KIND LOGLET-ID REPLICATION SEQUENCER NODESET
0 2 Replicated 0_1 {node: 2} N2:1 [N1, N2]
1 2 Replicated 1_1 {node: 2} N2:1 [N1, N2]
```
To restore the original node's ability to accept writes, we can update its metadata using `set-storage-state` subcommand.
Only proceed if you are confident that you understand the reason why the node is in this state, and are certain that its locally stored data is still intact. Since Restate cannot automatically validate that it safe to put this node back into service, we must use the `--force` flag to override the default state transition rules.
```shell theme={null}
restatectl replicated-loglet set-storage-state --node 3 --storage-state 'read-write' --force
```
```text theme={null}
Node N3 storage-state updated from data-loss to read-write
```
You can validate that the server is once again being used for log storage using `logs list` and `replicated-loglet servers` subcommands.
You are observing a partition processor repeatedly crash-looping with a `TrimGapEncountered` error, or see one of the following errors in the Restate server logs:
* `A log trim gap was encountered, but no snapshot repository is configured!`
* `A log trim gap was encountered, but no snapshot is available for this partition!`
* `The latest available snapshot is from an LSN before the target LSN!`
You are observing a situation where the local state available on a given worker node does not allow it to resume from the log's trim point - either because it is brand new, or because its applied partition state is behind the trim point of the partition log. If you are attempting to migrate from a single-node Restate to a cluster deployment, you can also refer to the [migration guide](/guides/local-to-replicated).
To recover from this situation, you need to make available a snapshot of the partition state from another worker, which is up to date with the log. This situation can arise if you have manually trimmed the log, the node is missing a snapshot repository configuration, or the snapshot repository is otherwise inaccessible. See [Log trimming and Snapshots](/server/snapshots#log-trimming-and-snapshots) for more context about how logs, partitions, and snapshots are related.
#### Recovery procedure
##### 1. Identify whether a snapshot repository is configured and accessible
If a snapshot repository is set up on other nodes in the cluster, and simply not configured on the node where you are seeing the partition processor startup errors, correct the configuration on the new node - refer to [Configuring Snapshots](/server/snapshots#configuring-automatic-snapshotting). If you have not yet set up a snapshot repository, please do so now. If it is impossible to use an object store to host the snapshots repository, you can export snapshots to a local filesystem and manually transfer them to other nodes - skip to step `2b`.
In your server configuration, you should have a snapshot path specified as follows:
```toml theme={null}
[worker.snapshots]
destination = "s3://snapshots/prefix"
```
Confirm that this is consistent with other nodes in the cluster.
Check the server logs for any access errors; does the node have the necessary credentials and are those credentials authorized to access the snapshots destination?
##### 2. Publish a snapshot to the repository
Snapshots are produced periodically by partition processors on certain triggers, such as a number of records being appended to the log. If you are seeing the following error, check that snapshot are being written to the object store destination you have configured.
Verify that this partition has an active node:
```shell theme={null}
restatectl partitions list
```
If you have lost all nodes which previously hosted this partition, you have permanent data loss - the partition state can not be fully recovered. Get in touch with us to assist in re-starting the partition accepting the data loss.
Request a snapshot for this partition:
```shell theme={null}
restatectl snapshots create-snapshot {partition_id}
```
You can manually confirm that the snapshot was published to the expected destination. Within the specified snapshot bucket and prefix, you will find a partition-based tree structure. Navigate to the bucket path `{prefix}/{partition_id}` - you should see an entry for the new snapshot id matching the output of the create snapshot command.
##### 2b. Alternative: Manually transfer snapshot from another node
If you are running a cluster but are unable to setup a snapshot repository in a shared object store destination, you can still recover node state by publishing a snapshot from a healthy node ot the local filesystem and manually transferring it to the new node.
**Experimenting with snapshots without an object store**:
Note that shared filesystems are not a supported target for cluster snapshots, and have known correctness risks. The `file://` protocol does not support conditional updates, which makes it unsuitable for potentially contended operation.
Identify an up-to-date node which is running the partition by running:
```shell theme={null}
restatectl partitions list
```
On this node, configure a local destination for the partition snapshot repository - make sure this already exists:
```toml theme={null}
[worker.snapshots]
destination = "file:///mnt/restate-data/snapshots-repository"
```
Restart the node. If you have multiple nodes which may assume leadership for this partition, you will need to either repeat this on all of them, or temporarily shut them down. Create snapshot(s) for the affected partition(s):
```shell theme={null}
restatectl snapshots create-snapshot {partition_id}
```
Copy the contents of the snapshots repository to the node experiencing issues, and configure it to point to the snapshot repository. If you have multiple snapshots produced by multiple peer nodes, you can merge them all in the same location - each partition's snapshots will be written to dedicated sub-directory for that partition.
##### 3. Confirm that the affected node starts up and bootstraps its partition store from a snapshot
Once you have confirmed that a snapshot for the partition is available at the configured location, the configured repository access credentials have the necessary permissions, and the local node configuration is correct, you should see the partition processor start up and join the partition. If you have updated the Restate server configuration in the process, you should restart the server process to ensure that the latest changes are picked up.
# Restate Server Configuration
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/server/configuration
Configure the Restate Server.
The Restate Server has a wide range of configuration options to tune it according to your needs.
To learn about the configuration options, have a look at the [full configuration reference](/references/server-config).
## Configuration file
The Restate Server accepts a [TOML](https://toml.io/en/) configuration file that can be specified either providing the command-line option `--config-file=` or setting the environment variable `RESTATE_CONFIG=`. If not set, the [default configuration](/references/server-config#default-configuration) will be applied.
## Overrides
Restate server accepts a sub-set of the configuration through command-line arguments, you can see all available options by adding `--help` to `restate-server`.
The order of applying configuration layers follows the following order:
1. Built-in defaults
2. Configuration file (`--config-file` or via `RESTATE_CONFIG`)
3. Environment variables
4. Command line arguments (`--cluster-name=`)
Every layer overrides the previous. For instance, command-line arguments will override a configuration key supplied through environment variable (if set).
### Environment variables
You can override any configuration entry with an environment variable, this overrides values loaded from the configuration file. To do that, the following rule applies:
* Prefix the configuration entry key with `RESTATE_`
* Separate every nested struct with `__` (double underscore) and all hyphens `-` with a `_` (single underscore).
For example, to override the `admin.bind-address`, the corresponding environment variable is `RESTATE_ADMIN__BIND_ADDRESS`.
## Configuration introspection
If you want to generate a configuration file that includes values loaded from your environment variables or overrides applied to `restate-server` command-line, you can add `--dump-config` to dump the default TOML config with overrides applied:
```shell theme={null}
restate-server --cluster-name=mycluster --dump-config
```
Example output:
```toml theme={null}
roles = [
"worker",
"admin",
"metadata-server",
"log-server",
"http-ingress",
]
cluster-name = "mycluster"
...
```
At any time, you ask restate daemon to print the loaded configuration to the log by sending a `SIGUSR1` to the server process. This prints a dump of the live configuration to standard error.
For instance on Mac/Linux, you can find the PID of restate-server by running:
```shell theme={null}
pgrep restate-server
994921
```
Then send the signal to the process ID returned from `pgrep`'s output:
```shell theme={null}
kill -USR1 994921
```
Observe the output of the server for a dump of the configuration file contents.
# CDK for AWS
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/server/deploy/cdk
# Cluster
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/server/deploy/cluster
Deploy a distributed Restate cluster.
This page describes how you can deploy a distributed Restate cluster.
To understand the terminology used on this page, it might be helpful to read through the [architecture reference](/references/architecture).
To migrate an existing single-node deployment into a multi-node deployment without losing data, check out [this guide](/guides/local-to-replicated).
## Quickstart using Docker Compose
To run a Restate cluster locally, check out the [Restate cluster guide](/guides/cluster) for a Docker Compose ready-made example.
## Configuration
To deploy a distributed Restate cluster without external dependencies, you need to configure the following settings in your [server configuration](/server/configuration):
```toml restate.toml theme={null}
# Every node needs to have a unique node name
node-name = "UNIQUE_NODE_NAME"
# All nodes need to have the same cluster name
cluster-name = "CLUSTER_NAME"
# Make sure it does not conflict with the other nodes
advertised-address = "ADVERTISED_ADDRESS"
# At most one node can be configured with auto-provision = true
auto-provision = false
# Default replication factor for both the logs and the partitions.
#
# Replicate the data to 2 nodes. This requires that the cluster has at least 2 nodes to
# become operational. If the cluster has at least 3 nodes, then it can tolerate 1 node failure.
#
# This also controls the default partition replication. A value of 2 means each partition
# will be running on (max) of 2 nodes, ensuring redundancy and fault tolerance.
default-replication = 2
[bifrost]
# Only the replicated Bifrost provider can be used in a distributed deployment
default-provider = "replicated"
[metadata-server]
# To tolerate node failures, use the replicated metadata server
type = "replicated"
[metadata-client]
# List all the advertised addresses of the nodes that run the metadata-server role
addresses = ["ADVERTISED_ADDRESS", "ADVERTISED_ADDRESS_NODE_X"]
[admin]
# Make sure it does not conflict with the other nodes
bind-address = "ADMIN_BIND_ADDRESS"
[ingress]
# Make sure it does not conflict with other nodes
bind-address = "INGRESS_BIND_ADDRESS"
```
It is important that every Restate node you start has a unique `node-name` specified.
All nodes that are part of the cluster need to have the same `cluster-name` specified.
At most one node can be configured with `auto-provision = true`.
If no node is allowed to auto provision, then you have to manually provision the cluster.
Refer to the [Cluster provisioning](#cluster-provisioning) section for more information.
The log provider needs to be configured with `default-provider = "replicated"`.
The `default-replication` should be set to the number of nodes that the data should be replicated to.
If you run at least `2 * default-replication-property - 1` nodes, then the cluster can tolerate `default-replication-property - 1` node failures.
The metadata server type should be set to `replicated` to tolerate node failures.
Every node that runs the `metadata-server` role will join the metadata store cluster.
To tolerate `n` metadata node failures, you need to run at least `2 * n + 1` Restate nodes with the `metadata-server` role configured.
The `metadata-client` should be configured with the advertised addresses of all nodes that run the `metadata-server` role.
Every Restate node that runs the `worker` role will also run the ingress server and accept incoming invocations.
For those nodes that run on the same machine, make sure that the ports do not conflict.
[Snapshots](/server/snapshots) are essential to support safe log trimming and also allow you to set partition replication to a subset of all cluster nodes, while still allowing for fast partition fail-over to any live node. Snapshots are also necessary to add more nodes in the future.
If you plan to scale your cluster over time, we strongly recommend enabling [snapshotting](/server/snapshots#configuring-automatic-snapshotting).
Without it, newly added nodes may not be fully utilized by the system.
## Cluster provisioning
Once you start the node that is configured with `auto-provision = true`, it will provision the cluster so that other nodes can join.
The provision step initializes the metadata store and writes the initial `NodesConfiguration` with the initial cluster configuration to the metadata store.
In case none of the nodes is allowed to auto-provision, then you need to provision the cluster manually via `restatectl`.
```shell theme={null}
restatectl provision --address --yes
```
This provisions the cluster with default settings specified in the server configuration.
# null
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/server/deploy/kubernetes
Deploy the Restate Server on Kubernetes.
This page describes how to deploy the Restate Server on [Kubernetes](https://kubernetes.io/).
## Helm Chart
The recommended Kubernetes deployment strategy is a one-replica StatefulSet. We recommend installing Restate in its own
namespace. The easiest way to do this is with our [Helm chart](https://github.com/restatedev/restate/tree/main/charts/restate-helm):
```shell theme={null}
helm install restate oci://ghcr.io/restatedev/restate-helm --namespace restate --create-namespace
```
## Restate Kubernetes Operator
If you want to run multiple Restate clusters in Kubernetes, or want advanced functionality like managing versions of Restate SDK services, you can also use the [Restate Operator](https://github.com/restatedev/restate-operator). Details
of how to install it and deploy a cluster can be found in the [README](https://github.com/restatedev/restate-operator/blob/main/README.md).
# Logging
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/server/monitoring/logging
Configure logging for Restate Server.
By default, Restate logs INFO, WARN and ERROR events using a pretty format.
## Log filter
You can modify the filter used for log events setting the [configuration entry](/server/configuration) `log-filter`, or alternatively setting the `RUST_LOG` environment variable. Check the [`RUST_LOG` documentation](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html) for more details about the filter format. For example to enable debug logs on all Restate components, and info on other logs, set the filter as `info,restate=debug`. See the paragraph below [components and log events](#components-and-log-event-context-fields) for more details about filter targets.
## Log format
You can modify the log format by setting the [configuration entry](/server/configuration) `log-format` (or via `restate-server --log-format=`) as follows:
* `pretty`: Very verbose pretty format with rendered error descriptions, when available
* `compact`: Compact single line format
* `json`: Newline delimited json format
## Components and log event context fields
The following components are producing relevant logs:
* `restate_ingress_http`: The Restate component ingesting HTTP requests
* `restate_admin`: The component responsible for holding the metadata information and executing service discovery
* `restate_invoker`: The component interacting with deployed service deployments
* `restate_worker::partition::state_machine`: The state machine effects
* `restate_partition_store`: Restate partition storage layer
* `restate_bifrost`: Restate durable log layer
* `hyper`: The HTTP library
Most log events generated by Restate will also have the following attached attributes:
* `rpc.service`: The related service
* `rpc.method`: The related method
* `restate.invocation.id`: The [invocation identifier](/services/invocation/http#invocation-identifier)
## Recommendations
When testing Restate locally, we recommend keeping the default configuration.
When deploying in production, we recommend setting the log level to `info` and enabling the `json` format in conjunction with a log collector, so you can later inspect and filter logs based on its event fields.
We recommend to set up logging to a more verbose filter, and use the `pretty` format. Some example filters:
* `info,restate_ingress_http=trace,restate_invoker=trace,restate=debug,hyper=debug` for network related issues
* `info,restate_worker::partition::effects=debug` to get insights on the state machine effects
* `info,restate_admin=trace` to check service discovery and registration
# Metrics
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/server/monitoring/metrics
Expose Restate Server Prometheus metrics.
Restate servers expose operational metrics in [Prometheus exposition format](https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md) via the NodeCtl(port `5122`) endpoint, i.e. `localhost:5122/metrics`. For instance, configure Prometheus to scrape this endpoint every 30 seconds by adding this section to Prometheus configuration (assuming Restate server's IP address is `10.10.10.1` and accessible by Prometheus:
```yml theme={null}
scrape_configs:
- job_name: restate_server_1
metrics_path: "/metrics"
static_configs:
- targets:
- 10.10.10.1:5122
```
Note that some metrics are dependent on the value of `rocksdb-statistics-level` in the configuration file. In most cases, the default value will be sufficient for production deployment monitoring.
## Example Metrics
This is a non-exhaustive list of metrics that can be used to measure system performance:
* `restate_ingress_requests_total` (counter) - Number of ingress requests in different states (admitted, completed, throttled, etc.)
* `restate_ingress_request_duration_seconds` (summary) - Total latency of Ingress request processing in seconds
* `restate_rocksdb_estimate_live_data_size_bytes` (Gauge) - Size of the live data in RocksDb databases in bytes
* `restate_invoker_invocation_task_total` (counter) - The number of invocation tasks to user handlers
For example, we can use the following Prometheus queries to visualize throughput (ops/s) of HTTP ingress requests with an overlay of P99 latency:
```javascript theme={null}
rate(restate_ingress_requests_total{job="restate_server_1"}[$__rate_interval])
```
```javascript theme={null}
restate_ingress_request_duration_seconds{job="restate_server_1", quantile="0.99"}
```
# Tracing
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/server/monitoring/tracing
Export OTEL traces of your invocations.
Restate supports the following tracing features:
* Runtime execution tracing per invocation
* Exporting traces to OTLP-compatible systems (e.g. Jaeger)
* Correlating parent traces of incoming HTTP requests, using the [W3C TraceContext](https://github.com/w3c/trace-context) specification.
## Setting up OTLP exporter
Set up the [OTLP exporter](https://github.com/open-telemetry/opentelemetry-collector/blob/main/exporter/otlpexporter/README.md) by pointing the configuration entry `tracing-endpoint` to your trace collector.
By default, a `tracing-endpoint` using the `http://` or `https://` scheme will emit trace data in the OTLP/gRPC format.
Restate also supports the `otlp+http://` and `otlp+https://` schemes, to emit trace data in the OTLP/HTTP format.
Note that when using OTLP/HTTP, the `tracing-endpoint` URI usually needs to include an e.g. `/v1/traces` path.
### Exporting traces to Jaeger
[Jaeger](https://www.jaegertracing.io/docs/2.4/deployment/) accepts OTLP trace data on port `4317` (gRPC) and `4318` (HTTP).
Start Jaeger locally with Docker, for example:
```shell theme={null}
docker run -d --name jaeger \
-p 4317:4317 -p 4318:4318 -p 16686:16686 \
jaegertracing/jaeger:2.4.0
```
Configure the tracing endpoint in Restate as a valid URL:
```shell theme={null}
restate-server --tracing-endpoint http://localhost:4317 # for gRPC
restate-server --tracing-endpoint otlp+http://localhost:4318/v1/traces # for HTTP (note /v1/traces)
```
If you run Restate in Docker, then instead add the environment variable `-e RESTATE_TRACING_ENDPOINT=http://host.docker.internal:4317`.
If you now spin up your services and send requests to them, you will see the traces appear in the Jaeger UI at [http://localhost:16686](http://localhost:16686)
You can specify additional headers to be sent with the trace data by setting the `tracing-headers` configuration entry.
For example, to specify an `authorization` header add the following snippet to the [configuration file](/server/configuration/#configuration-file):
```toml theme={null}
[tracing-headers]
authorization = "Bearer some-auth-token"
```
You can configure a span/event filter in a similar fashion to the [log filter](/server/monitoring/logging#log-filter) setting the `tracing-filter` configuration entry.
If you can't configure a Jaeger agent, you can export traces by writing them to files, using the Jaeger JSON format.
To do so, set up the configuration entry `tracing-json-path` pointing towards the path where trace files should be written.
You can import the trace files using the Jaeger UI:
## Understanding traces
The traces contain detailed information about the context calls that were done during the invocation (e.g. sleep, one-way calls, interaction with state):
The initial `ingress_invoke` spans show when the HTTP request was received by Restate. The `invoke` span beneath it shows when Restate invoked the service deployment to process the request.
The tags of the spans contain the metadata of the context calls (e.g. call arguments, invocation id).
When a service invokes another service, the child invocation is linked automatically to the parent invocation, as you can see in the image.
Note that the spans of one-way calls are shown as separate traces. The parent invocation only shows that the one-way call was scheduled, not its entire tracing span.
To see this information, search for the trace of the one-way call by filtering on the invocation id tag `restate.invocation.id="inv_19maBIcE9uRD0gIu30mu6eqhZ4pQT"`.
## Searching traces
Traces export attributes and tags that correlate the trace with the service and/or invocation. For example, in the Jaeger UI, you can filter on the invocation id (`restate.invocation.id`) or any other tag:
# Snapshots & Backups
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/server/snapshots
Understanding and configuring snapshots and data backups in Restate clusters
Restate provides two different mechanisms for data persistence and recovery, each serving distinct purposes: snapshots and backups.
To understand the terminology used on this page, it might be helpful to read through the [architecture reference](/references/architecture).
## Overview
A Restate cluster maintains three essential types of state:
* **Metadata**: cluster membership as well as log and partition configuration
* **Logs**: The Bifrost log disseminates all events and state changes to partition workers
* **Partition store**: Stores ongoing invocations and their journals, persisted state, timers, deployments and more, for each partition
A disaster recovery and backup strategy must address all three plus the configuration of the cluster nodes.
### Snapshots
Internal mechanism for cluster operations and state sharing between nodes:
* **Goal**: Enable fast bootstrap of new nodes and support [log trimming](/server/snapshots#log-trimming-and-snapshots) in clusters
* **Scope**: A snapshot of the most recent state of a specific partition, produced by a fully caught up partition processor
* **When**: Essential for multi-node clusters; optional for single-node deployments
### Data Backups
Full copies of all data stored by Restate for disaster recovery:
* **Goal**: Restore a Restate Server to a previous point in time
* **Scope**: Complete copy of the `restate-data` directory or storage volumes
* **When**: Currently only for single-node deployments due to timing coordination challenges
Coordinating simultaneous backups across multiple nodes presents significant timing precision requirements. Even millisecond differences in backup timing can result in one node capturing state that's progressed further than another, creating inconsistent snapshots across the cluster. This timing skew leads to data inconsistencies that prevent successful cluster restoration from backup.
While atomically snapshotting restate-data at the volume level is still very useful as part of a broader disaster recovery and backup strategy, some manual repair work may be required to restore from such backups. There will also be some expected data loss between the latest LSN/point in time captured by the snapshot and the latest accepted/processed transaction by the cluster before it lost availability.
Since tooling for automated cluster restoration is not yet available, cluster-wide full node snapshots would currently require manual intervention to repair the system back into a workable state.
## When to Use Each
### Use Snapshots When:
* Operating a multi-node cluster (required)
* Adding or removing nodes from a cluster
* Enabling [log trimming](/server/snapshots#log-trimming-and-snapshots) to manage storage
* Supporting fast partition processor failover (having warm standbys ready for near-instant takeover)
* Growing the cluster or replacing completely failed nodes (newly added nodes can bootstrap from snapshots)
### Use Backups When:
* Doing point-in-time recovery of a single-node deployment
## Snapshots
[Snapshots](operate/data-backup#snapshots) are essential for multi-node cluster operations, enabling efficient state sharing and log management.
Snapshots are essential to support safe [log trimming](/server/snapshots#log-trimming-and-snapshots) and fast partition fail-over to a different cluster node. Snapshots are optional for single-node deployments and required for multi-node clusters.
Restate Partition Processors can be configured to periodically publish snapshots of their partition state to a shared S3-compatible object store.
Snapshots serve to allow nodes that do not have an up-to-date local copy of a partition's state to quickly start a processor for the given partition.
Without snapshots, trimming the log could lead to data loss if all the nodes replicating a particular partition are lost. Additionally, starting new partition processors would require the full replay of that partition's log which might take a long time.
When partition processors successfully publish a snapshot, this is reflected in the archived log sequence number (LSN). This value is the safe point up to which Restate can trim the Bifrost log.
### Configuring Automatic Snapshotting
Restate clusters should always be configured with a snapshot repository to allow nodes to efficiently share partition state, and for new nodes to be added to the cluster in the future.
Restate currently supports using Amazon S3 (or an API-compatible object store) as a shared snapshot repository.
To set up a snapshot destination, update your [server configuration](/server/configuration) as follows:
```toml theme={null}
[worker.snapshots]
destination = "s3://snapshots-bucket/cluster-prefix"
snapshot-interval-num-records = 10000
```
This enables automated periodic snapshots to be written to the specified bucket. You can also trigger snapshot creation manually using [`restatectl`](/installation#advanced%3A-operating-clusters-with-restatectl):
```shell theme={null}
restatectl snapshots create-snapshot --partition-id
```
We recommend testing the snapshot configuration by requesting a snapshot and examining the contents of the bucket.
You should see a new prefix with each partition's id, and a `latest.json` file pointing to the most recent snapshot.
No additional configuration is required to enable restoring snapshots.
When partition processors first start up, and no local partition state is found, the processor will attempt to restore the latest snapshot from the repository.
This allows for efficient bootstrapping of additional partition workers.
For testing purposes, you can also use the `file://` protocol to publish snapshots to a local directory. This is mostly useful when experimenting with multi-node configurations on a single machine. The `file` provider does not support conditional updates, which makes it unsuitable for potentially contended operation.
### Object Store endpoint and access credentials
Restate supports Amazon S3 and S3-compatible object stores. In typical server deployments to AWS, the configuration will be automatically inferred. Object store locations are specified in the form of a URL where the scheme is `s3://` and the authority is the name of the *bucket*. Optionally, you may supply an additional path within the bucket, which will be used as a common prefix for all operations. If you need to specify a custom endpoint for S3-compatible stores, you can override the API endpoint using the `aws-endpoint-url` config key.
For typical server deployments in AWS, you might not need to set region or credentials at all when using Amazon S3 beyond setting the path. Restate's object store support uses the conventional [AWS SDKs and Tools](https://docs.aws.amazon.com/sdkref/latest/guide/file-format.html) credentials discovery. We strongly recommend against using long-lived credentials in configuration. For development, you can use short-term credentials provided by a profile.
#### Local development with Minio
Minio is a common target while developing locally. You can configure it as follows:
```toml theme={null}
[worker.snapshots]
destination = "s3://bucket/cluster-name"
snapshot-interval-num-records = 1000
aws-region = "local"
aws-access-key-id = "minioadmin"
aws-secret-access-key = "minioadmin"
aws-endpoint-url = "http://localhost:9000"
aws-allow-http = true
```
#### Local development with S3
Assuming you have a profile set up to assume a specific role granted access to your bucket, you can work with S3 directly using a configuration like:
```toml theme={null}
[worker.snapshots]
destination = "s3://bucket/cluster-name"
snapshot-interval-num-records = 1000
aws-profile = "restate-dev"
```
This assumes that in your `~/.aws/config` you have a profile similar to:
```
[profile restate-dev]
source_profile = ...
region = us-east-1
role_arn = arn:aws:iam::123456789012:role/restate-local-dev-role
```
### Log trimming and Snapshots
In a distributed environment, the shared log is the mechanism for replicating partition state among nodes.
Therefore it is critical to that all cluster members can get all the relevant events recorded in the log, even newly built nodes that will join the cluster in the future.
This requirement is at odds with an immutable log growing unboundedly. Snapshots enable log trimming - the process of removing older segments of the log.
By default, Restate will attempt to trim logs once an hour which you can override or disable in the server configuration:
```toml theme={null}
[admin]
log-trim-check-interval = "1h"
```
This interval determines only how frequently the check is performed, and does not guarantee that logs will be trimmed. Restate will automatically determine the appropriate safe trim point for each partition's log.
In multi-node clusters, or any time a snapshot repository is configured, the log safe trim point is determined based on the archived LSN. If a snapshot repository is not configured, then archived LSNs are not reported. Therefore, on multi-node deployments, always make sure to configure a snapshot repository so that the log size can be kept in check. On single-node deployments without a snapshot repository, the log is trimmed based on the latest LSN durably committed to local storage. If you decide to expand a single-node into a multi-node cluster in the future, you will need to configure snapshotting for partition state transfer to the new nodes.
The presence of unreachable nodes in a cluster does not affect trimming, as long as the remaining nodes continue to produce snapshots. However, active partition processors that behind the archived LSN will cause trimming to be delayed to allow them to catch up.
Nodes that are temporarily down when the log is trimmed will use snapshots to fast-forward local state when they come back.
If you observe repeated `Shutting partition processor down because it encountered a trim gap in the log.` errors in the Restate server log, it is an indication that a processor is failing to start up due to missing log records. To recover, you must ensure that a snapshot repository is correctly configured and accessible from the node reporting errors. You can still recover even if no snapshots were taken previously as long as there is at least one healthy node with a copy of the partition data. In that case, you must first configure the existing node(s) to publish snapshots for the affected partition(s) to a shared destination. See the [Handling missing snapshots](/server/clusters#handling-missing-snapshots) section for detailed recovery steps.
### Observing processor persisted state
You can use [`restatectl`](/server/clusters#controlling-clusters-with-restatectl) to see the progress of partition processors with the `list` subcommand:
```shell theme={null}
restatectl partitions list
```
This will produce output similar to the below:
```
Alive partition processors (nodes config v6, partition table v2)
ID NODE MODE STATUS EPOCH APPLIED DURABLE ARCHIVED LSN-LAG UPDATED
0 N1:4 Leader Active e4 121428 121343 115779 0 268 ms ago
1 N1:4 Leader Active e4 120778 120735 116216 0 376 ms ago
2 N1:4 Leader Active e4 121348 121303 117677 0 394 ms ago
3 N1:4 Leader Active e4 120328 120328 117303 0 259 ms ago
4 N1:4 Leader Active e4 121108 120989 119359 0 909 ms ago
5 N1:4 Leader Active e4 121543 121481 119818 0 467 ms ago
6 N1:4 Leader Active e4 121253 121194 119568 0 254 ms ago
7 N1:4 Leader Active e4 120598 120550 118923 0 387 ms ago
```
There are three notable persistence-related attributes in `restatectl`'s partition list output:
* **Applied LSN** - the latest log record record applied by this processor
* **Durable LSN** - the log position of the latest partition store flushed to local node storage; by default processors optimize performance by relying on Bifrost for durability and only periodically flush partition store to disk
* **Archived LSN** - if a snapshot repository is configured, this LSN represents the latest published snapshot; this determines the log safe trim point in multi-node clusters
### Pruning the snapshot repository
Restate does not currently support pruning older snapshots from the snapshot repository. We recommend implementing an object lifecycle policy directly in the object store to manage retention.
## Data Backups
Data backups are primarily used for single-node Restate deployments.
### What does a backup contain?
The Restate server persists both metadata (such as the details of deployed services, in-flight invocations) and data (e.g., virtual object and workflow state keys) in its data store, which is located in its base directory (by default, the `restate-data` path relative to the startup working directory). Restate is configured to perform write-ahead logging with fsync enabled to ensure that effects are fully persisted before being acknowledged to participating services.
Backing up the full contents of the Restate base directory will ensure that you can recover this state in the event of a server failure. We recommend placing the data directory on fast block storage that supports atomic snapshots, such as Amazon EBS volume snapshots. Alternatively, you can stop the restate-server process, archive the base directory contents, and then restart the process. This ensures that the backup contains an atomic view of the persisted state.
In addition to the data store, you should also make sure you have a back up of the effective Restate server configuration. Be aware that this may be spread across command line arguments, environment variables, and the server configuration file.
### Restoring Backups
To restore from backup, ensure the following:
* Use a Restate server release that is compatible with the version that produced the data store snapshot. See the [Upgrading](/server/upgrading) section.
* Use an equivalent [Restate server configuration](/server/configuration). In particular, ensure that the `cluster-name` and `node-name` attributes match those of the previous Restate server operating on this data store.
* Exclusive access to a data store restored from the most recent atomic snapshot of the previous Restate installation.
Restate cannot guarantee that it is the only instance of the given node. You must ensure that only one instance of any given Restate node is running when restoring the data store from a backup. Running multiple instances could lead to a "split-brain" scenario where different servers process invocations for the same set of services, causing state divergence.
# Upgrading Restate
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/server/upgrading
Version upgrades of Restate Server, compatibility policy, and rollback strategy
Restate follows [Semantic Versioning](https://semver.org/). The server persists compatibility markers which enable it to detect incompatible data versions. However, you should be careful to follow supported version migration paths and perform [data backups](/server/snapshots#data-backups) when performing software upgrades.
## Version compatibility
Consult the release notes for the specific details of any new version when planning upgrades. As with all critical infrastructure changes, we recommend that you always verify the upgrade path in an isolated test environment first.
Upgrading to the latest patch version should always be possible and is recommended to benefit from the latest bug fixes and enhancements.
Incremental minor version upgrades will retain functional compatibility with the immediate prior version. That is, for any minor version update, you will be able to upgrade from `x.y` to `x.(y+1)` while retaining all persisted data and metadata. You must not skip minor version upgrades, i.e. go directly from `x.y` to `x.(y+2)`, as it may bypass necessary data store migrations required for preserving forward compatibility.
Since later versions may introduce new functionality that is on by default, it's crucial that you baseline your configuration on the release from which you will be upgrading. If you haven't done so already, be sure to capture the [effective runtime configuration](/server/configuration/#configuration-introspection) of your existing Restate cluster on the original version, and use this configuration initially when you upgrade.
If you encounter any issues with a new version, you can downgrade a Restate installation to the latest patch level of the previous minor version. For example, you can safely rollback the Restate server version from `x.y.0` to `x.(y-1).z` if you encounter any issues. However, rollback is only supported as long as you have not used any new features exclusive to the newer version. You should enable new features in the server configuration only after you have verified the overall system behaviour on the new version. Going back to more than one minor version behind to the most recent version used with the data store is not supported.
## Service compatibility
Registered Restate services must use an SDK compatible with the service protocol version(s) of the running Restate server. Note that Restate SDKs follow independent versioning from the server. You can find the latest SDK compatibility matrix in the respective SDK version documentation.
* [Java SDK](https://github.com/restatedev/sdk-java#versions)
* [TypeScript SDK](https://github.com/restatedev/sdk-typescript#versions)
* [Go SDK](https://github.com/restatedev/sdk-go#versions)
* [Python SDK](https://github.com/restatedev/sdk-python#versions)
* [Rust SDK](https://github.com/restatedev/sdk-rust#versions)
# Service Configuration
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/services/configuration
Configure service-level behavior like retries, timeouts, retention, and privacy.
Restate services support several configuration options to adapt their behavior for different use cases.
These include timeout settings, retry policies, retention policies, and privacy controls.
## Retries
Restate retries failed invocations according to a retry policy.
The retry policy always uses exponential back-off. You can tune the initial interval, the exponentiation factor and the maximum interval.
The retry policy can set the maximum number of attempts, which can be either limited or unlimited.
When attempts are exhausted, you can configure what Restate should do with the invocation:
* **Pause** it, requiring the user to [**manually resume it**](/services/invocation/managing-invocations#resume).
* **Kill** it, which automatically fails the invocation and responds to the caller with a terminal error. Note: **compensation logic will not be executed**, so this may leave your service in an inconsistent state. For more details, check [killing the invocation](/services/invocation/managing-invocations#kill).
**Default:** Unlimited retries with exponential policy up to 10 seconds of interval (tunable in self-hosted Restate as [`default-retry-policy`](/references/server-config#param-default-retry-policy))
**When to adjust:** To bound the maximum number of attempts when running on FaaS, avoiding costs for unnecessary retries. For example, when an invocation constantly fails on the same bug. Use longer retry intervals when you want to reduce the load on downstream services, or shorter intervals when you want snappier retries.
[Learn how to configure this](#how-to-configure)
## Retention of completed invocations
You can tune the retention period of the metadata and journal of completed invocations.
Decrease the retention period to save disk space. Increase it to preserve a longer history of your invocations.
How long Restate retains idempotency keys to prevent duplicate request processing.
You can see the status of the completed invocation in the UI.
**Default:** 24 hours
**When to adjust:** For services that need longer idempotency windows, such as financial transactions or critical business operations.
The workflow retention describes for how long you will be able to call the shared handlers on a workflow after the run handler has finished.
The retention time starts counting down from the moment the run handler completes.
After this time, the workflow's K/V state is cleared, you cannot call any shared handlers anymore, and any [Durable Promises](/develop/ts/external-events#durable-promises) are discarded.
**Default:** 24 hours
**When to adjust:** For workflows that need longer retention windows, to retrieve state or the result of a [Durable Promise](/develop/ts/external-events#durable-promises).
How long Restate keeps invocation journals after completion for debugging and auditing.
Increase to see journals of completed invocations in the log.
For workflow executions or invocations with idempotency keys:
* the journal retention is capped by the workflow retention or idempotency retention time
* If the journal retention is set to 0, you can see completion status but not journal entries.
**Default:** 24 hours (tunable in self-hosted Restate as [`default-journal-retention`](/references/server-config#param-default-journal-retention))
**When to adjust:** For workflows, AI agents, or any service where you need to inspect the execution history after completion.
[Learn how to configure these](#how-to-configure)
## Timeouts
There are two types of timeouts describing the behavior between Restate and the service deployment.
You should tune these values if you have long-running operations in your code.
The maximum time Restate waits for new journal entries from a service before Restate considers it stalled.
After this timeout, Restate will ask the service to suspend.
**Default**: 1 minute (tunable in self-hosted Restate as [`worker.invoker.inactivity-timeout`](/references/server-config#param-inactivity-timeout))
**When to adjust:** For long-running operations like LLM calls, long-running tasks, or external API calls that take longer than the default timeout.
Once the inactivity timeout is reached, Restate will wait for the abort timeout before interrupting the user code.
So this is the maximum time Restate will wait for a service to suspend before it forcibly aborts the execution.
**Default**: 1 minute (tunable in self-hosted Restate as [`worker.invoker.abort-timeout`](/references/server-config#param-abort-timeout))
**When to adjust:** For long-running operations like LLM calls, long-running tasks, or external API calls that take longer than the default timeout.
[Learn how to configure these](#how-to-configure)
## Private services
Services can receive requests from HTTP, Kafka and via other services.
You can mark services as private to prevent receiving requests from HTTP and Kafka, while allowing internal service-to-service communication.
**Default**: Public
**When to adjust:** When you want to forbid requests from 3rd party services through the Restate [HTTP API](/services/invocation/http), e.g. for an internal utility service that shouldn't be exposed externally, or for a backend service that only other Restate services should access.
[Learn how to configure this](#how-to-configure)
## State
Virtual Objects and Workflow allow to store [persistent state](/foundations/key-concepts#consistent-state). You can tune the state access behavior in your services and handlers.
To access the Virtual object/Workflow embedded K/V store, there are two strategies:
* Eager: A full snapshot of all K/V entries of the invoked Virtual Object/Workflow instance is sent when an invocation is invoked.
* Lazy: Each K/V entry is individually loaded when read through `ctx.get`. On Lambda and other FaaS platforms that doesn't support bidirectional streaming, this requires the invocation to suspend and replay.
**Default**: Eager
**When to adjust:** Enable lazy state when you have potentially large state entries, but your handler doesn't get all of them. Enable eager state if your state is small, or you're on AWS Lambda and the cost of replay is too high.
[Learn how to configure this](#how-to-configure)
## How to configure
### Service-level configuration
To configure your services:
```ts TypeScript {"CODE_LOAD::ts/src/services/configuration/my_advanced_service.ts#options"} theme={null}
// Add service options to the service definition
const myWorkflow = restate.workflow({
name: "MyWorkflow",
handlers: {
run: async (ctx: restate.Context) => {},
},
options: {
retryPolicy: {
initialInterval: { seconds: 1 },
maxInterval: { seconds: 30 },
maxAttempts: 10,
onMaxAttempts: "pause",
},
abortTimeout: { minutes: 15 },
inactivityTimeout: { minutes: 15 },
idempotencyRetention: { days: 3 },
workflowRetention: { days: 3 }, // only for workflows
journalRetention: { days: 7 },
ingressPrivate: true,
enableLazyState: true, // only for Virtual Objects and Workflows
},
});
```
```java Java {"CODE_LOAD::java/src/main/java/services/configuration/MyWorkflow.java#options"} theme={null}
// Specify service options when binding them to an endpoint
RestateHttpServer.listen(
Endpoint.bind(
new MyWorkflow(),
conf ->
conf.invocationRetryPolicy(
InvocationRetryPolicy.builder()
.initialInterval(Duration.ofSeconds(1))
.maxInterval(Duration.ofSeconds(1))
.maxAttempts(10)
.onMaxAttempts(InvocationRetryPolicy.OnMaxAttempts.PAUSE))
.abortTimeout(Duration.ofMinutes(15))
.inactivityTimeout(Duration.ofMinutes(15))
.idempotencyRetention(Duration.ofDays(3))
.workflowRetention(Duration.ofDays(10)) // Only for workflows
.journalRetention(Duration.ofDays(7))
.ingressPrivate(true)
.enableLazyState(true)));
```
```python Python {"CODE_LOAD::python/src/services/configuration.py#options"} theme={null}
# Specify service options when you create them
my_workflow = restate.Workflow(
"MyWorkflow",
inactivity_timeout=timedelta(minutes=15),
abort_timeout=timedelta(minutes=15),
idempotency_retention=timedelta(days=3),
journal_retention=timedelta(days=7),
ingress_private=True,
enable_lazy_state=True, # only for Objects/Workflows
invocation_retry_policy=restate.InvocationRetryPolicy(
initial_interval=timedelta(seconds=1),
max_interval=timedelta(seconds=30),
max_attempts=10,
on_max_attempts="pause"
)
)
```
```go Go {"CODE_LOAD::go/services/configuration.go#options"} theme={null}
// Specify service options when binding them to an endpoint
if err := server.NewRestate().
Bind(
restate.Reflect(
MyWorkflow{},
restate.WithInvocationRetryPolicy(
restate.WithInitialInterval(time.Second),
restate.WithMaxInterval(30*time.Second),
restate.WithMaxAttempts(10),
restate.PauseOnMaxAttempts()),
restate.WithInactivityTimeout(15*time.Minute),
restate.WithAbortTimeout(15*time.Minute),
restate.WithIdempotencyRetention(3*24*time.Hour),
restate.WithJournalRetention(7*24*time.Hour),
restate.WithIngressPrivate(true),
restate.WithEnableLazyState(true),
restate.WithWorkflowRetention(10*24*time.Hour), // Only for workflows
),
).
Start(context.Background(), "0.0.0.0:9080"); err != nil {
log.Fatal(err)
}
```
### Handler-level configuration
To set configuration at the handler level:
```ts TypeScript {"CODE_LOAD::ts/src/services/configuration/my_advanced_service.ts#handleropts"} theme={null}
// Add handler options to the handler definition
// For services:
const myService = restate.service({
name: "MyService",
handlers: {
myHandler: restate.handlers.handler(
{
retryPolicy: {
initialInterval: { seconds: 1 },
maxInterval: { seconds: 30 },
maxAttempts: 10,
onMaxAttempts: "pause",
},
abortTimeout: { minutes: 15 },
inactivityTimeout: { minutes: 15 },
idempotencyRetention: { days: 3 },
journalRetention: { days: 7 },
ingressPrivate: true,
},
async (ctx: restate.Context) => {}
),
},
});
// For Virtual Objects:
const myObject = restate.object({
name: "MyObject",
handlers: {
myHandler: restate.handlers.object.exclusive(
{
retryPolicy: {
initialInterval: { seconds: 1 },
maxInterval: { seconds: 30 },
maxAttempts: 10,
onMaxAttempts: "pause",
},
abortTimeout: { minutes: 15 },
inactivityTimeout: { minutes: 15 },
idempotencyRetention: { days: 3 },
journalRetention: { days: 7 },
ingressPrivate: true,
enableLazyState: true,
},
async (ctx: restate.ObjectContext) => {}
),
mySharedHandler: restate.handlers.object.shared(
{
/*... my options ...*/
},
async (ctx: restate.ObjectSharedContext) => {}
),
},
});
// For Workflows:
const myWf = restate.workflow({
name: "MyWf",
handlers: {
run: restate.handlers.workflow.workflow(
{
retryPolicy: {
initialInterval: { seconds: 1 },
maxInterval: { seconds: 30 },
maxAttempts: 10,
onMaxAttempts: "pause",
},
abortTimeout: { minutes: 15 },
inactivityTimeout: { minutes: 15 },
journalRetention: { days: 7 },
ingressPrivate: true,
enableLazyState: true,
},
async (ctx: restate.WorkflowContext) => {}
),
signal: restate.handlers.workflow.shared(
{
/*... my options ...*/
},
async (ctx: restate.WorkflowSharedContext) => {}
),
},
});
```
```java Java {"CODE_LOAD::java/src/main/java/services/configuration/MyWorkflow.java#handleropts"} theme={null}
// Or specify handler options when binding their service to an endpoint
RestateHttpServer.listen(
Endpoint.bind(
new MyWorkflow(),
conf ->
conf.configureHandler(
"run",
handlerConf ->
handlerConf
.invocationRetryPolicy(
InvocationRetryPolicy.builder()
.initialInterval(Duration.ofSeconds(1))
.maxInterval(Duration.ofSeconds(1))
.maxAttempts(10)
.onMaxAttempts(InvocationRetryPolicy.OnMaxAttempts.PAUSE))
.abortTimeout(Duration.ofMinutes(15))
.inactivityTimeout(Duration.ofMinutes(15))
.journalRetention(Duration.ofDays(7))
.ingressPrivate(true)
.enableLazyState(true))));
```
```python Python {"CODE_LOAD::python/src/services/configuration.py#handleropts"} theme={null}
# Specify handler options via the decorator
@my_workflow.main(
inactivity_timeout=timedelta(minutes=15),
abort_timeout=timedelta(minutes=15),
workflow_retention=timedelta(days=3),
# -> or idempotency_retention for Services/Objects
journal_retention=timedelta(days=7),
ingress_private=True,
enable_lazy_state=True, # only for Objects/Workflows
invocation_retry_policy=restate.InvocationRetryPolicy(
initial_interval=timedelta(seconds=1),
max_interval=timedelta(seconds=30),
max_attempts=10,
on_max_attempts="pause"
)
)
async def run(ctx: restate.WorkflowContext, req: str) -> str:
# ... implement workflow logic here ---
return "success"
```
```go Go {"CODE_LOAD::go/services/configuration.go#handleropts"} theme={null}
// Use ConfigureHandler to customize a handler configuration
if err := server.NewRestate().
Bind(
restate.Reflect(MyWorkflow{}).
ConfigureHandler("MyHandler",
restate.WithInvocationRetryPolicy(
restate.WithInitialInterval(time.Second),
restate.WithMaxInterval(30*time.Second),
restate.WithMaxAttempts(10),
restate.PauseOnMaxAttempts()),
restate.WithInactivityTimeout(15*time.Minute),
restate.WithAbortTimeout(15*time.Minute),
restate.WithIdempotencyRetention(3*24*time.Hour),
restate.WithJournalRetention(7*24*time.Hour),
restate.WithIngressPrivate(true),
restate.WithEnableLazyState(true),
restate.WithWorkflowRetention(10*24*time.Hour), // Only for workflows
),
).
Start(context.Background(), "0.0.0.0:9080"); err != nil {
log.Fatal(err)
}
```
### Override configuration
You can override the service options configured in your code via the UI or CLI.
These options apply until the next revision of the service is registered.
Use the [Restate UI](/installation#restate-ui) to configure services interactively:
* Navigate to your registered deployment
* Click on the service you want to configure
* Modify settings through the web interface
Use the Restate CLI to edit the configuration:
```bash theme={null}
restate services config edit
```
# Cloudflare Workers
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/services/deploy/cloudflare-workers
Run your TypeScript services on Cloudflare Workers
Deploy your Restate services on Cloudflare Workers.
This guide covers project configuration, service registration, and security setup.
Follow the [quickstart](/quickstart#cloudflare-workers) to try Cloudflare Workers and Restate locally.
## Set up your project
Start with the [cloudflare-workers-template](https://github.com/restatedev/cloudflare-workers-template) and follow its README.
Import the Restate SDK with the `fetch` component:
```typescript theme={null}
import * as restate from "@restatedev/restate-sdk-cloudflare-workers/fetch";
// Export the Restate handler
export default {
fetch: restate.createEndpointHandler({ services: [greeter] })
};
```
Make sure your `wrangler.toml` sets the `nodejs_compat` flag and enables preview urls:
```toml {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/templates/cloudflare-worker/wrangler.toml"} theme={null}
# The name of your Cloudflare Worker project
name = "restate-cloudflare-worker"
# Entrypoint
main = "./src/index.ts"
# Preview URLs are used to register to Restate
preview_urls = true
# Enable NodeJS compatibility (used by the SDK for the Node Buffer API)
compatibility_date = "2025-09-23"
compatibility_flags = [ "nodejs_compat" ]
```
## Local development
A Workers dev server can be started on port 9080 using:
```shell theme={null}
wrangler dev --port 9080
```
Register the service with Restate:
```shell theme={null}
npx @restatedev/restate deployments register --use-http1.1 http://localhost:9080
```
`wrangler` only supports HTTP/1.1 when running locally, so the `--use-http1.1` flag is required. This flag is not needed when deploying to Cloudflare Workers.
## Register the service to Restate
After deploying to Cloudflare Workers, [register the service](/services/versioning) with Restate CLI or UI providing [Preview URLs](https://developers.cloudflare.com/workers/configuration/previews/) to target specific service versions:
```shell theme={null}
npx @restatedev/restate deployments register \
https://-..workers.dev
```
You're set up. Head over to the **Overview page > Greeter > Playground** and start sending requests to your service.
## Restate identity keys (for Restate Cloud)
To allow only a specific Restate Cloud environment to send requests to your Cloudflare Workers deployment,
head over to your Restate Cloud Dashboard to set up Restate identity keys.
Set up Restate identity keys
## CI/CD Automation
To automatically deploy to Cloudflare Workers on each git commit to `main` and automatically register new [Restate service versions](/services/versioning):
```yml theme={null}
name: Deploy Worker
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v5
- run: npm ci
- name: Build & Deploy Worker
uses: cloudflare/wrangler-action@v3
id: deploy
with:
# Check https://developers.cloudflare.com/workers/ci-cd/external-cicd/github-actions/
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
# Add a message to the deployment with the commit
command: versions upload --message "Deployed via GitHub Actions - commit ${{ github.sha }}"
- name: Get deployment URL
id: get-url
env:
WRANGLER_OUTPUT: ${{ steps.deploy.outputs.command-output }}
run: |
URL=$(echo "$WRANGLER_OUTPUT" | awk '/Version Preview URL:/ {gsub(/.*Version Preview URL: /, ""); print}')
echo "deployment-url=$URL" >> $GITHUB_OUTPUT
- name: Register Restate deployment
env:
RESTATE_ADMIN_URL: ${{ secrets.RESTATE_ADMIN_URL }}
RESTATE_AUTH_TOKEN: ${{ secrets.RESTATE_AUTH_TOKEN }}
run: npx -y @restatedev/restate deployment register -y ${{ steps.get-url.outputs.deployment-url }}
```
Make sure your Cloudflare Worker project exists. If it doesn't, create it with: `npx wrangler deploy`
For this workflow to execute, you need to add the following [**GitHub Actions repository secrets**](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets):
* `CLOUDFLARE_ACCOUNT_ID`: Your Cloudflare Account. To get your account id, check [https://developers.cloudflare.com/workers/ci-cd/external-cicd/github-actions/#cloudflare-account-id](https://developers.cloudflare.com/workers/ci-cd/external-cicd/github-actions/#cloudflare-account-id)
* `CLOUDFLARE_API_TOKEN`: Your Cloudflare API token. To get a token, check [https://developers.cloudflare.com/workers/ci-cd/external-cicd/github-actions/](https://developers.cloudflare.com/workers/ci-cd/external-cicd/github-actions/)
* `RESTATE_ADMIN_URL`: The Admin URL. You can find it in [Developers > Admin URL](https://cloud.restate.dev/to/developers/integration#admin)
* `RESTATE_AUTH_TOKEN`: Your Restate Cloud auth token. To get one, go to [Developers > API Keys > Create API Key](https://cloud.restate.dev?createApiKey=true\&createApiKeyDescription=deployment-key\&createApiKeyRole=rst:role::AdminAccess), and make sure to select **Admin** for role
You can use this workflow with Self-hosted Restate as well,
just make sure to correctly set up `RESTATE_AUTH_TOKEN` and `RESTATE_ADMIN_URL` to reach your Restate cluster.
# Deno Deploy
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/services/deploy/deno-deploy
Run your TypeScript services on Deno Deploy
export const CustomVars = ({name}) => {
const RESTATE_VERSION = "1.4";
const TYPESCRIPT_SDK_VERSION = "1.7.2";
const JAVA_SDK_VERSION = "2.4.0";
const GO_SDK_VERSION = "0.18.0";
const PYTHON_SDK_VERSION = "0.8.0";
const RUST_SDK_VERSION = "0.6.0";
const mapping = {
RESTATE_VERSION,
TYPESCRIPT_SDK_VERSION,
JAVA_SDK_VERSION,
GO_SDK_VERSION,
PYTHON_SDK_VERSION,
RUST_SDK_VERSION,
JAVA_HTTP_DEPENDENCY: `dev.restate:sdk-java-http:${JAVA_SDK_VERSION}`,
KOTLIN_HTTP_DEPENDENCY: `dev.restate:sdk-kotlin-http:${JAVA_SDK_VERSION}`,
JAVA_LAMBDA_DEPENDENCY: `dev.restate:sdk-java-lambda:${JAVA_SDK_VERSION}`,
KOTLIN_LAMBDA_DEPENDENCY: `dev.restate:sdk-kotlin-lambda:${JAVA_SDK_VERSION}`,
JAVA_SDK_REQUEST_IDENTITY: `dev.restate:sdk-request-identity:${JAVA_SDK_VERSION}`,
JAVA_TESTING: `dev.restate:sdk-testing:${JAVA_SDK_VERSION}`,
JAVA_CLIENT: `dev.restate:client:${JAVA_SDK_VERSION}`,
KOTLIN_CLIENT: `dev.restate:client-kotlin:${JAVA_SDK_VERSION}`,
DENO_FETCH: `npm:@restatedev/restate-sdk@^${TYPESCRIPT_SDK_VERSION}/fetch`
};
return mapping[name];
};
Deploy your Restate services on Deno Deploy.
This guide covers project configuration, service registration, and security setup.
Follow the [quickstart](/quickstart#deno) to try Deno and Restate locally.
## Set up your project
Start with the [deno-template](https://github.com/restatedev/examples/tree/main/typescript/templates/deno) and follow its README.
[](https://console.deno.com/new?clone=https://github.com/restatedev/deno-template)
Import the Restate SDK with the `fetch` component:
```typescript theme={null}
import * as restate from "@restatedev/restate-sdk/fetch";
// Create the Restate endpoint
// Here you need to register your services
const handler = restate.createEndpointHandler({
services: [greeter],
bidirectional: true,
});
// Serve using Deno
Deno.serve({ port: 9080 }, handler);
```
## Register the service to Restate
Once deployed on Deno Deploy EA, [register the service](/services/versioning) with Restate using the CLI or UI. Use **Preview URLs** so that Restate can target specific Deno deployments:
```shell theme={null}
npx @restatedev/restate deployments register \
https://your-project-name-.deno.dev
```
You're set up. Head over to the **Overview page > Greeter > Playground** and start sending requests to your service.
## Restate identity keys (for Restate Cloud)
In order to make sure only a specific Restate Cloud environment can push requests to your Deno Deploy deployment,
head over to your Restate Cloud Dashboard to set up Restate identity keys.
Set up Restate identity keys
## CI/CD Automation
You can set up automation to register new [Restate service versions](/services/versioning) every time you deploy to Deno Deploy:
```yml theme={null}
name: Register to Restate
on:
repository_dispatch:
types: [deno_deploy.build.routed] # Listen for successful builds
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Register Restate deployment
env:
RESTATE_ADMIN_URL: ${{ secrets.RESTATE_ADMIN_URL }}
RESTATE_AUTH_TOKEN: ${{ secrets.RESTATE_AUTH_TOKEN }}
run: npx -y @restatedev/restate deployment register -y ${{ github.event.client_payload.revision.preview_url }}
```
For this workflow to execute, you need to add the following [**GitHub Actions repository secrets**](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets):
* `RESTATE_ADMIN_URL`: The Admin URL. You can find it in [Developers > Admin URL](https://cloud.restate.dev/to/developers/integration#admin)
* `RESTATE_AUTH_TOKEN`: Your Restate Cloud auth token. To get one, go to [Developers > API Keys > Create API Key](https://cloud.restate.dev/to/developers/integration?createApiKey=true\&createApiKeyDescription=deployment-key\&createApiKeyRole=rst:role::AdminAccess), and make sure to select **Admin** for the role
For [Deno Deploy Classic](https://docs.deno.com/deploy/classic/), we suggest the following CI/CD setup that deploys to Deno and then registers the deployment with Restate:
```yml theme={null}
name: Deploy Deno
on:
push:
branches:
- main
env:
# Create the Deno project going to https://dash.deno.com/new_project linking this repository.
# When creating the project, check **Just link the repo, I’ll set up GitHub Actions myself**.
# Make sure this project name matches the deno project.
DENO_PROJECT_NAME: ${{ vars.DENO_PROJECT_NAME }}
jobs:
deploy:
permissions:
id-token: write # required
contents: read
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
# Deploy using deployctl
- name: Deploy to Deno Deploy
uses: denoland/deployctl@v1
id: deploy
with:
project: ${{ env.DENO_PROJECT_NAME }}
entrypoint: main.ts
- name: Register Restate deployment
env:
RESTATE_ADMIN_URL: ${{ secrets.RESTATE_ADMIN_URL }}
RESTATE_AUTH_TOKEN: ${{ secrets.RESTATE_AUTH_TOKEN }}
# Revision URL https://docs.deno.com/deploy/classic/deployments/#production-vs.-preview-deployments
run: npx -y @restatedev/restate deployment register -y https://${{ env.DENO_PROJECT_NAME }}-${{ steps.deploy.outputs.deployment-id }}.deno.dev
```
You can use this workflow with Self-hosted Restate as well,
just make sure to correctly set up `RESTATE_AUTH_TOKEN` and `RESTATE_ADMIN_URL` to reach your Restate cluster.
# Standalone/Kubernetes
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/services/deploy/kubernetes
Learn how to run Restate applications on Kubernetes.
You can deploy your Restate service on any standalone machine.
## Running services behind a load balancer
If you want to spread load across multiple machines, it is best to put your services behind a load balancer (L4/L7).
If you want to run your service behind a load balancer, please make sure that you configure the load balancer to allow HTTP2 traffic.
## Deploying Restate services to Kubernetes
Service deployments can be deployed like any Kubernetes service; a Deployment of more than one replica is generally appropriate,
and a Service is used to provide a stable DNS name and IP for the pods.
If your services are running over HTTP2 (the default), each Restate partition will generally have only one TCP connection to a single destination pod.
However, because there are many partitions (by default 24, typically more in a larger cluster) your pods should get a reasonably
even distribution of traffic even without a L7 load balancing solution (Cilium, Istio etc).
### RestateDeployment CRD
The [Restate operator](https://github.com/restatedev/restate-operator) allows for the use of a `RestateDeployment` CRD to deploy your services.
The CRD is an extension of a native `Deployment` object, but will manage registration and [versioning](/services/configuration) for you, by keeping old
ReplicaSets around with an associated Service object so that in-flight invocations can drain against the old code versions. You can deploy a CRD as follows:
```yaml theme={null}
apiVersion: restate.dev/v1beta1
kind: RestateDeployment
metadata:
name: service
spec:
replicas: 1
restate:
register:
# set this if you want to register against your RestateCluster named 'restate'
# alternatively, you can set a url or a Service reference to register against
cluster: restate
selector:
matchLabels:
app: service
template:
metadata:
labels:
app: service
spec:
containers:
- name: service
image: path.to/yourrepo:yourtag
env:
- name: PORT
value: "9080"
ports:
- containerPort: 9080
name: restate
```
### Kubernetes Deployment and Service definition
If you want to deploy without the operator, a simple deployment setup with a single pod in Kubernetes is as follows:
```yaml theme={null}
apiVersion: apps/v1
kind: Deployment
metadata:
name: service
spec:
replicas: 1
selector:
matchLabels:
app: service
template:
metadata:
labels:
app: service
spec:
containers:
- name: service
image: path.to/yourrepo:yourtag
env:
- name: PORT
value: "9080"
ports:
- containerPort: 9080
name: restate
---
apiVersion: v1
kind: Service
metadata:
name: service
spec:
selector:
app: service
ports:
- port: 9080
name: restate
type: ClusterIP
```
You would then register the service at `http://.:9080`. Note however that this setup will not account for keeping around old code versions, so updating your code can break in-flight invocations.
### Knative
Restate supports Knative services. Knative allows scaling to zero when there are no in-flight invocations and automatically configures an L7 load balancer. There are no special requirements to deploy a service deployment container with Knative:
```shell theme={null}
$ kn service create service-name --port h2c:9080 --image path.to/yourrepo:yourtag
```
Or using the YAML manifest:
```yaml theme={null}
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: service-name
spec:
template:
spec:
containers:
- image: path.to/yourrepo:yourtag
ports:
- name: h2c
containerPort: 9080
```
The service will be accessible at `http://.` but to handle [versioning](/services/configuration), it is preferable to register the new revision url like `http://-0001.` as part of your deployment workflow.
By default Knative exposes the service through the Ingress. This is not required by Restate, and you can disable this behavior adding the argument `--cluster-local` to the aforementioned creation command.
# AWS Lambda
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/services/deploy/lambda
Run Restate services on AWS Lambda.
You can run your Restate services as serverless functions on [AWS Lambda](https://aws.amazon.com/lambda/).
The easiest way to run Restate handlers on AWS Lambda is to use the [Restate CDK construct library](https://github.com/restatedev/cdk).
To deploy a Restate service as a Lambda function, you can follow the [guidelines of AWS](https://docs.aws.amazon.com/lambda/latest/dg/typescript-package.html)
for deploying plain TypeScript NodeJS functions. Restate does not add any complexity to this.
Follow [the serving docs](/develop/ts/serving#creating-a-lambda-handler) to create a Lambda endpoint in your Restate service.
Build a zip file containing the application code and dependencies and upload this to AWS Lambda.
If you are using the Restate node template, then you can create a zip file with:
```shell theme={null}
npm run bundle
```
AWS Lambda assumes that the handler can be found under `index.handler` in the uploaded code.
By default, this is also the case for the Lambda functions developed with the Restate SDK.
[Register](/services/versioning#registering-deployments) the Lambda function ARN with Restate.
Make sure you first publish a new version of the Lambda function before registering it with Restate.
```shell theme={null}
restate deployments register arn:aws:lambda:region:account-id:function:function-name:version
```
# Vercel
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/services/deploy/vercel
Run your TypeScript services on Vercel
export const CustomVars = ({name}) => {
const RESTATE_VERSION = "1.4";
const TYPESCRIPT_SDK_VERSION = "1.7.2";
const JAVA_SDK_VERSION = "2.4.0";
const GO_SDK_VERSION = "0.18.0";
const PYTHON_SDK_VERSION = "0.8.0";
const RUST_SDK_VERSION = "0.6.0";
const mapping = {
RESTATE_VERSION,
TYPESCRIPT_SDK_VERSION,
JAVA_SDK_VERSION,
GO_SDK_VERSION,
PYTHON_SDK_VERSION,
RUST_SDK_VERSION,
JAVA_HTTP_DEPENDENCY: `dev.restate:sdk-java-http:${JAVA_SDK_VERSION}`,
KOTLIN_HTTP_DEPENDENCY: `dev.restate:sdk-kotlin-http:${JAVA_SDK_VERSION}`,
JAVA_LAMBDA_DEPENDENCY: `dev.restate:sdk-java-lambda:${JAVA_SDK_VERSION}`,
KOTLIN_LAMBDA_DEPENDENCY: `dev.restate:sdk-kotlin-lambda:${JAVA_SDK_VERSION}`,
JAVA_SDK_REQUEST_IDENTITY: `dev.restate:sdk-request-identity:${JAVA_SDK_VERSION}`,
JAVA_TESTING: `dev.restate:sdk-testing:${JAVA_SDK_VERSION}`,
JAVA_CLIENT: `dev.restate:client:${JAVA_SDK_VERSION}`,
KOTLIN_CLIENT: `dev.restate:client-kotlin:${JAVA_SDK_VERSION}`,
DENO_FETCH: `npm:@restatedev/restate-sdk@^${TYPESCRIPT_SDK_VERSION}/fetch`
};
return mapping[name];
};
Deploy your Restate services on Vercel.
This guide covers project configuration, service registration, and security setup.
Follow the [quickstart](/quickstart) to try Next.js and Restate locally.
## Set up your project
Start from the [vercel-template](https://github.com/restatedev/vercel-template).
Create a `fetch` endpoint, providing the services:
```typescript {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/templates/vercel/src/restate/serve.ts"} theme={null}
import * as restate from "@restatedev/restate-sdk/fetch";
import { greeter } from "@/restate/greeter";
// Create the Restate endpoint.
// Here you need to register your services
const endpoint = restate.createEndpointHandler({ services: [greeter] });
// Adapt it to Next.js route handlers
export const serve = () => {
return {
POST: (req: Request) => endpoint(req),
GET: (req: Request) => endpoint(req),
};
};
```
And expose the endpoint in a Next.js parameterized route, e.g. `/restate/[[...services]]`:
```typescript {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/templates/vercel/src/app/restate/%5B%5B...services%5D%5D/route.ts"} theme={null}
import { serve } from "@/restate/serve";
// This is the route that exposes the Restate services.
// You can register it to Restate using /restate subpath (check the README)
// To call it, open Restate > Overview > Greeter > Playground
export const { GET, POST } = serve();
```
## Configure Vercel project
With the GitHub repository set up, go over to [the Vercel dashboard](https://vercel.com/new) to create the project.
For Restate to push requests to the Vercel project, you need Vercel Generated URLs to be accessible by Restate.
You have two alternatives:
* Disable [Vercel Authentication](https://vercel.com/docs/security/deployment-protection/methods-to-protect-deployments/vercel-authentication) for the project, making its URLs publicly accessible.
* Enable [Protection Bypass for Automation](https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection/protection-bypass-automation). This makes the URLs accessible only if the `x-vercel-protection-bypass` header is provided with the right value.
## Register the service to Restate
In order for Restate to push requests to your services, you need to [register the service to Restate](/services/versioning) using the CLI or UI.
Make sure to register the [Commit URL](https://vercel.com/docs/deployments/generated-urls#generated-from-git) so that Restate can address specific Vercel deployments:
```shell theme={null}
npx @restatedev/restate deployments register \
--use-http1.1 \
https://--.vercel.app/restate
```
`--use-http1.1` is required to interact with Next.js projects.
If you set up **Protection Bypass for Automation** as described above, pass the additional header as an argument:
```shell theme={null}
npx @restatedev/restate deployments register \
--use-http1.1 \
--extra-header x-vercel-protection-bypass= \
https://--.vercel.app/restate
```
You're set up. Head over to the **Overview page > Greeter > Playground** and start sending requests to your service.
## Restate identity keys (for Restate Cloud)
In order to make sure only a specific Restate Cloud environment can push requests to your Vercel deployment,
head over to your Restate Cloud Dashboard to set up Restate identity keys.
Set up Restate identity keys
## CI/CD Automation
You can set up automation to automatically register new [Restate service versions](/services/versioning) every time a Vercel deployment gets promoted:
```yml theme={null}
name: Register to Restate
# React to vercel.deployment.promoted event
on:
repository_dispatch:
types:
- 'vercel.deployment.promoted'
jobs:
deploy-to-restate:
if: github.event_name == 'repository_dispatch'
runs-on: ubuntu-latest
steps:
- name: Register Restate deployment
env:
# In your Restate Cloud dashboard, go to Developers > API Keys > Create API Key, and make sure to select **Admin** for the role
RESTATE_ADMIN_URL: ${{ secrets.RESTATE_ADMIN_URL }}
# You can find this out in Developers > Admin URL.
RESTATE_AUTH_TOKEN: ${{ secrets.RESTATE_AUTH_TOKEN }}
# Run service registration
run: |
npx -y @restatedev/restate deployment register -y \
--use-http1.1 \
${{ secrets.VERCEL_PROTECTION_BYPASS_TOKEN && format('--extra-header x-vercel-protection-bypass={0}', secrets.VERCEL_PROTECTION_BYPASS_TOKEN) }} \
${{ github.event.client_payload.url }}/restate
```
For this workflow to execute, you need to add the following [**GitHub Actions repository secrets**](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets):
* `RESTATE_ADMIN_URL`: The Admin URL. You can find it in [Developers > Admin URL](https://cloud.restate.dev/to/developers/integration#admin)
* `RESTATE_AUTH_TOKEN`: Your Restate Cloud auth token. To get one, go to [Developers > API Keys > Create API Key](https://cloud.restate.dev/to/developers/integration?createApiKey=true\&createApiKeyDescription=deployment-key\&createApiKeyRole=rst:role::AdminAccess), and make sure to select **Admin** for the role
* If you set up the Protection Bypass for Automation as described above, add the secret `VERCEL_PROTECTION_BYPASS_TOKEN` with the token value
You can use this workflow with Self-hosted Restate as well,
just make sure to correctly set up `RESTATE_AUTH_TOKEN` and `RESTATE_ADMIN_URL` to reach your Restate cluster.
# Introspection
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/services/introspection
Inspect the status of invocations/services.
Restate exposes information on invocations and application state via its CLI and [Introspection SQL API](/references/sql-introspection). You can use this to gain insight into the status of invocations and the service state that is stored.
This can be useful for troubleshooting. For example, a Virtual Object might be blocked and you want to kill the invocation that is blocking it, but you don't know the invocation ID. Or you want to check what is currently stored in the state of a service.
You can inspect what is stored in Restate via the UI, via the [CLI](/installation#running-restate-server--cli-locally) (via commands or SQL queries), and via curl.
You can use the [UI](/installation#restate-ui) to debug your applications.
Have a look at the [UI announcement blog post](https://restate.dev/blog/announcing-restate-ui/) to get some inspiration on how you can use the UI for debugging and understanding your applications.
## SQL over the data in Restate
Restate exposes the following SQL tables:
* `sys_invocation`: to inspect invocations
* `sys_inbox`: to inspect queue of pending invocations
* `sys_keyed_service_status`: to inspect the status of a Virtual Object
* `sys_journal`: to inspect the invocations' journal
* `sys_service`: to inspect the registered services
* `sys_deployment`: to inspect service deployments
* `sys_idempotency`: to inspect idempotency keys
* `state`: to inspect application state
You can find the schema of each of the tables in the [references](/references/sql-introspection).
The Restate Introspection SQL API has been implemented based on [DataFusion](https://arrow.apache.org/datafusion/) and supports standard SQL syntax.
## Inspecting invocations
You can execute SQL queries via the CLI or over HTTP.
For each of the queries we will show the CLI command and the equivalent SQL query that you can execute via the CLI or over HTTP.
```shell CLI theme={null}
restate invocations list
```
```shell CLI-SQL theme={null}
restate sql --json "select * from sys_invocation;"
```
```shell curl theme={null}
curl localhost:9070/query --json '{ "query" : "select * from sys_invocation" }'
```
Restate only retains the entries for active invocations, workflows or invocations that were invoked with an idempotency key.
Active invocations are invocations that haven't completed yet and are either invoked or suspended.
For workflows and invocations that were invoked with an idempotency key, the entries are retained for their specified retention time.
The CLI command only shows the active invocations, not the completed ones. Use `--all` to see completed ones as well.
```shell CLI theme={null}
restate invocations describe my_invocation_id
```
```shell CLI-SQL theme={null}
restate sql --json "select * from sys_invocation where id = 'my_invocation_id';"
```
```shell curl theme={null}
curl localhost:9070/query --json '{ "query" : "select * from sys_invocation where id = 'my_invocation_id';" }'
```
The status is either:
* `pending`: enqueued waiting for its turn
* `ready`: ready to be processed, but not yet running
* `running`: actively processing
* `backing-off`: retrying due to a failure
* `suspended`: waiting on some external input (e.g. request-response call, awakeable, sleep, ...)
* `completed`: completed (this is shown only for idempotent invocations)
```shell CLI theme={null}
restate invocations describe my_invocation_id
```
```shell CLI-SQL theme={null}
restate sql --json "select * from sys_journal where id = 'my_invocation_id';"
```
```shell curl theme={null}
curl localhost:9070/query --json '{ "query" : "select * from sys_journal where id = 'my_invocation_id';" }'
```
You see the journal printed in the output.
To have a look at the invocations that are currently in a retry loop, you can execute:
```shell CLI theme={null}
restate invocations list --status backing-off
```
```shell CLI-SQL theme={null}
restate sql --json "select * from sys_invocation where retry_count > 1;"
```
```shell curl theme={null}
curl localhost:9070/query --json '{ "query" : "select * from sys_invocation where retry_count > 1;" }'
```
You can retrieve the invocation ID that is currently blocking a Virtual Object via:
```shell CLI theme={null}
restate invocations list --service MyService --key myKey
```
```shell CLI-SQL theme={null}
restate sql --json "select invocation_id from sys_keyed_service_status where service_name = 'test.MyServiceName' and service_key = 'myKey';"
```
```shell curl theme={null}
curl localhost:9070/query --json '{ "query" : "select invocation_id from sys_keyed_service_status where service_name = 'test.MyServiceName' and service_key = 'myKey';" }'
```
With the CLI, you can also drill down and list only invocations that are blocking any Virtual Object:
```shell theme={null}
restate invocations list --virtual-objects-only
```
Add `--key my-key` to list only invocations that are blocking a specific Virtual Object.
You can then use the invocation ID to [cancel the invocation](/services/invocation/managing-invocations#cancel).
```shell CLI theme={null}
restate invocations describe my_invocation_id
```
```shell CLI-SQL theme={null}
restate sql --json "select modified_at from sys_invocation where id = 'my_invocation_id';"
```
```shell curl theme={null}
curl localhost:9070/query --json '{ "query" : "select modified_at from sys_invocation where id = 'my_invocation_id';" }'
```
This includes any modification to the invocation "data", for example when the service last switched its status from `invoked` to `suspended`, or when the last journal entry was added.
To find out if an invocation was triggered via the ingress or by another service:
```shell CLI theme={null}
restate invocations describe my_invocation_id
```
```shell CLI-SQL theme={null}
restate sql --json "select invoked_by, invoked_by_service_name, invoked_by_id from sys_invocation where id = 'my_invocation_id';"
```
```shell curl theme={null}
curl localhost:9070/query --json '{ "query" : "select invoked_by, invoked_by_service_name, invoked_by_id from sys_invocation where id = 'my_invocation_id';" }'
```
With the CLI, you see the caller at the root of the tree in the invocation progress:
```shell theme={null}
🚂 Invocation Progress:
―――――――――――――――――――――――
[Ingress]
└──(this)─> Greeter/greet
```
For the SQL queries, the `invoked_by` field contains either `ingress` or `service`.
If the invocation was triggered by another service, then the fields `invoked_by_service_name` and `invoked_by_id` will supply more information about the invoking service.
```shell CLI theme={null}
restate invocations describe my_invocation_id
```
```shell CLI-SQL theme={null}
restate sql --json "select trace_id from sys_invocation where id = 'my_invocation_id';"
```
```shell curl theme={null}
curl localhost:9070/query --json '{ "query" : "select trace_id from sys_invocation where id = 'my_invocation_id';" }'
```
Afterwards, you can use this trace ID to [search for spans in Jaeger](/server/monitoring/tracing#searching-traces).
To list the oldest invocations that are not making progress:
```shell CLI theme={null}
restate invocations list --oldest-first --status pending,backing-off,suspended
```
```shell CLI-SQL theme={null}
restate sql --json "select * from sys_invocation where to_timestamp(modified_at) <= now() - interval '1' hour;"
```
```shell curl theme={null}
curl localhost:9070/query --json '{ "query" : "select * from sys_invocation where to_timestamp(modified_at) <= now() - interval '1' hour;" }'
```
Zombie invocations are invocations that are pinned to a specific deployment but that deployment was forcefully removed. You can list them by executing:
```shell CLI theme={null}
restate invocations list --zombie
```
```shell CLI-SQL theme={null}
restate sql --json "select * from sys_invocation where pinned_deployment_id = 'my_deployment_id';"
```
```shell curl theme={null}
curl localhost:9070/query --json '{ "query" : "select * from sys_invocation where pinned_deployment_id = 'my_deployment_id';" }'
```
For the SQL queries, you need to know the deployment ID of the deployment that was forcefully removed.
## Inspecting application state
### With the CLI
To retrieve the state of a specific service and service key, do:
```shell theme={null}
restate kv get
```
Example output:
```log theme={null}
🤖 State:
―――――――――
service counter
key bob
KEY VAL
seen 8
```
If the values are not JSON-encoded UTF-8 strings, then it is also possible to use the `--binary` flag,
and get the value as base64 encoded string.
### With SQL queries
You can query the application state via the `state` table.
```shell CLI-SQL theme={null}
restate sql --json "select * from state where service_name = 'test.MyServiceName' and service_key = 'myKey';"
```
```shell curl theme={null}
curl localhost:9070/query --json '{ "query" : "select * from state where service_name = 'test.MyServiceName' and service_key = 'myKey';" }'
```
If your state value is a regular string, then you can access its content in the column `value_utf8`.
To retrieve the state of a specific service name, service key and state key, do:
```shell CLI-SQL theme={null}
restate sql --json "select * from state where service_name = 'MyServiceName' and service_key = 'myKey' and key = 'myStateKey';"
```
```shell curl theme={null}
curl localhost:9070/query --json '{ "query" : "select * from state where service_name = 'MyServiceName' and service_key = 'myKey' and key = 'myStateKey';" }'
```
The state key is the name you used to store the state with the SDK. For example, the code snippet `ctx.set("count", 1)` stores `1` under the key `count`.
To join the `sys_invocation` and `state` table:
```shell CLI-SQL theme={null}
restate sql --json "select * from sys_invocation JOIN state on sys_invocation.target_service_name = state.service_name and sys_invocation.target_service_key = state.service_key;"
```
```shell curl theme={null}
curl localhost:9070/query --json '{ "query" : "select * from sys_invocation JOIN state on sys_invocation.target_service_name = state.service_name and sys_invocation.target_service_key = state.service_key;" }'
```
### Edit application state
You can edit the application state either via the state tab of the UI or via the CLI:
```shell theme={null}
restate kv edit
```
This command opens your default editor (as configured in the `cli env`).
It sends the new state values back to the runtime to be applied.
Use `--binary` if the values are not JSON-encoded UTF-8 strings.
In this case, you need to decode the base64-encoded string, and encode it back to base64 after editing.
Use `--plain` to retrieve the state as a JSON object.
This can be useful in combination with tools like `jq` for example:
```shell theme={null}
restate kv get counter bob --plain | jq '.seen'
```
If during the editing of the state with the CLI, an invocation changed the state as well, then the edit of the CLI will not take affect.
If you want the CLI state edit to be applied even if the state has changed in the meantime, then use the `--force` flag.
An example on how to edit the K/V state of the service `counter` for the key `bob`:
```shell !command theme={null}
restate kv edit counter bob
```
```log !output theme={null}
ℹ️ About to write the following state :
―――――――――――――――――――――――――――――――――――――――
service counter
key bob
force? false
binary? false
KEY VAL
seen 8
✔ Are you sure? · yes
Enqueued successfully for processing
```
# Go SDK Clients
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/services/invocation/clients/go-sdk
Invoke services from any Go code.
An invocation is a request to execute a handler.
The Go SDK client library lets you invoke Restate handlers from anywhere in your application.
Use this only in non-Restate services without access to the Restate Context.
Each invocation has its own unique ID and lifecycle.
Have a look at [managing invocations](/services/invocation/managing-invocations) to learn about the lifecycle, kill or cancel it.
Always [invoke handlers via the context](/develop/ts/service-communication), if you have access to it.
Restate then attaches information about the invocation to the parent invocation.
## Invoking handlers with the SDK clients
First, add the dependency to your project `github.com/restatedev/sdk-go/ingress`.
Then, [register the service](/services/versioning) you want to invoke.
Finally, connect to Restate and invoke the handler with your preferred semantics.
* **Request-response invocations** allow you to wait on a response from the handler.
```go {"CODE_LOAD::go/develop/ingressclients.go#rpc"} theme={null}
restateClient := restateingress.NewClient("http://localhost:8080")
// To call a service
svcResponse, err := restateingress.Service[*MyInput, *MyOutput](
restateClient, "MyService", "MyHandler").
Request(context.Background(), &input)
// To call a virtual object
objResponse, err := restateingress.Object[*MyInput, *MyOutput](
restateClient, "MyObject", "Mary", "MyHandler").
Request(context.Background(), &input)
// To Run a workflow
wfResponse, err := restateingress.Workflow[*MyInput, *MyOutput](
restateClient, "MyWorkflow", "Mary", "Run").
Request(context.Background(), &input)
// To interact with a workflow
status, err := restateingress.Object[*MyInput, *MyOutput](
restateClient, "MyWorkflow", "Mary", "interactWithWorkflow").
Request(context.Background(), &input)
```
* **One-way invocations** allow you to send a message without waiting for a response.
```go {"CODE_LOAD::go/develop/ingressclients.go#one_way_call"} theme={null}
restateClient := restateingress.NewClient("http://localhost:8080")
// To message a service
restateingress.ServiceSend[*MyInput](
restateClient, "MyService", "MyHandler").
Send(context.Background(), &input)
// To message a virtual object
restateingress.ObjectSend[*MyInput](
restateClient, "MyObject", "Mary", "MyHandler").
Send(context.Background(), &input)
// To Run a workflow without waiting for the result
restateingress.WorkflowSend[*MyInput](
restateClient, "MyWorkflow", "Mary", "Run").
Send(context.Background(), &input)
```
* **Delayed invocations** allow you to schedule an invocation for a later point in time.
```go {"CODE_LOAD::go/develop/ingressclients.go#delayed_call"} theme={null}
restateClient := restateingress.NewClient("http://localhost:8080")
// To message a service with a delay
restateingress.ServiceSend[*MyInput](
restateClient, "MyService", "MyHandler").
Send(context.Background(), &input, restate.WithDelay(5*24*time.Hour))
// To message a virtual object with a delay
restateingress.ObjectSend[*MyInput](
restateClient, "MyObject", "Mary", "MyHandler").
Send(context.Background(), &input, restate.WithDelay(5*24*time.Hour))
// To Run a workflow without waiting for the result
restateingress.WorkflowSend[*MyInput](
restateClient, "MyWorkflow", "Mary", "Run").
Send(context.Background(), &input, restate.WithDelay(5*24*time.Hour))
```
## Invoke a handler idempotently
By using Restate and an idempotency key, you can make any service call idempotent, without any extra code or setup. This is a very powerful feature to ensure that your system stays consistent and doesn’t perform the same operation multiple times.
To make a service call idempotent, you can use the idempotency key feature.
Add the idempotency key [to the header](https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/) via:
```go {"CODE_LOAD::go/develop/ingressclients.go#service_idempotent"} theme={null}
restateClient := restateingress.NewClient("http://localhost:8080")
restateingress.ServiceSend[*MyInput](
restateClient, "MyService", "MyHandler").
Send(context.Background(), &input, restate.WithIdempotencyKey("abc"))
```
After the invocation completes, Restate persists the response for a retention period of one day (24 hours).
If you re-invoke the service with the same idempotency key within 24 hours, Restate sends back the same response and doesn't re-execute the request to the service.
The call options, with which we set the idempotency key, also let you add other headers to the request.
Check out the [service configuration docs](/services/configuration) to tune the retention time.
## Retrieve result of invocations and workflows
You can use the client library to retrieve the results of invocations **with an idempotency key** or workflows.
### Attach to an invocation with an idempotency key
For invocations with an idempotency key, you can attach to the invocation and wait for it to finish:
```go {"CODE_LOAD::go/develop/ingressclients.go#service_attach"} theme={null}
restateClient := restateingress.NewClient("http://localhost:8080")
// The call to which we want to attach later
handle := restateingress.ServiceSend[*MyInput](
restateClient, "MyService", "MyHandler").
Send(context.Background(), &input, restate.WithIdempotencyKey("my-idempotency-key"))
// ... do something else ...
// ---------------------------------
// OPTION 1: With the invocation Id
invocationId := handle.Id
result1, err := restateingress.AttachInvocation[*MyOutput](
restateClient, invocationId).
Attach(context.Background())
// ---------------------------------
// OPTION 2: With the idempotency key
result2, err := restateingress.AttachService[*MyOutput](
restateClient, "MyService", "MyHandler", "my-idempotency-key").
Attach(context.Background())
```
### Attach/peek at a workflow execution
For workflows, you can attach to the workflow execution and wait for it to finish or peek at the output:
```go {"CODE_LOAD::go/develop/ingressclients.go#workflow_attach"} theme={null}
restateClient := restateingress.NewClient("http://localhost:8080")
// The workflow to which we want to attach later
wfHandle := restateingress.WorkflowSend[*MyInput](
restateClient, "MyWorkflow", "Mary", "Run").
Send(context.Background(), &input)
// ... do something else ...
// ---------------------------------
// OPTION 1: With the handle returned by the workflow submission
result, err := restateingress.AttachInvocation[*MyOutput](
restateClient, wfHandle.Id).
Attach(context.Background())
// ---------------------------------
// OPTION 2: With the workflow ID
wfHandle2, err := restateingress.AttachWorkflow[*MyOutput](
restateClient, "MyWorkflow", "wf-id").
Attach(context.Background())
```
# Java/Kotlin SDK Clients
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/services/invocation/clients/java-sdk
Invoke services from any Java/Kotlin code.
export const CustomVars = ({name}) => {
const RESTATE_VERSION = "1.4";
const TYPESCRIPT_SDK_VERSION = "1.7.2";
const JAVA_SDK_VERSION = "2.4.0";
const GO_SDK_VERSION = "0.18.0";
const PYTHON_SDK_VERSION = "0.8.0";
const RUST_SDK_VERSION = "0.6.0";
const mapping = {
RESTATE_VERSION,
TYPESCRIPT_SDK_VERSION,
JAVA_SDK_VERSION,
GO_SDK_VERSION,
PYTHON_SDK_VERSION,
RUST_SDK_VERSION,
JAVA_HTTP_DEPENDENCY: `dev.restate:sdk-java-http:${JAVA_SDK_VERSION}`,
KOTLIN_HTTP_DEPENDENCY: `dev.restate:sdk-kotlin-http:${JAVA_SDK_VERSION}`,
JAVA_LAMBDA_DEPENDENCY: `dev.restate:sdk-java-lambda:${JAVA_SDK_VERSION}`,
KOTLIN_LAMBDA_DEPENDENCY: `dev.restate:sdk-kotlin-lambda:${JAVA_SDK_VERSION}`,
JAVA_SDK_REQUEST_IDENTITY: `dev.restate:sdk-request-identity:${JAVA_SDK_VERSION}`,
JAVA_TESTING: `dev.restate:sdk-testing:${JAVA_SDK_VERSION}`,
JAVA_CLIENT: `dev.restate:client:${JAVA_SDK_VERSION}`,
KOTLIN_CLIENT: `dev.restate:client-kotlin:${JAVA_SDK_VERSION}`,
DENO_FETCH: `npm:@restatedev/restate-sdk@^${TYPESCRIPT_SDK_VERSION}/fetch`
};
return mapping[name];
};
An invocation is a request to execute a handler.
The Restate SDK client library lets you invoke Restate handlers from anywhere in your application.
Use this only in non-Restate services without access to the Restate Context.
Each invocation has its own unique ID and lifecycle.
Have a look at [managing invocations](/services/invocation/managing-invocations) to learn how to manage the lifecycle of an invocation.
Always [invoke handlers via the context](/develop/ts/service-communication), if you have access to it.
Restate then attaches information about the invocation to the parent invocation.
## Invoking handlers with the SDK clients
First, add the dependency to your project
* For Java: {}
* For Kotlin: {}
Then, [register the service](/services/versioning) you want to invoke.
Finally, connect to Restate and invoke the handler with your preferred semantics.
* **Request-response invocations** allow you to wait on a response from the handler.
```java Java {"CODE_LOAD::java/src/main/java/develop/IngressClient.java#rpc"} theme={null}
Client restateClient = Client.connect("http://localhost:8080");
// To call a service
String svcResponse = MyServiceClient.fromClient(restateClient).myHandler("Hi");
// To call a virtual object
String objResponse = MyObjectClient.fromClient(restateClient, "Mary").myHandler("Hi");
// To submit a workflow
String wfResponse =
MyWorkflowClient.fromClient(restateClient, "Mary").submit("Hi").attach().response();
// To interact with a workflow
String status =
MyWorkflowClient.fromClient(restateClient, "Mary").interactWithWorkflow("my signal");
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/IngressClient.kt#rpc"} theme={null}
val restateClient = Client.connect("http://localhost:8080")
// To call a service
val svcResponse = MyServiceClient.fromClient(restateClient).myHandler("Hi")
// To call a virtual object
val objResponse = MyObjectClient.fromClient(restateClient, "Mary").myHandler("Hi")
// To submit a workflow
val wfResponse =
MyWorkflowClient.fromClient(restateClient, "Mary").submit("Hi").attach().response()
// To interact with a workflow
val status =
MyWorkflowClient.fromClient(restateClient, "Mary").interactWithWorkflow("my signal")
```
* **One-way invocations** allow you to send a message without waiting for a response.
```java Java {"CODE_LOAD::java/src/main/java/develop/IngressClient.java#one_way_call"} theme={null}
Client restateClient = Client.connect("http://localhost:8080");
// To message a service
MyServiceClient.fromClient(restateClient).send().myHandler("Hi");
// To message a virtual object
MyObjectClient.fromClient(restateClient, "Mary").send().myHandler("Hi");
// To submit a workflow without waiting for the result
MyWorkflowClient.fromClient(restateClient, "Mary").submit("Hi");
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/IngressClient.kt#one_way_call"} theme={null}
val restateClient = Client.connect("http://localhost:8080")
// To message a service
MyServiceClient.fromClient(restateClient).send().myHandler("Hi")
// To message a virtual object
MyObjectClient.fromClient(restateClient, "Mary").send().myHandler("Hi")
// To submit a workflow without waiting for the result
MyWorkflowClient.fromClient(restateClient, "Mary").submit("Hi")
```
* **Delayed invocations** allow you to schedule an invocation for a later point in time.
```java Java {"CODE_LOAD::java/src/main/java/develop/IngressClient.java#delayed_call"} theme={null}
Client restateClient = Client.connect("http://localhost:8080");
// To message a service with a delay
MyServiceClient.fromClient(restateClient).send().myHandler("Hi", Duration.ofDays(5));
// To message a virtual object with a delay
MyObjectClient.fromClient(restateClient, "Mary").send().myHandler("Hi", Duration.ofDays(5));
// To submit a workflow with a delay
MyWorkflowClient.fromClient(restateClient, "Mary").submit("Hi", Duration.ofDays(5));
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/IngressClient.kt#delayed_call"} theme={null}
val restateClient = Client.connect("http://localhost:8080")
// To message a service with a delay
MyServiceClient.fromClient(restateClient).send().myHandler("Hi", 5.days)
// To message a virtual object with a delay
MyObjectClient.fromClient(restateClient, "Mary").send().myHandler("Hi", 5.days)
```
## Invoke a handler idempotently
By using Restate and an idempotency key, you can make any service call idempotent, without any extra code or setup. This is a very powerful feature to ensure that your system stays consistent and doesn’t perform the same operation multiple times.
To make a service call idempotent, you can use the idempotency key feature.
Add the idempotency key [to the header](https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/) via:
```java Java {"CODE_LOAD::java/src/main/java/develop/IngressClient.java#service_idempotent"} theme={null}
Client restateClient = Client.connect("http://localhost:8080");
MyObjectClient.fromClient(restateClient, "Mary")
.send()
.myHandler("Hi", opt -> opt.idempotencyKey("abc"));
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/IngressClient.kt#service_idempotent"} theme={null}
val restateClient = Client.connect("http://localhost:8080")
MyServiceClient.fromClient(restateClient).send().myHandler("Hi") { idempotencyKey = "abc" }
```
After the invocation completes, Restate persists the response for a retention period of one day (24 hours).
If you re-invoke the service with the same idempotency key within 24 hours, Restate sends back the same response and doesn't re-execute the request to the service.
The call options, with which we set the idempotency key, also let you add other headers to the request.
Check out the [service configuration docs](/services/configuration) to tune the retention time.
## Retrieve result of invocations and workflows
You can use the client library to retrieve the results of invocations **with an idempotency key** or workflows.
### Attach to an invocation with an idempotency key
For invocations with an idempotency key, you can attach to the invocation and wait for it to finish:
```java Java {"CODE_LOAD::java/src/main/java/develop/IngressClient.java#service_attach"} theme={null}
Client restateClient = Client.connect("http://localhost:8080");
// The call to which we want to attach later
var handle =
MyServiceClient.fromClient(restateClient)
.send()
.myHandler("Hi", opt -> opt.idempotencyKey("my-idempotency-key"));
// ... do something else ...
// ---------------------------------
// OPTION 1: With the handle returned by the call
// - Attach
String result1 = handle.attach().response();
// - Peek
Output output = handle.getOutput().response();
if (output.isReady()) {
String result2 = output.getValue();
}
// ---------------------------------
// OPTION 2: With the Invocation ID
// Retrieve the invocation ID from the handle and send it to another process
String invocationId = handle.invocationId();
// Attach/peek later from the other process
var handle2 = restateClient.invocationHandle(invocationId, String.class);
// use it to attach or peek (see above)
// ---------------------------------
// OPTION 3: With the idempotency key
var myService = Target.service("MyService", "myHandler");
var handle3 =
restateClient.idempotentInvocationHandle(myService, "my-idempotency-key", String.class);
// use it to attach or peek (see above)
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/IngressClient.kt#service_attach"} theme={null}
val restateClient = Client.connect("http://localhost:8080")
// The call to which we want to attach later
val handle =
MyServiceClient.fromClient(restateClient).send().myHandler("Hi") {
idempotencyKey = "my-idempotency-key"
}
// ... do something else ...
// ---------------------------------
// OPTION 1: With the handle returned by the call
// - Attach
val result1 = handle.attach().response()
// - Peek
val output = handle.getOutput().response()
if (output.isReady()) {
val result2 = output.getValue()
}
// ---------------------------------
// OPTION 2: With the Invocation ID
// Retrieve the invocation ID from the handle and send it to another process
val invocationId = handle.invocationId()
// Attach/peek later from the other process
val handle2 = restateClient.invocationHandle(invocationId, typeTag())
// use it to attach or peek (see above)
// ---------------------------------
// OPTION 3: With the idempotency key
val target = Target.service("MyService", "myHandler")
val handle3 =
restateClient.idempotentInvocationHandle(target, "my-idempotency-key", typeTag())
// use it to attach or peek (see above)
```
### Attach/peek at a workflow execution
For workflows, you can attach to the workflow execution and wait for it to finish or peek at the output:
```java Java {"CODE_LOAD::java/src/main/java/develop/IngressClient.java#workflow_attach"} theme={null}
Client restateClient = Client.connect("http://localhost:8080");
// The workflow to which we want to attach later
var wfHandle = MyWorkflowClient.fromClient(restateClient, "Mary").submit("Hi");
// ... do something else ...
// ---------------------------------
// OPTION 1: With the handle returned by the workflow submission
// - Attach
String result = wfHandle.attach().response();
// - Peek
Output output = wfHandle.getOutput().response();
if (output.isReady()) {
String result2 = output.getValue();
}
// ---------------------------------
// OPTION 2: With the workflow ID
var wfHandle2 = restateClient.workflowHandle("MyWorkflow", "wf-id", String.class);
// use it to attach or peek (see above)
```
```kotlin Kotlin {"CODE_LOAD::kotlin/src/main/kotlin/develop/IngressClient.kt#workflow_attach"} theme={null}
val restateClient = Client.connect("http://localhost:8080")
// The workflow to which we want to attach later
val wfHandle = MyWorkflowClient.fromClient(restateClient, "Mary").submit("Hi")
// ... do something else ...
// ---------------------------------
// OPTION 1: With the handle returned by the workflow submission
// - Attach
val result = wfHandle.attach().response()
// - Peek
val output = wfHandle.getOutput().response()
if (output.isReady()) {
val result2 = output.getValue()
}
// ---------------------------------
// OPTION 2: With the workflow ID
val wfHandle2 = restateClient.workflowHandle("MyWorkflow", "wf-id", typeTag())
// use it to attach or peek (see above)
```
# TypeScript SDK Clients
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/services/invocation/clients/typescript-sdk
Invoke services from any TypeScript code.
An invocation is a request to execute a handler.
The Restate SDK client library lets you invoke Restate handlers from anywhere in your application.
Use this only in non-Restate services without access to the Restate Context.
Each invocation has its own unique ID and lifecycle.
Have a look at [managing invocations](/services/invocation/managing-invocations) to learn how to manage the lifecycle of an invocation.
Always [invoke handlers via the context](/develop/ts/service-communication), if you have access to it.
Restate then attaches information about the invocation to the parent invocation.
## Invoking handlers with the SDK clients
First, add the dependency to your project
```shell theme={null}
npm install @restatedev/restate-sdk-clients
```
Then, [register the service](/services/versioning) you want to invoke.
Finally, connect to Restate and invoke the handler with your preferred semantics.
* **Request-response invocations** allow you to wait on a response from the handler.
```ts {"CODE_LOAD::ts/src/develop/clients/ingress.ts#rpc_call_node"} theme={null}
// import * as clients from "@restatedev/restate-sdk-clients";
const restateClient = clients.connect({ url: "http://localhost:8080" });
// To call a service
const greet = await restateClient
.serviceClient({ name: "MyService" })
.greet({ greeting: "Hi" });
// To call an object
const count = await restateClient
.objectClient({ name: "MyObject" }, "Mary")
.greet({ greeting: "Hi" });
// To call a workflow
const handle = await restateClient
.workflowClient({ name: "MyWorkflow" }, "someone")
.workflowSubmit({ greeting: "Hi" });
const result = await restateClient.result(handle);
const status = await restateClient
.workflowClient({ name: "MyWorkflow" }, "someone")
.myOtherHandler();
```
* **One-way invocations** allow you to send a message without waiting for a response.
```ts {"CODE_LOAD::ts/src/develop/clients/ingress.ts#one_way_call_node"} theme={null}
// import * as clients from "@restatedev/restateClient-sdk-clients";
const restateClient = clients.connect({ url: "http://localhost:8080" });
// To send a message to a service
await restateClient
.serviceSendClient({ name: "MyService" })
.greet({ greeting: "Hi" });
// To send a message to an object
await restateClient
.objectSendClient({ name: "MyObject" }, "Mary")
.greet({ greeting: "Hi" });
// To send a message to a workflow
const handle = await restateClient
.workflowClient({ name: "MyWorkflow" }, "someone")
.workflowSubmit({ greeting: "Hi" });
// You cannot send a message to a shared handler in a workflow
```
* **Delayed invocations** allow you to schedule an invocation for a later point in time.
```ts {"CODE_LOAD::ts/src/develop/clients/ingress.ts#delayed_call_node"} theme={null}
// import * as clients from "@restatedev/restate-sdk-clients";
const restateClient = clients.connect({ url: "http://localhost:8080" });
// To send a delayed message to a service
await restateClient
.serviceSendClient({ name: "MyService" })
.greet({ greeting: "Hi" }, clients.rpc.sendOpts({ delay: { seconds: 1 } }));
// To send a delayed message to an object
await restateClient
.objectSendClient({ name: "MyObject" }, "Mary")
.greet({ greeting: "Hi" }, clients.rpc.sendOpts({ delay: { seconds: 1 } }));
// To send a delayed message to a workflow
const handle = await restateClient
.workflowClient({ name: "MyWorkflow" }, "someone")
.workflowSubmit(
{ greeting: "Hi" },
clients.rpc.sendOpts({ delay: { seconds: 1 } })
);
// You cannot send a delayed message to a shared handler in a workflow
```
## Invoke a handler idempotently
By using Restate and an idempotency key, you can make any service call idempotent, without any extra code or setup. This is a very powerful feature to ensure that your system stays consistent and doesn’t perform the same operation multiple times.
To make a service call idempotent, you can use the idempotency key feature.
Add the idempotency key [to the header](https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/) via:
```typescript {"CODE_LOAD::ts/src/develop/clients/ingress.ts#service_idempotent"} theme={null}
await restateClient
.serviceSendClient({ name: "MyService" })
.greet(request, clients.rpc.sendOpts({ idempotencyKey: "abcde" }));
```
After the invocation completes, Restate persists the response for a retention period of one day (24 hours).
If you re-invoke the service with the same idempotency key within 24 hours, Restate sends back the same response and doesn't re-execute the request to the service.
The call options, with which we set the idempotency key, also let you add other headers to the request.
Check out the [service configuration docs](/services/configuration) to tune the retention time.
## Retrieve result of invocations and workflows
You can use the client library to retrieve the results of invocations **with an idempotency key** or workflows.
### Attach to an invocation with an idempotency key
For invocations with an idempotency key, you can attach to the invocation and wait for it to finish:
```typescript {"CODE_LOAD::ts/src/develop/clients/ingress.ts#service_attach"} theme={null}
// import * as clients from "@restatedev/restate-sdk-clients";
const restateClient = clients.connect({ url: "http://localhost:8080" });
// To send a message
const handle = await restateClient
.serviceSendClient({ name: "MyService" })
.greet(request, clients.rpc.sendOpts({ idempotencyKey: "abcde" }));
// ... do something else ...
// Attach later to retrieve the result
const response = await restateClient.result(handle);
```
### Attach/peek at a workflow execution
For workflows, you can attach to the workflow execution and wait for it to finish or peek at the output:
```typescript {"CODE_LOAD::ts/src/develop/clients/ingress.ts#workflow_attach"} theme={null}
// import * as clients from "@restatedev/restate-sdk-clients";
const restateClient = clients.connect({ url: "http://localhost:8080" });
// Option 1: attach and wait for result with workflow ID
const result = await restateClient
.workflowClient({ name: "MyWorkflow" }, "someone")
.workflowAttach();
// Option 2: peek to check if ready with workflow ID
const peekOutput = await restateClient
.workflowClient({ name: "MyWorkflow" }, "someone")
.workflowOutput();
if (peekOutput.ready) {
const result2 = peekOutput.result;
}
```
## Advanced: sharing service type definitions
Have a look at the options listed in the [Service Communication docs](/develop/ts/service-communication#advanced%3A-sharing-service-type-definitions). These are also valid for the SDK clients.
# HTTP
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/services/invocation/http
Learn how to invoke Restate services over HTTP.
An invocation is a request to execute a handler.
You can invoke handlers over HTTP with or without waiting for a response, and with or without an idempotency key.
Make sure to first [register the handler](/services/versioning) you want to invoke.
The [UI](/installation#restate-ui) helps you with invoking your services.
Open the UI at port 9070, register your service, click on the service, open the playground, and invoke your handlers from there.
Each invocation has its own unique ID and lifecycle.
Have a look at [managing invocations](/services/invocation/managing-invocations) to learn how to manage the lifecycle of an invocation.
## Request-response calls
You can invoke services over HTTP 1.1 or higher.
Request/response bodies should be encoded as JSON.
Invoking `myHandler` of `myService` as follows:
```shell theme={null}
curl localhost:8080/MyService/myHandler \
--json '{"name": "Mary", "age": 25}'
```
Invoke `myHandler` of `myVirtualObject` for `myKey` as follows:
```shell theme={null}
curl localhost:8080/MyVirtualObject/myObjectKey/myHandler \
--json '{"name": "Mary", "age": 25}'
```
Call the `run` handler of the `MyWorkflow` as follows:
```shell theme={null}
curl localhost:8080/MyWorkflow/myWorkflowId/run \
--json '{"name": "Mary", "age": 25}'
```
A workflow can be submitted only once. Resubmission of the same workflow will fail with "Previously accepted". The invocation ID can be found in the request header `x-restate-id`.
Follow the same pattern for calling the other handlers of the workflow.
Note that all invocations go first via the Restate Server. The server then forwards the request to the appropriate service.
Therefore, `localhost:8080` refers to ingress port of the Restate Server, not the service instance.
## Sending messages
If you do not want to wait for the response, you can also send a message by adding `/send` to the URL path:
```shell theme={null}
curl localhost:8080/MyService/myHandler/send \
--json '{"name": "Mary", "age": 25}'
```
Example output:
```json theme={null}
{"invocationId":"inv_1aiqX0vFEFNH1Umgre58JiCLgHfTtztYK5","status":"Accepted"}
```
The response contains the [Invocation ID](#invocation-identifier).
You can use this identifier [to cancel](#cancelling-invocations) or [kill the invocation](#killing-invocations).
## Delayed messages
You can **delay the message** by adding a delay request parameter in ISO8601 notation or using [humantime format](https://docs.rs/humantime/latest/humantime/):
```shell humantime theme={null}
curl "localhost:8080/MyService/myHandler/send?delay=10s" \
--json '{"name": "Mary", "age": 25}'
```
```shell ISO8601 theme={null}
curl "localhost:8080/MyService/myHandler/send?delay=PT10S" \
--json '{"name": "Mary", "age": 25}'
```
You cannot yet use this feature for workflows.
Workflows can only be scheduled with a delay from within another Restate handler ([TS](/develop/ts/service-communication#delayed-calls)/[Java/Kotlin](/develop/java/service-communication#delayed-calls)).
## Using an idempotency key
You can send requests to Restate providing an idempotency key, through the [`Idempotency-Key` header](https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/):
```shell theme={null}
curl localhost:8080/MyService/myHandler \
-H 'idempotency-key: ad5472esg4dsg525dssdfa5loi' \
--json '{"name": "Mary", "age": 25}'
```
After the invocation completes, Restate persists the response for a retention period of one day (24 hours).
If you re-invoke the service with the same idempotency key within 24 hours, Restate sends back the same response and doesn't re-execute the request to the service.
Check out the [service configuration docs](/services/configuration) to tune the retention time.
With Restate and an idempotency key, you can make any service call idempotent, without any extra code or setup.
This is a very powerful feature to ensure that your system stays consistent and doesn't perform the same operation multiple times.
## Attach to an invocation
Restate allows you to retrieve the result of workflows and invocations with an idempotency key.
There are two options:
* To **attach** to an invocation or workflow and wait for it to finish, use `/attach`.
* To **peek at the output** of an invocation or workflow, use `/output`. This will return:
* `{"message":"not ready"}` for ongoing workflows
* The result for finished workflows
* `{"message":"not found"}` for non-existing workflows
You can attach to a service/object invocation only if the invocation used an idempotency key:
```shell theme={null}
# Via invocation ID
curl localhost:8080/restate/invocation/myInvocationId/attach
curl localhost:8080/restate/invocation/myInvocationId/output
# For Services, via idempotency key
curl localhost:8080/restate/invocation/MyService/myHandler/myIdempotencyKey/attach
curl localhost:8080/restate/invocation/MyService/myHandler/myIdempotencyKey/output
# For Virtual Objects, via idempotency key
curl localhost:8080/restate/invocation/myObject/myKey/myHandler/myIdempotencyKey/attach
curl localhost:8080/restate/invocation/myObject/myKey/myHandler/myIdempotencyKey/output
# For Workflows, with the Workflow ID
curl localhost:8080/restate/workflow/MyWorkflow/myWorkflowId/attach
curl localhost:8080/restate/workflow/MyWorkflow/myWorkflowId/output
```
## OpenAPI support
Restate exposes for every service an OpenAPI 3.1 definition, to get it:
```shell theme={null}
curl localhost:9070/services/MyService/openapi > MyService_openapi.json
```
You can use this definition with any OpenAPI 3.1 compliant tool to generate clients for your service, such as [openapi-generator](https://openapi-generator.tech/).
Depending on the SDKs, the rich input/output JSON schemas are included as well. At the moment, rich schemas are supported for:
* [TypeScript SDK with Zod schemas](/develop/ts/serialization#zod-schemas)
* [Python SDK with Pydantic](/develop/python/serialization#pydantic) models
* Java and Kotlin SDK
# null
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/services/invocation/kafka
Invoke handlers via Kafka events.
Connect your Restate handlers to Kafka topics.
Restate takes care of Kafka consumer management and pushes events to your Restate handlers.
You get zero-overhead consumer management with automatic retries, durable execution, and stateful processing capabilities.
Each event leads to an invocation, meaning a request to execute a handler. Each invocation has its own unique ID and lifecycle.
Have a look at [managing invocations](/services/invocation/managing-invocations) to learn how to manage the lifecycle of an invocation.
## Invoking Handlers via Kafka Events
You can invoke handlers via Kafka events, by doing the following:
You can invoke any handler via Kafka events.
The event payload will be (de)serialized as JSON.
* When invoking **Virtual Object** or **Workflow** handlers via Kafka, the key of the Kafka record will be used to determine the Virtual Object/Workflow key.
The key needs to be a valid UTF-8 string.
The events are delivered to the subscribed handler in the order in which they arrived on the topic partition.
* When invoking **Virtual Object** or **Workflow** *shared* handlers via Kafka, the key of the Kafka record will be used to determine the Virtual Object/Workflow key.
The key needs to be a valid UTF-8 string.
The events are delivered to the subscribed handler in parallel without ordering guarantees.
* When invoking **Service** handlers over Kafka, events are delivered in parallel without ordering guarantees.
Since you can invoke any handler via Kafka events, a single handler can be invoked both by RPC and via Kafka.
Define the Kafka cluster that Restate needs to connect to in the [Restate configuration file](/server/configuration#configuration-file):
```toml restate.toml theme={null}
[[ingress.kafka-clusters]]
name = "my-cluster"
brokers = ["PLAINTEXT://broker:9092"]
```
And make sure the Restate Server uses it via `restate-server --config-file restate.toml`.
Check the [configuration docs](/server/configuration) for more details.
You can also configure the Kafka clusters via the `RESTATE_INGRESS__KAFKA_CLUSTERS` environment variable:
```bash theme={null}
RESTATE_INGRESS__KAFKA_CLUSTERS=[{name="my-cluster",brokers=["PLAINTEXT://broker:9092"]}]
```
Let Restate forward events from the Kafka topic to the event handler by creating a subscription:
```bash theme={null}
curl localhost:9070/subscriptions --json '{
"source": "kafka://my-cluster/my-topic",
"sink": "service://MyService/handle",
"options": {"auto.offset.reset": "earliest"}
}'
```
Once you've created a subscription, Restate immediately starts consuming events from Kafka.
The handler will be invoked for each event received from Kafka.
The `options` field is optional and accepts any configuration parameter from [librdkafka configuration](https://github.com/confluentinc/librdkafka/blob/master/CONFIGURATION.md).
You can pass arbitrary Kafka cluster options in the `restate.toml`, and those options will be applied for all the subscriptions to that cluster, for example:
```toml restate.toml theme={null}
[[ingress.kafka-clusters]]
name = "my-cluster"
brokers = ["PLAINTEXT://broker:9092"]
sasl.username = "me"
sasl.password = "pass"
```
For the full list of options, check [librdkafka configuration](https://github.com/confluentinc/librdkafka/blob/master/CONFIGURATION.md).
You can configure multiple kafka clusters in the `restate.toml` file:
```toml restate.toml theme={null}
[[ingress.kafka-clusters]]
name = "my-cluster-1"
brokers = ["PLAINTEXT://localhost:9092"]
[[ingress.kafka-clusters]]
name = "my-cluster-2"
brokers = ["PLAINTEXT://localhost:9093"]
```
And then, when creating the subscriptions, you refer to the specific cluster by `name`:
```bash theme={null}
# Subscription to my-cluster-1
curl localhost:9070/subscriptions --json '{
"source": "kafka://my-cluster-1/topic-1",
"sink": "service://MyService/handleCluster1"
}'
# Subscription to my-cluster-2
curl localhost:9070/subscriptions --json '{
"source": "kafka://my-cluster-2/topic-2",
"sink": "service://MyService/handleCluster2"
}'
```
You can access the event metadata in the handler by getting the request headers map:
```ts TypeScript {"CODE_LOAD::ts/src/develop/kafka.ts#headers"} theme={null}
ctx.request().headers,
```
```java Java {"CODE_LOAD::java/src/main/java/develop/MyKafkaVirtualObject.java#headers"} theme={null}
ctx.request().headers();
```
```go Go {"CODE_LOAD::go/develop/kafka.go#headers"} theme={null}
ctx.Request().Headers
```
```python Python {"CODE_LOAD::python/src/develop/kafka.py#headers"} theme={null}
ctx.request().headers
```
Each event carries within this map the following entries:
* `restate.subscription.id`: The subscription identifier, as shown by the Admin API.
* `kafka.offset`: The record offset.
* `kafka.partition`: The record partition.
* `kafka.timestamp`: The record timestamp.
Check out the serialization documentation of your SDK to learn how to receive raw events in your handler.
## Managing Kafka Subscriptions
Restate can trigger handlers via Kafka events.
### Create Subscriptions
Subscribe a handler to a Kafka topic:
```bash theme={null}
curl localhost:9070/subscriptions --json '{
"source": "kafka://my-cluster/my-topic",
"sink": "service://MyService/Handle",
"options": {"auto.offset.reset": "earliest"}
}'
```
The `options` field is optional and accepts any [librdkafka configuration](https://github.com/confluentinc/librdkafka/blob/master/CONFIGURATION.md) parameter.
### List Subscriptions
View current subscriptions:
```bash theme={null}
curl localhost:9070/subscriptions
```
**Example response:**
```json theme={null}
{
"subscriptions": [
{
"id": "sub_11XHoawrCiWtv8kzhEyGtsR",
"source": "kafka://my-cluster/my-topic",
"sink": "service://Greeter/greet",
"options": {
"auto.offset.reset": "earliest",
"client.id": "restate",
"group.id": "sub_11XHoawrCiWtv8kzhEyGtsR"
}
}
]
}
```
### Delete Subscriptions
Remove a subscription using its ID (starts with `sub_`):
```bash theme={null}
curl -X DELETE localhost:9070/subscriptions/sub_11XHoawrCiWtv8kzhEyGtsR
```
When you delete a subscription, Restate stops the associated consumer group. Messages already enqueued by Restate will still be processed.
# Managing Invocations
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/services/invocation/managing-invocations
Understand the lifecycle of a Restate invocation and how to manage it.
An invocation is a request to execute a handler. Each invocation has its own unique ID and lifecycle.
## Invocation ID
*Invocations* have a unique **Invocation ID** starting with `inv_`. You can find this ID in every place where an invocation is mentioned:
* The invocations page of the [Restate UI](/installation#restate-ui)
* Logs and traces (`restate.invocation.id`), both in Restate and SDKs
* In CLI commands such as `restate invocation ls`
## Lifecycle
Invocations follow a well-defined lifecycle:
This page describes how to manage invocations in different states.
## Cancel
You can cancel an invocation at any point in its lifecycle.
Cancellation has the following characteristics:
* Frees held resources
* Cooperates with your handler code to roll back any changes made so far
* Allows proper cleanup
```shell CLI theme={null}
restate invocations cancel inv_1gdJBtdVEcM942bjcDmb1c1khoaJe11Hbz
# Or bulk cancel, e.g. per service or handler
restate invocations cancel Greeter
restate invocations cancel Greeter/greet
restate invocations cancel CartObject/cart55/add
```
```shell curl theme={null}
curl -X PATCH http://localhost:9070/invocations/inv_1gdJBtdVEcM942bjcDmb1c1khoaJe11Hbz/cancel
```
Cancellation is non-blocking. The API call may return before cancellation completes. In rare cases, cancellation may not take effect - retry the operation if needed.
For proper rollback, handlers must include compensation logic to maintain service state consistency. Check the [sagas guide](/guides/sagas).
When an invocation is canceled, Restate propagates the cancellation recursively through the call graph:
1. **Propagate to leaves**: Cancellation first reaches the leaves of the call graph. Restate interrupts any ongoing sleeps, awakeables, or other context actions at those points.
2. **Throw terminal error**: At the cancellation point, Restate throws a terminal error, signaling that normal execution cannot continue.
3. **Run compensation**: The handler runs its defined compensation logic to undo or mitigate side effects.
4. **Propagate upward**: The cancellation response then flows back up the call graph, ensuring that each parent invocation also executes its compensation logic if defined.
## Kill
Use kill when cancellation fails (e.g., when endpoints are permanently unavailable).
Killing immediately stops all calls in the invocation tree **without executing compensation logic**. This may leave your service in an inconsistent state. Only use as a last resort after trying other fixes.
Note that one-way calls and delayed calls are not killed because they're detached from the originating call tree.
```shell CLI theme={null}
restate invocations kill inv_1gdJBtdVEcM942bjcDmb1c1khoaJe11Hbz
# Or bulk kill, e.g. per service
restate invocations kill Greeter
restate invocations kill Greeter/greet
restate invocations kill CartObject/cart55/add
```
```shell curl theme={null}
curl -X PATCH http://localhost:9070/invocations/inv_1gdJBtdVEcM942bjcDmb1c1khoaJe11Hbz/kill
```
## Resume
If an invocation retries for too many times, Restate will pause it.
When an invocation is paused, you need to resume it manually:
```shell CLI theme={null}
restate invocations resume inv_1gdJBtdVEcM942bjcDmb1c1khoaJe11Hbz
# Or bulk resume, e.g. per service
restate invocations resume Greeter
```
```shell curl theme={null}
curl -X PATCH http://localhost:9070/invocations/inv_1gdJBtdVEcM942bjcDmb1c1khoaJe11Hbz/resume
```
Check out the [service configuration docs](/services/configuration#retries) to tune the invocation retry policy,
including the retry interval and how many attempts to perform before pausing.
You can also resume an invocation that is waiting on a retry timer, instructing Restate to retry immediately.
When resuming, the invocation will run on the same deployment where it started.
If the invocation failed due to some bug you fixed on a new/different deployment,
you can override on which deployment the invocation should be resumed:
```shell curl theme={null}
curl -X PATCH http://localhost:9070/invocations/inv_1gdJBtdVEcM942bjcDmb1c1khoaJe11Hbz/resume\?deployment=dp_17sztQp4gnEC1L0OCFM9aEh
```
Be aware that if the business logic/flow between the old and the new deployment code differ, once resumed the invocation will start fail with **non-determinism errors**!
## Purge
After an invocation completes, it will be retained by Restate for some time, in order to introspect it and, in case of idempotent requests, to perform deduplication.
Check out the [service configuration docs](/services/configuration#retention-of-completed-invocations) to tune the retention time.
You can also manually purge completed invocations if you need to free up disk space:
```shell CLI theme={null}
restate invocations purge inv_1gdJBtdVEcM942bjcDmb1c1khoaJe11Hbz
# Or bulk purge, e.g. per service
restate invocations purge Greeter
```
```shell curl theme={null}
curl -X PATCH http://localhost:9070/invocations/inv_1gdJBtdVEcM942bjcDmb1c1khoaJe11Hbz/purge
```
## Restart as new
Invocations, once completed, can be **restarted as new invocations**:
```shell CLI theme={null}
restate invocations restart-as-new inv_1gdJBtdVEcM942bjcDmb1c1khoaJe11Hbz
# Or bulk restart as new, e.g. per service
restate invocations restart-as-new Greeter
```
```shell curl theme={null}
curl -X PATCH http://localhost:9070/invocations/inv_1gdJBtdVEcM942bjcDmb1c1khoaJe11Hbz/restart-as-new
```
The new invocation will have a different invocation ID, but the input and headers of the original request.
The old invocation will be left untouched.
Keep in mind that when restarting an invocation as new, no partial progress will be kept, meaning all the operations the service executed will be executed again.
This feature is not available for workflows.
# Security
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/services/security
Restrict access to Restate services
## Private services
When registering an endpoint, every service is by default reachable via HTTP requests to the ingress.
You can configure a service as `private`, via the [service configuration](/services/configuration).
Note that private services can still be invoked by other handlers via the SDK.
## Locking down service access
Only Restate needs to be able to make requests to your services.
The Restate Server will proxy all requests for these services.
Therefore, it is advisable to ensure that only Restate can reach your service.
Unrestricted access to the services is dangerous. If you're working with multiple Restate instances, you also may want to check that requests are
coming from the right instance.
To make this easier, Restate has a native request identity feature which can be
used in the SDK to cryptographically verify that requests have come from a
particular Restate instance.
To get started, you need an ED25519 private key, which you can generate using
openssl as follows:
```bash theme={null}
openssl genpkey -algorithm ed25519 -outform pem -out private.pem
```
You can provide the path to the key to Restate on startup using an environment
variable: `RESTATE_REQUEST_IDENTITY_PRIVATE_KEY_PEM_FILE=private.pem`.
On start, Restate will log out the public key in a convenient compact
format:
```
INFO restate_service_client::request_identity::v1
Loaded request identity key
kid: "publickeyv1_w7YHemBctH5Ck2nQRQ47iBBqhNHy4FV7t2Usbye2A6f"
path: private.pem
```
You can also obtain the public key in this format without running Restate using
the following script:
```shell generate.sh expandable theme={null}
#!/usr/bin/env bash
set -euo pipefail
# generate private key
openssl genpkey -algorithm ed25519 -outform pem -out private.pem
echo "Wrote private key to private.pem"
base58_chars="123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
# encode public key
encoded=$(openssl ec -in private.pem -inform pem -pubout -outform der -out /dev/stdout 2>/dev/null |
tail -c +13 |
basenc --base16 "${1:-/dev/stdin}" -w0 |
if
read
[[ $REPLY =~ ^((00)*)(([[:xdigit:]]{2})*) ]]
echo -n "${BASH_REMATCH[1]//00/1}" # leading 0s -> 1
(( ${#BASH_REMATCH[3]} > 0 ))
then
dc -e "16i0${BASH_REMATCH[3]^^} Ai[58~rd0 public-key
echo "Wrote publickeyv1_${encoded} to public-key"
```
The string `publickeyv1_w7YHemBctH5Ck2nQRQ47iBBqhNHy4FV7t2Usbye2A6f` does not
need to be kept secret and can be safely included in your code when providing to the
SDK. To learn how to provide the public key to the SDK, see the serving docs of your SDK:
([TS](/develop/ts/serving#validating-request-identity) /
[Java](/develop/java/serving#validating-request-identity) /
[Go](/develop/go/serving#validating-request-identity) /
[Python](/develop/python/serving#validating-request-identity) /
[Rust](https://docs.rs/restate-sdk/latest/restate_sdk/http_server/index.html#validating-request-identity))
# Versioning
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/services/versioning
Understand deployments, registration, and versioning in Restate.
**Service versioning is critical for durable execution.** When requests can sleep for hours or days, Restate needs to ensure they always use the same code version they started with.
When Restate resumes an invocation, it replays the journal. The journal should be executed against the same code version to ensure deterministic behaviour.
This requires understanding two fundamental approaches: **immutable versions** and **in-place updates**.
## Immutable versions vs In-place updates
**Immutable versions** create a completely new deployment for each code change. Once a deployment is registered with Restate, its code cannot change. Restate automatically proxies new requests to the latest version, while ongoing requests continue with their original version until completion.
**In-place updates** modify the code at an existing endpoint without creating a new deployment. While sometimes necessary for critical bug fixes, this approach breaks determinism if not handled carefully.
**Restate requires the use of immutable versions.** They guarantee that your durable executions remain deterministic without requiring you to think about the complexities of breaking existing in-flight invocations.
You should in-place updates only in special situations, such as bug fixes.
## What is a deployment?
A deployment in Restate is a specific, versioned instance of your service code, whether running as an HTTP endpoint, a Lambda function, or another supported environment.
Each deployment is immutable by design: once registered, its code and endpoint must not change.
An invocation is bound to a specific deployment: it starts and completes within that same deployment. This ensures that in-flight requests always see the code they started with, preserving correctness and determinism.
If possible, **avoid long-running handlers** (days or months).
Otherwise, you need to keep old deployments around for a long time (until all invocations complete).
Instead, break work into smaller chunks and chain them via service-to-service calls.
For example, instead of a handler that sleeps for hours, have it schedule a new invocation via a delayed message.
It's possible to [resume an invocation on a different deployment](/services/invocation/managing-invocations#resume).
This is useful if there is a bug in the original deployment’s application code, and you want to move the invocation to a new deployment which contains a fix.
## Registering a deployment
After deploying your service, you must register it with Restate so it can be discovered and invoked.
**You can register a deployment using:**
* The [Restate UI](/installation#restate-ui)
* The CLI:
```bash theme={null}
restate deployments register http://localhost:9080
```
* The Admin API:
```bash theme={null}
curl localhost:9070/deployments --json '{"uri": "http://localhost:9080"}'
```
**Note:**
* For AWS Lambda, use the function ARN instead of a URL (e.g., `arn:aws:lambda:region:account-id:function:function-name:version`).
* If running Restate in Docker, use `host.docker.internal` instead of `localhost`.
### Deployments supporting only HTTP1.1
Some deployments only support HTTP/1.1, and not HTTP/2.
This means Restate cannot use bidirectional streaming of journal entries and needs to communicate with the service in request-response mode ([learn more](/guides/request-lifecycle#request-response-deployment-targets)).
To register such deployments, you need to specify using HTTP/1.1 during registration:
```shell CLI theme={null}
restate deployments register http://localhost:9080 --use-http1.1
```
```bash curl theme={null}
curl localhost:9070/deployments --json '{"uri": "http://localhost:9080", "use_http_11": true}'
```
When registering deployments via the UI, you can select the HTTP/1.1 checkbox.
## Automatic versioning with FaaS platforms
Function-as-a-Service (FaaS) platforms automatically handle immutable versioning through version-specific URLs or ARNs. This makes them ideal for Restate deployments as they eliminate the complexity of manual version management.
Have a look at the dedicated deployment docs to learn more:
* [Vercel](/services/deploy/vercel#register-the-service-to-restate): Register the Commit URL so that Restate can address specific Vercel deployments.
* [AWS Lambda](/services/deploy/lambda): When you [publish a Lambda function](https://docs.aws.amazon.com/lambda/latest/dg/configuration-versions.html), it automatically creates an immutable version with a unique ARN that never changes.
* [Deno Deploy](/services/deploy/deno-deploy#register-the-service-to-restate): Register the Preview URLs so that Restate can target specific Deno deployments.
* [Cloudflare Workers](/services/deploy/cloudflare-workers#register-the-service-to-restate): Register the Preview URLs so that Restate can target specific Cloudflare deployments.
## Automatic versioning with Kubernetes Operator
The Restate Kubernetes operator provides a higher-level abstraction for managing service deployments and their versions automatically.
The operator handles the complete versioning lifecycle:
1. **Deploy new versions**: Create a new `RestateDeployment` with your updated container image
2. **Automatic registration**: The operator registers the new deployment with the Restate cluster
3. **Traffic routing**: New requests automatically route to the latest version
4. **Graceful draining**: The operator monitors older deployments for ongoing invocations
5. **Auto-scaling to zero**: Once drained, older versions automatically scale to zero
For complete examples and specifications, see [Restate Kubernetes Operator](https://github.com/restatedev/restate-operator).
## Manual deployment management
For non-FaaS deployments or when you need direct control, you can manually manage immutable deployments.
### Creating immutable deployments
Since deployments are immutable, updates require creating new deployments:
Deploy your updated service code to a new endpoint (e.g., `http://greeter-v2/`).
Then register it with Restate:
```shell CLI theme={null}
restate deployments register http://greeter-v2/
```
```shell curl theme={null}
curl localhost:9070/deployments --json '{"uri": "http://greeter-v2/"}'
```
Restate automatically routes new requests to the latest deployment. Existing requests continue on the original deployment.
Check for in-flight invocations on deployments [via the UI](https://restate.dev/blog/announcing-restate-ui/#an-aid-for-versioning) or CLI.
```shell CLI theme={null}
# Find the deployment ID of your service
restate services list
# Check the number of active invocations for each deployment
restate deployments list
# Get detailed information about a specific deployment
restate deployment describe
```
Once all invocations are complete, you can safely remove the old deployment.
```shell CLI theme={null}
restate deployments remove dp_14LsPzGz9HBxXIeBoH5wYUh
```
```bash curl theme={null}
curl -X DELETE localhost:9070/deployments/dp_14LsPzGz9HBxXIeBoH5wYUh
```
If you need to force removal before the deployment is fully drained, use the `--force` flag in CLI, or `?force=true` for curl.
Virtual Object state persists across versions. Ensure your state schema remains backward compatible.
### Removing a service
Restate does not support removing individual services directly. Instead, you must remove the deployment that contains the service.
To do this safely, follow the steps below:
1. Ensure no other handlers or services have business logic that calls the service you're removing.
2. If several services are bundled in the same deployment, you can't remove only one of them. You have to remove the whole deployment.
So make sure that you first deploy the services you want to keep in a separate new deployment.
3. [Make the service private](/services/security#private-services) to avoid accepting new HTTP requests.
4. Check whether the service has pending invocations by filtering the invocations on deployment ID in the [UI](/installation#restate-ui) or via `restate services status`, and wait until the service is drained (i.e. no ongoing invocations).
**When all prerequisites are fulfilled**, you can remove the deployment containing the service via the [UI](/installation#restate-ui) or via:
```shell CLI theme={null}
restate deployments remove dp_14LsPzGz9HBxXIeBoH5wYUh
```
```bash curl theme={null}
curl -X DELETE localhost:9070/deployments/dp_14LsPzGz9HBxXIeBoH5wYUh
```
If the deployment isn't drained yet but you still want to remove it, use the `--force` flag in CLI, or `?force=true` for curl.
## Advanced: Updating deployments in-place
While deployments should be immutable, critical bugs sometimes require updating deployed code to fix stuck invocations.
### When This is Needed
If a bug (like a null pointer exception) occurs mid-execution, registering a new deployment only fixes new invocations. Existing stuck invocations need the original deployment fixed.
### Two Approaches
1. **Update underlying code** at the same URI (does not work for Restate Kubernetes Operator or many FaaS platforms)
2. **Update deployment endpoint** to point to a patched version:
```shell theme={null}
curl -X PUT localhost:9070/deployments/dp_14LsPzGz9HBxXIeBoH5wYUh \
--json '{"uri": "http://greeter-patched/"}'
```
### Common Scenarios
The current deployment handling new invocations has bugs:
1. Develop a fix, based on the current deployed version, that resolves the failing invocations.
Care should be taken to ensure that the new version has the same behaviour as the old version, for any code paths that in-flight invocations have successfully completed (ie, any changes must be from the point of failure onwards).
2. By updating the underlying code or with the update deployment API, change the active deployment to include the fix. Verify that this resolves the issue both for new invocations, and for those already failing.
It's common to notice failing invocations because they are preventing an old deployment from fully draining. In this case there are several concerns; the failing invocations on deployment 1, any failing invocations on deployment 2,
and the potential for new failing invocations to occur on deployment 2 as well. The following steps should be taken:
1. Develop a fix as above, based on the version backing deployment 1.
2. By updating the underlying code or with the update deployment API, change deployment 1 to include the fix. Verify that this resolves the failing invocations on this deployment.
3. Rebase the fix onto the version backing deployment 2.
4. By updating the underlying code or with the update deployment API, change deployment 2 to include the fix. Verify that this resolves any failing invocations, if any, new invocations.
It is possible to use the update deployment API to give a deployment the same URI/ARN as another deployment. This is useful where the an appropriate fix for a drained deployment has already been registered as a new deployment.
If this is done, there will be two deployments with the same endpoint, which is otherwise not allowed. It is strongly recommended that you delete one of the two deployments when the failing invocations have been resolved.
# Microservice Orchestration
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/tour/microservice-orchestration
Learn how to orchestrate microservices with durable execution, sagas, and async communication patterns.
export const GitHubLink = ({url}) =>
;
};
Microservice orchestration is about coordinating multiple services to complete complex business workflows. Restate provides powerful primitives for building resilient, observable orchestration patterns.
In this guide, you'll learn how to:
* Build durable, fault-tolerant service orchestrations with automatic failure recovery
* Implement sagas for distributed transactions with resilient compensation
* Use durable timers and external events for complex async patterns
* Implement stateful entities with Virtual Objects
Select your SDK:
## Getting Started
A Restate application is composed of two main components:
* **Restate Server**: The core engine that manages durable execution and orchestrates services. It acts as a message broker or reverse proxy in front of your services.
* **Your Services**: Your business logic, implemented as service handlers using the Restate SDK to perform durable operations.
A basic subscription service orchestration looks like this:
```ts src/getstarted/service.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-orchestration-typescript/src/getstarted/service.ts?collapse_prequel"} theme={null}
export const subscriptionService = restate.service({
name: "SubscriptionService",
handlers: {
add: async (ctx: Context, req: SubscriptionRequest) => {
const paymentId = ctx.rand.uuidv4();
const payRef = await ctx.run("pay", () =>
createRecurringPayment(req.creditCard, paymentId),
);
for (const subscription of req.subscriptions) {
await ctx.run(`add-${subscription}`, () =>
createSubscription(req.userId, subscription, payRef),
);
}
},
},
});
```
```java getstarted/SubscriptionService.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-orchestration-java/src/main/java/my/example/getstarted/SubscriptionService.java?collapse_prequel"} theme={null}
@Service
public class SubscriptionService {
@Handler
public void add(Context ctx, SubscriptionRequest req) {
var paymentId = ctx.random().nextUUID().toString();
String payRef =
ctx.run("pay", String.class, () -> createRecurringPayment(req.creditCard(), paymentId));
for (String subscription : req.subscriptions()) {
ctx.run("add-" + subscription, () -> createSubscription(req.userId(), subscription, payRef));
}
}
}
```
```go getstarted.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-orchestration-go/examples/getstarted.go?collapse_prequel"} theme={null}
type SubscriptionService struct{}
func (SubscriptionService) Add(ctx restate.Context, req SubscriptionRequest) error {
paymentId := restate.Rand(ctx).UUID().String()
payRef, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
return CreateRecurringPayment(req.CreditCard, paymentId)
}, restate.WithName("pay"))
if err != nil {
return err
}
for _, subscription := range req.Subscriptions {
_, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
return CreateSubscription(req.UserId, subscription, payRef)
}, restate.WithName(fmt.Sprintf("add-%s", subscription)))
if err != nil {
return err
}
}
return nil
}
```
```python app/getstarted/service.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-orchestration-python/app/getstarted/service.py?collapse_prequel"} theme={null}
subscription_service = restate.Service("SubscriptionService")
@subscription_service.handler()
async def add(ctx: restate.Context, req: SubscriptionRequest) -> None:
payment_id = str(ctx.uuid())
pay_ref = await ctx.run_typed(
"pay",
create_recurring_payment,
credit_card=req.credit_card,
payment_id=payment_id,
)
for subscription in req.subscriptions:
await ctx.run_typed(
f"add-{subscription}",
create_subscription,
user_id=req.user_id,
subscription=subscription,
payment_ref=pay_ref,
)
```
A service has handlers that can be called over HTTP. Each handler receives a `Context` object that provides durable execution primitives. Any action performed with the Context is automatically recorded and can survive failures.
You don't need to run your services in any special way. Restate works with how you already deploy your code, whether that's in Docker, on Kubernetes, or via AWS Lambda.
The endpoint that serves the services of this tour over HTTP is defined in `src/app.ts`.
The endpoint that serves the services of this tour over HTTP is defined in `AppMain.java`.
The endpoint that serves the services of this tour over HTTP is defined in `main.go`.
The endpoint that serves the services of this tour over HTTP is defined in `__main__.py`.
### Run the example
[Install Restate](/installation) and launch it:
```bash theme={null}
restate-server
```
Get the example:
```bash theme={null}
restate example typescript-tour-of-orchestration && cd typescript-tour-of-orchestration
npm install
```
Run the example:
```bash theme={null}
npm run dev
```
Then, tell Restate where your services are running via the UI (`http://localhost:9070`) or CLI:
```bash theme={null}
restate deployments register http://localhost:9080
```
This registers a set of services that we will be covering in this tutorial.
To invoke a handler, send a request to `restate-ingress/MyServiceName/handlerName`:
```bash theme={null}
curl localhost:8080/SubscriptionService/add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime"]}'
```
Get the example:
```bash theme={null}
restate example java-tour-of-orchestration && cd java-tour-of-orchestration
```
Run the example:
```bash theme={null}
./gradlew run
```
Then, tell Restate where your services are running via the UI (`http://localhost:9070`) or CLI:
```bash theme={null}
restate deployments register http://localhost:9080
```
This registers a set of services that we will be covering in this tutorial.
To invoke a handler, send a request to `restate-ingress/MyServiceName/handlerName`:
```bash theme={null}
curl localhost:8080/SubscriptionService/add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime"]}'
```
Get the example:
```bash theme={null}
restate example go-tour-of-orchestration && cd go-tour-of-orchestration
```
Run the example:
```bash theme={null}
go run .
```
Then, tell Restate where your services are running via the UI (`http://localhost:9070`) or CLI:
```bash theme={null}
restate deployments register http://localhost:9080
```
This registers a set of services that we will be covering in this tutorial.
To invoke a handler, send a request to `restate-ingress/MyServiceName/handlerName`:
```bash theme={null}
curl localhost:8080/SubscriptionService/Add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime"]}'
```
Get the example:
```bash theme={null}
restate example python-tour-of-orchestration && cd python-tour-of-orchestration
```
Run the example:
```bash theme={null}
uv run .
```
Then, tell Restate where your services are running via the UI (`http://localhost:9070`) or CLI:
```bash theme={null}
restate deployments register http://localhost:9080
```
This registers a set of services that we will be covering in this tutorial.
To invoke a handler, send a request to `restate-ingress/MyServiceName/handlerName`:
```bash theme={null}
curl localhost:8080/SubscriptionService/add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime"]}'
```
Click in the UI's invocations tab on the inovcation ID of your request to see the execution trace of your request.
## Durable Execution
Restate uses Durable Execution to ensure your orchestration logic survives failures and restarts.
Whenever a handler executes an action with the Restate `Context`, this gets send over to the Restate Server and persisted in a log.
On a failure or a crash, the Restate Server sends a retry request that contains the log of the actions that were executed so far.
The service then replays the log to restore state and continues executing the remaining actions.
This process continues until the handler runs till completion.
**Key Benefits:**
* Context `run` actions make external calls or non-deterministic operations durable. They get replayed on failures.
* If the service crashes after payment creation, it resumes at the subscription step
* Deterministic IDs logged with the context ensure operations are idempotent
* Full execution traces for debugging and monitoring
Try to add a subscription for Netflix:
```bash theme={null}
curl localhost:8080/SubscriptionService/add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime", "Netflix"]}'
```
On the invocation page in the UI, you can see that your request is retrying because the Netflix API is down:
To fix the problem, remove the line `failOnNetflix` from the `createSubscription` function in the `utils.ts` file:
```ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-orchestration-typescript/src/utils.ts#subscription"} theme={null}
export function createSubscription(
userId: string,
subscription: string,
_paymentRef: string,
): string {
failOnNetflix(subscription);
terminalErrorOnDisney(subscription);
console.log(`>>> Created subscription ${subscription} for user ${userId}`);
return "SUCCESS";
}
```
Try to add a subscription for Netflix:
```bash theme={null}
curl localhost:8080/SubscriptionService/add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime", "Netflix"]}'
```
On the invocation page in the UI, you can see that your request is retrying because the Netflix API is down:
To fix the problem, remove the line `failOnNetflix` from the `createSubscription` function in the `auxiliary/clients/SubscriptionClient.java` file:
```java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-orchestration-java/src/main/java/my/example/auxiliary/clients/SubscriptionClient.java#subscription"} theme={null}
public static String createSubscription(String userId, String subscription, String paymentRef) {
failOnNetflix(subscription);
terminalErrorOnDisney(subscription);
System.out.println(">>> Created subscription " + subscription + " for user " + userId);
return "SUCCESS";
}
```
Try to add a subscription for Netflix:
```bash theme={null}
curl localhost:8080/SubscriptionService/Add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime", "Netflix"]}'
```
On the invocation page in the UI, you can see that your request is retrying because the Netflix API is down:
To fix the problem, remove the line `failOnNetflix` from the `CreateSubscription` function in the `utils.go` file:
```go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-orchestration-go/examples/utils.go#subscription"} theme={null}
func CreateSubscription(userId, subscription, paymentRef string) (string, error) {
if err := failOnNetflix(subscription); err != nil {
return "", err
}
if err := terminalErrorOnDisney(subscription); err != nil {
return "", err
}
fmt.Printf(">>> Created subscription %s for user %s\n", subscription, userId)
return "SUCCESS", nil
}
```
Try to add a subscription for Netflix:
```bash theme={null}
curl localhost:8080/SubscriptionService/add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime", "Netflix"]}'
```
On the invocation page in the UI, you can see that your request is retrying because the Netflix API is down:
To fix the problem, remove the line `fail_on_netflix` from the `create_subscription` function in the `utils.py` file:
```python {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-orchestration-python/app/utils.py#subscription"} theme={null}
def create_subscription(user_id: str, subscription: str, payment_ref: str) -> str:
fail_on_netflix(subscription)
terminal_error_on_disney(subscription)
print(f">>> Created subscription {subscription} for user {user_id}")
return "SUCCESS"
```
Once you restart the service, the workflow finishes successfully:
## Error Handling
By default, Restate retries failures infinitely with an exponential backoff strategy.
For some failures, you might not want to retry or only retry a limited number of times.
For these cases, Restate distinguishes between two types of errors:
* **Transient Errors**: These are temporary issues that can be retried, such as network timeouts or service unavailability. Restate automatically retries these errors.
* **Terminal Errors**: These indicate a failure that will not be retried, such as invalid input or business logic violations. Restate stops execution and allows you to handle these errors gracefully.
Throw a terminal error in your handler to indicate a terminal failure:
```typescript {"CODE_LOAD::ts/src/tour/microservices/terminal_error.ts#terminal_error"} theme={null}
throw new TerminalError("Invalid credit card");
```
```java {"CODE_LOAD::java/src/main/java/tour/microservices/ErrorHandler.java#here"} theme={null}
throw new TerminalException("Invalid credit card");
```
```go {"CODE_LOAD::go/tour/microservices/errorhandling.go#here"} theme={null}
return restate.TerminalError(fmt.Errorf("invalid credit card"))
```
```python {"CODE_LOAD::python/src/tour/microservices/terminal_error.py#here"} theme={null}
from restate.exceptions import TerminalError
raise TerminalError("Invalid credit card")
```
Some actions let you configure their retry behavior, for example to limit the number of retries of a run block:
```ts {"CODE_LOAD::ts/src/tour/microservices/retries.ts#retries"} theme={null}
const retryPolicy = {
maxRetryAttempts: 3,
initialRetryIntervalMillis: 1000,
};
const payRef = await ctx.run(
"pay",
() => createRecurringPayment(req.creditCard, paymentId),
retryPolicy
);
```
```java {"CODE_LOAD::java/src/main/java/tour/microservices/Retries.java#here"} theme={null}
RetryPolicy myRunRetryPolicy =
RetryPolicy.defaultPolicy().setInitialDelay(Duration.ofSeconds(1)).setMaxAttempts(3);
String payRef =
ctx.run(
"pay",
String.class,
myRunRetryPolicy,
() -> createRecurringPayment(req.creditCard(), paymentId));
```
```go {"CODE_LOAD::go/tour/microservices/retries.go#here"} theme={null}
result, err := restate.Run(ctx,
func(ctx restate.RunContext) (string, error) {
return createRecurringPayment(req.CreditCard, paymentId)
},
restate.WithInitialRetryInterval(time.Millisecond*100),
restate.WithMaxRetryAttempts(3),
restate.WithName("pay"),
)
if err != nil {
return err
}
```
```python {"CODE_LOAD::python/src/tour/microservices/retries.py#here"} theme={null}
retry_opts = restate.RunOptions(
max_attempts=10, max_retry_duration=timedelta(seconds=30)
)
pay_ref = await ctx.run_typed(
"pay",
lambda: create_recurring_payment(req["creditCard"], payment_id),
retry_opts,
)
```
When the retries are exhausted, the run block will throw a `TerminalError`, that you can handle in your handler logic.
Learn more with the [Error Handling Guide](/guides/error-handling).
## Sagas and Rollback
On a terminal failure, Restate stops the execution of the handler.
You might, however, want to roll back the changes made by the workflow to keep your system in a consistent state.
This is where Sagas come in.
Sagas are a pattern for rolling back changes made by a handler when it fails.
In Restate, you can implement a saga by building a list of compensating actions for each step of the workflow.
On a terminal failure, you execute them in reverse order:
```ts src/sagas/service.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-orchestration-typescript/src/sagas/service.ts?collapse_prequel"} theme={null}
export const subscriptionSaga = restate.service({
name: "SubscriptionSaga",
handlers: {
add: async (ctx: Context, req: SubscriptionRequest) => {
const compensations = [];
try {
const paymentId = ctx.rand.uuidv4();
compensations.push(() =>
ctx.run("undo-pay", () => removeRecurringPayment(paymentId)),
);
const payRef = await ctx.run("pay", () =>
createRecurringPayment(req.creditCard, paymentId),
);
for (const subscription of req.subscriptions) {
compensations.push(() =>
ctx.run(`undo-${subscription}`, () =>
removeSubscription(req.userId, subscription),
),
);
await ctx.run(`add-${subscription}`, () =>
createSubscription(req.userId, subscription, payRef),
);
}
} catch (e) {
if (e instanceof restate.TerminalError) {
for (const compensation of compensations.reverse()) {
await compensation();
}
}
throw e;
}
},
},
});
```
```java sagas/SubscriptionSaga.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-orchestration-java/src/main/java/my/example/sagas/SubscriptionSaga.java?collapse_prequel"} theme={null}
@Service
public class SubscriptionSaga {
@Handler
public void add(Context ctx, SubscriptionRequest req) {
List compensations = new ArrayList<>();
try {
var paymentId = ctx.random().nextUUID().toString();
compensations.add(
() -> ctx.run("undo-pay", () -> PaymentClient.removeRecurringPayment(paymentId)));
String payRef =
ctx.run(
"pay",
String.class,
() -> PaymentClient.createRecurringPayment(req.creditCard(), paymentId));
for (String subscription : req.subscriptions()) {
compensations.add(
() ->
ctx.run(
"undo-" + subscription,
() -> SubscriptionClient.removeSubscription(req.userId(), subscription)));
ctx.run(
"add-" + subscription,
() -> SubscriptionClient.createSubscription(req.userId(), subscription, payRef));
}
} catch (TerminalException e) {
// Run compensations in reverse order
Collections.reverse(compensations);
for (Runnable compensation : compensations) {
compensation.run();
}
throw e;
}
}
}
```
```go sagas.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-orchestration-go/examples/sagas.go?collapse_prequel"} theme={null}
type SubscriptionSaga struct{}
func (SubscriptionSaga) Add(ctx restate.Context, req SubscriptionRequest) (err error) {
var compensations []func() error
// Run compensations at the end if err != nil
defer func() {
if err != nil {
for _, compensation := range slices.Backward(compensations) {
if compErr := compensation(); compErr != nil {
err = compErr
}
}
}
}()
paymentId := restate.Rand(ctx).UUID().String()
// Add compensation for payment
compensations = append(compensations, func() error {
_, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return RemoveRecurringPayment(paymentId)
}, restate.WithName("undo-pay"))
return err
})
// Create payment
payRef, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
return CreateRecurringPayment(req.CreditCard, paymentId)
}, restate.WithName("pay"))
if err != nil {
return err
}
// Process subscriptions
for _, subscription := range req.Subscriptions {
// Add compensation for this subscription
sub := subscription // Capture loop variable
compensations = append(compensations, func() error {
_, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return RemoveSubscription(req.UserId, sub)
}, restate.WithName(fmt.Sprintf("undo-%s", sub)))
return err
})
// Create subscription
_, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
return CreateSubscription(req.UserId, subscription, payRef)
}, restate.WithName(fmt.Sprintf("add-%s", subscription)))
if err != nil {
return err
}
}
return nil
}
```
```python app/sagas/service.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-orchestration-python/app/sagas/service.py?collapse_prequel"} theme={null}
subscription_saga = restate.Service("SubscriptionSaga")
@subscription_saga.handler()
async def add(ctx: restate.Context, req: SubscriptionRequest) -> None:
compensations = []
try:
payment_id = str(ctx.uuid())
# Add compensation for payment
compensations.append(
lambda: ctx.run_typed(
"undo-pay", remove_recurring_payment, payment_id=payment_id
)
)
# Create payment
pay_ref = await ctx.run_typed(
"pay",
create_recurring_payment,
credit_card=req.credit_card,
payment_id=payment_id,
)
# Process subscriptions
for subscription in req.subscriptions:
# Add compensation for this subscription
compensations.append(
lambda s=subscription: ctx.run_typed(
f"undo-{s}",
remove_subscription,
user_id=req.user_id,
subscription=s,
)
)
# Create subscription
await ctx.run_typed(
f"add-{subscription}",
create_subscription,
user_id=req.user_id,
subscription=subscription,
payment_ref=pay_ref,
)
except restate.TerminalError as e:
# Run compensations in reverse order
for compensation in reversed(compensations):
await compensation()
raise e
```
**Benefits with Restate:**
* The list of compensations can be recovered after a crash, and Restate knows which compensations still need to be run.
* Sagas always run till completion (success or complete rollback)
* Full trace of all operations and compensations
* No complex state machines needed
Add a subscription for Disney:
```bash theme={null}
curl localhost:8080/SubscriptionSaga/add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime", "Disney"]}'
```
```bash theme={null}
curl localhost:8080/SubscriptionSaga/add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime", "Disney"]}'
```
```bash theme={null}
curl localhost:8080/SubscriptionSaga/Add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime", "Disney"]}'
```
```bash theme={null}
curl localhost:8080/SubscriptionSaga/add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime", "Disney"]}'
```
The Disney subscription is not available, so the handler will fail and run compensations:
Learn more with the [Sagas Guide](/guides/sagas).
## Virtual Objects
Until now, the services we looked at did not share any state between requests.
To implement stateful entities like shopping carts, user profiles, or AI agents, Restate provides **Virtual Objects**.
Each Virtual Object instance maintains isolated state and is identified by a unique key.
Here is an example of a Virtual Object that tracks user subscriptions:
```ts src/objects/service.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-orchestration-typescript/src/objects/service.ts?collapse_prequel"} theme={null}
export const userSubscriptions = restate.object({
name: "UserSubscriptions",
handlers: {
add: async (ctx: ObjectContext, subscription: string) => {
// Get current subscriptions
const subscriptions = (await ctx.get("subscriptions")) ?? [];
// Add new subscription
if (!subscriptions.includes(subscription)) {
subscriptions.push(subscription);
}
ctx.set("subscriptions", subscriptions);
// Update metrics
ctx.set("lastUpdated", await ctx.date.toJSON());
},
getSubscriptions: restate.handlers.object.shared(
async (ctx: ObjectSharedContext) => {
return (await ctx.get("subscriptions")) ?? [];
},
),
},
});
```
Virtual Objects are ideal for implementing any entity with mutable state:
* **Long-lived state**: K/V state is stored permanently. It has no automatic expiry. Clear it via `ctx.clear()`.
* **Durable state changes**: State changes are logged with Durable Execution, so they survive failures and are consistent with code execution
* **State is queryable** via the state tab in the UI:
* **Built-in concurrency control**: Restate’s Virtual Objects have built-in queuing and consistency guarantees per object key. Handlers either have read-write access (`ObjectContext`) or read-only access (shared object context).
* Only one handler with write access can run at a time per object key to prevent concurrent/lost writes or race conditions.
* Handlers with read-only access can run concurrently to the write-access handlers.
```java objects/UserSubscriptions.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-orchestration-java/src/main/java/my/example/objects/UserSubscriptions.java?collapse_prequel"} theme={null}
@VirtualObject
public class UserSubscriptions {
private static final StateKey> SUBSCRIPTIONS =
StateKey.of("subscriptions", new TypeRef<>() {});
private static final StateKey LAST_UPDATED = StateKey.of("lastUpdated", String.class);
@Handler
public void add(ObjectContext ctx, String subscription) {
// Get current subscriptions
Set subscriptions = ctx.get(SUBSCRIPTIONS).orElse(new HashSet<>());
// Add new subscription
subscriptions.add(subscription);
ctx.set(SUBSCRIPTIONS, subscriptions);
// Update metrics
ctx.set(LAST_UPDATED, Instant.now().toString());
}
@Shared
public Set getSubscriptions(SharedObjectContext ctx) {
return ctx.get(SUBSCRIPTIONS).orElse(Set.of());
}
}
```
Virtual Objects are ideal for implementing any entity with mutable state:
* **Long-lived state**: K/V state is stored permanently. It has no automatic expiry. Clear it via `ctx.clear()`.
* **Durable state changes**: State changes are logged with Durable Execution, so they survive failures and are consistent with code execution
* **State is queryable** via the state tab in the UI:
* **Built-in concurrency control**: Restate’s Virtual Objects have built-in queuing and consistency guarantees per object key. Handlers either have read-write access (`ObjectContext`) or read-only access (shared object context).
* Only one handler with write access can run at a time per object key to prevent concurrent/lost writes or race conditions.
* Handlers with read-only access can run concurrently to the write-access handlers.
```go objects.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-orchestration-go/examples/objects.go?collapse_prequel"} theme={null}
type UserSubscriptions struct{}
func (UserSubscriptions) Add(ctx restate.ObjectContext, subscription string) error {
// Get current subscriptions
subscriptions, err := restate.Get[[]string](ctx, "subscriptions")
if err != nil {
return err
}
if subscriptions == nil {
subscriptions = []string{}
}
// Add new subscription if not already present
found := false
for _, sub := range subscriptions {
if sub == subscription {
found = true
break
}
}
if !found {
subscriptions = append(subscriptions, subscription)
}
// Save subscriptions
restate.Set(ctx, "subscriptions", subscriptions)
// Update metrics
restate.Set(ctx, "lastUpdated", time.Now().Format(time.RFC3339))
return nil
}
func (UserSubscriptions) GetSubscriptions(ctx restate.ObjectSharedContext) ([]string, error) {
return restate.Get[[]string](ctx, "subscriptions")
}
```
Virtual Objects are ideal for implementing any entity with mutable state:
* **Long-lived state**: K/V state is stored permanently. It has no automatic expiry. Clear it via `restate.Clear(ctx, "my-key")`.
* **Durable state changes**: State changes are logged with Durable Execution, so they survive failures and are consistent with code execution
* **State is queryable** via the state tab in the UI:
* **Built-in concurrency control**: Restate’s Virtual Objects have built-in queuing and consistency guarantees per object key. Handlers either have read-write access (`ObjectContext`) or read-only access (shared object context).
* Only one handler with write access can run at a time per object key to prevent concurrent/lost writes or race conditions.
* Handlers with read-only access can run concurrently to the write-access handlers.
```python app/objects/service.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-orchestration-python/app/objects/service.py?collapse_prequel"} theme={null}
user_subscriptions = restate.VirtualObject("UserSubscriptions")
@user_subscriptions.handler()
async def add(ctx: restate.ObjectContext, subscription: str) -> None:
# Get current subscriptions
subscriptions = await ctx.get("subscriptions") or []
# Add new subscription
if subscription not in subscriptions:
subscriptions.append(subscription)
ctx.set("subscriptions", subscriptions)
# Update metrics
ctx.set("lastUpdated", datetime.now().isoformat())
@user_subscriptions.handler("getSubscriptions")
async def get_subscriptions(ctx: restate.ObjectSharedContext) -> List[str]:
return await ctx.get("subscriptions") or []
```
Virtual Objects are ideal for implementing any entity with mutable state:
* **Long-lived state**: K/V state is stored permanently. It has no automatic expiry. Clear it via `ctx.clear()`.
* **Durable state changes**: State changes are logged with Durable Execution, so they survive failures and are consistent with code execution
* **State is queryable** via the state tab in the UI:
* **Built-in concurrency control**: Restate’s Virtual Objects have built-in queuing and consistency guarantees per object key. Handlers either have read-write access (`ObjectContext`) or read-only access (shared object context).
* Only one handler with write access can run at a time per object key to prevent concurrent/lost writes or race conditions.
* Handlers with read-only access can run concurrently to the write-access handlers.
Add a few subscriptions for some users.
To call a Virtual Object, you specify the object key in the URL (here `user-123` and `user-456`):
```bash theme={null}
curl localhost:8080/UserSubscriptions/user-123/add --json '"Hulu"'
curl localhost:8080/UserSubscriptions/user-123/add --json '"Prime"'
curl localhost:8080/UserSubscriptions/user-123/add --json '"Disney"'
curl localhost:8080/UserSubscriptions/user-456/add --json '"Netflix"'
```
Get the subscriptions for `user-123`:
```bash theme={null}
curl localhost:8080/UserSubscriptions/user-123/getSubscriptions
```
```bash theme={null}
curl localhost:8080/UserSubscriptions/user-123/add --json '"Hulu"'
curl localhost:8080/UserSubscriptions/user-123/add --json '"Prime"'
curl localhost:8080/UserSubscriptions/user-123/add --json '"Disney"'
curl localhost:8080/UserSubscriptions/user-456/add --json '"Netflix"'
```
Get the subscriptions for `user-123`:
```bash theme={null}
curl localhost:8080/UserSubscriptions/user-123/getSubscriptions
```
```bash theme={null}
curl localhost:8080/UserSubscriptions/user-123/Add --json '"Hulu"'
curl localhost:8080/UserSubscriptions/user-123/Add --json '"Prime"'
curl localhost:8080/UserSubscriptions/user-123/Add --json '"Disney"'
curl localhost:8080/UserSubscriptions/user-456/Add --json '"Netflix"'
```
Get the subscriptions for `user-123`:
```bash theme={null}
curl localhost:8080/UserSubscriptions/user-123/GetSubscriptions
```
```bash theme={null}
curl localhost:8080/UserSubscriptions/user-123/add --json '"Hulu"'
curl localhost:8080/UserSubscriptions/user-123/add --json '"Prime"'
curl localhost:8080/UserSubscriptions/user-123/add --json '"Disney"'
curl localhost:8080/UserSubscriptions/user-456/add --json '"Netflix"'
```
Get the subscriptions for `user-123`:
```bash theme={null}
curl localhost:8080/UserSubscriptions/user-123/getSubscriptions
```
Or use the UI's state tab to explore the object state.
## Resilient Communication
The Restate SDK includes clients to call other handlers reliably. You can call another handler in three ways:
* **Request-Response**: Wait for a response
* **One-Way Messages**: Fire-and-forget
* **Delayed Messages**: Schedule for later
When you call another handler, the Restate Server acts as a message broker.
All communication is proxied via the Restate Server where it gets durably logged and retried till completion.
Imagine a handler which processes a concert ticket purchase, and calls multiple services to handle payment, ticket delivery, and reminders:
```ts src/communication/service.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-orchestration-typescript/src/communication/service.ts?collapse_prequel"} theme={null}
export const concertTicketingService = restate.service({
name: "ConcertTicketingService",
handlers: {
buy: async (ctx: Context, req: PurchaseTicketRequest) => {
// Request-response call - wait for payment to complete
const payRef = await ctx.serviceClient(paymentService).charge(req);
// One-way message - fire and forget ticket delivery
ctx.serviceSendClient(emailService).emailTicket(req);
// Delayed message - schedule reminder for day before concert
ctx
.serviceSendClient(emailService)
.sendReminder(req, sendOpts({ delay: dayBefore(req.concertDate) }));
return `Ticket purchased successfully with payment reference: ${payRef}`;
},
},
});
```
```java communication/ConcertTicketingService.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-orchestration-java/src/main/java/my/example/communication/ConcertTicketingService.java?collapse_prequel"} theme={null}
@Service
public class ConcertTicketingService {
@Handler
public String buy(Context ctx, PurchaseTicketRequest req) {
// Request-response call - wait for payment to complete
String payRef = PaymentServiceClient.fromContext(ctx).charge(req).await();
// One-way message - fire and forget ticket delivery
EmailServiceClient.fromContext(ctx).send().emailTicket(req);
// Delayed message - schedule reminder for day before concert
EmailServiceClient.fromContext(ctx).send().sendReminder(req, req.dayBefore());
return "Ticket purchased successfully with payment reference: " + payRef;
}
}
```
```go communication.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-orchestration-go/examples/communication.go?collapse_prequel"} theme={null}
type ConcertTicketingService struct{}
func (ConcertTicketingService) Buy(ctx restate.Context, req PurchaseTicketRequest) (string, error) {
// Request-response call - wait for payment to complete
payRef, err := restate.Service[string](ctx, "PaymentService", "Charge").Request(req)
if err != nil {
return "", err
}
// One-way message - fire and forget ticket delivery
restate.Service[restate.Void](ctx, "EmailService", "EmailTicket").Send(req)
// Delayed message - schedule reminder for day before concert
delay := DayBefore(req.ConcertDate)
restate.Service[restate.Void](ctx, "EmailService", "SendReminder").
Send(req, restate.WithDelay(delay))
return fmt.Sprintf("Ticket purchased successfully with payment reference: %s", payRef), nil
}
```
```python app/communication/service.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-orchestration-python/app/communication/service.py?collapse_prequel"} theme={null}
concert_ticketing_service = restate.Service("ConcertTicketingService")
@concert_ticketing_service.handler()
async def buy(ctx: restate.Context, req: PurchaseTicketRequest) -> str:
# Request-response call - wait for payment to complete
pay_ref = await ctx.service_call(charge, req)
# One-way message - fire and forget ticket delivery
ctx.service_send(email_ticket, req)
# Delayed message - schedule reminder for day before concert
ctx.service_send(send_reminder, req, send_delay=day_before(req.concert_date))
return f"Ticket purchased successfully with payment reference: {pay_ref}"
```
Each of these calls gets persisted in Restate's log and will be retried upon failures.
The handler can finish execution without waiting for the ticket delivery or reminder to complete.
You can use Restate's communication primitives to implement microservices that communicate reliably and scale independently.
Buy a concert ticket:
```bash theme={null}
curl localhost:8080/ConcertTicketingService/buy --json '{
"ticketId": "ticket-789",
"price": 100,
"customerEmail": "me@mail.com",
"concertDate": "2026-10-01T20:00:00Z"
}'
```
```bash theme={null}
curl localhost:8080/ConcertTicketingService/buy --json '{
"ticketId": "ticket-789",
"price": 100,
"customerEmail": "me@mail.com",
"concertDate": "2026-10-01T20:00:00Z"
}'
```
```bash theme={null}
curl localhost:8080/ConcertTicketingService/Buy --json '{
"ticketId": "ticket-789",
"price": 100,
"customerEmail": "me@mail.com",
"concertDate": "2026-10-01T20:00:00Z"
}'
```
```bash theme={null}
curl localhost:8080/ConcertTicketingService/buy --json '{
"ticketId": "ticket-789",
"price": 100,
"customerEmail": "me@mail.com",
"concertDate": "2026-10-01T20:00:00Z"
}'
```
See in the UI how the first call had the response logged, while the ticket delivery happened asynchronously and the reminder was scheduled for in 406 days:
## Request Idempotency
Restate allows adding an idempotency header to your requests. It will then deduplicate requests with the same idempotency key, ensuring that they only execute once.
This can help us prevent duplicate calls the concert ticketing service if the user accidentally clicks "buy" multiple times.
Add an idempotency header to your request:
```shell theme={null}
curl -X POST localhost:8080/ConcertTicketingService/buy \
-H 'Idempotency-Key: unique-key-123' \
--json '{"ticketId": "ticket-789", "price": 100, "customerEmail": "me@mail.com", "concertDate": "2023-10-01T20:00:00Z"}'
```
```shell theme={null}
curl -X POST localhost:8080/ConcertTicketingService/buy \
-H 'Idempotency-Key: unique-key-123' \
--json '{"ticketId": "ticket-789", "price": 100, "customerEmail": "me@mail.com", "concertDate": "2023-10-01T20:00:00Z"}'
```
```shell theme={null}
curl -X POST localhost:8080/ConcertTicketingService/Buy \
-H 'Idempotency-Key: unique-key-123' \
--json '{"ticketId": "ticket-789", "price": 100, "customerEmail": "me@mail.com", "concertDate": "2023-10-01T20:00:00Z"}'
```
```shell theme={null}
curl -X POST localhost:8080/ConcertTicketingService/buy \
-H 'Idempotency-Key: unique-key-123' \
--json '{"ticketId": "ticket-789", "price": 100, "customerEmail": "me@mail.com", "concertDate": "2023-10-01T20:00:00Z"}'
```
Notice how doing the same request with the same idempotency key will print the same payment reference.
Instead of executing the handler again, Restate returns the result of the first execution.
## External Events
Until now we showed either synchronous API calls via `run` or calls to other Restate services.
Another common scenario is APIs that respond asynchronously via webhooks or callbacks.
For this, you can use Restate's awakeables.
For example, some payment providers like Stripe require you to initiate a payment and then wait for their webhook to confirm the transaction.
```ts src/events/service.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-orchestration-typescript/src/events/service.ts?collapse_prequel"} theme={null}
export const payments = restate.service({
name: "Payments",
handlers: {
process: async (ctx: Context, req: PaymentRequest) => {
// Create awakeable to wait for webhook payment confirmation
const confirmation = ctx.awakeable();
// Initiate payment with external provider (Stripe, PayPal, etc.)
const paymentId = ctx.rand.uuidv4();
await ctx.run("pay", () => initPayment(req, paymentId, confirmation.id));
// Wait for external payment provider to call our webhook
return confirmation.promise;
},
// Webhook handler called by external payment provider
confirm: async (
ctx: Context,
confirmation: { id: string; result: PaymentResult },
) => {
// Resolve the awakeable to continue the payment flow
ctx.resolveAwakeable(confirmation.id, confirmation.result);
},
},
});
```
```java events/Payments.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-orchestration-java/src/main/java/my/example/events/Payments.java?collapse_prequel"} theme={null}
@Service
public class Payments {
@Handler
public PaymentResult process(Context ctx, PaymentRequest req) {
// Create awakeable to wait for webhook payment confirmation
var confirmation = ctx.awakeable(PaymentResult.class);
// Initiate payment with external provider (Stripe, PayPal, etc.)
var paymentId = ctx.random().nextUUID().toString();
ctx.run(() -> initPayment(req, paymentId, confirmation.id()));
// Wait for external payment provider to call our webhook
return confirmation.await();
}
// Webhook handler called by external payment provider
@Handler
public void confirm(Context ctx, ConfirmationRequest confirmation) {
// Resolve the awakeable to continue the payment flow
ctx.awakeableHandle(confirmation.id()).resolve(PaymentResult.class, confirmation.result());
}
}
```
```go events.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-orchestration-go/examples/events.go?collapse_prequel"} theme={null}
type Payments struct{}
func (Payments) Process(ctx restate.Context, req PaymentRequest) (PaymentResult, error) {
// Create awakeable to wait for webhook payment confirmation
confirmation := restate.Awakeable[PaymentResult](ctx)
// Initiate payment with external provider (Stripe, PayPal, etc.)
paymentId := restate.Rand(ctx).UUID().String()
_, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
return InitPayment(req, paymentId, confirmation.Id())
}, restate.WithName("pay"))
if err != nil {
return PaymentResult{}, err
}
// Wait for external payment provider to call our webhook
return confirmation.Result()
}
// Webhook handler called by external payment provider
func (Payments) Confirm(ctx restate.Context, confirmation ConfirmationRequest) error {
// Resolve the awakeable to continue the payment flow
restate.ResolveAwakeable(ctx, confirmation.Id, confirmation.Result)
return nil
}
```
```python app/events/service.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-orchestration-python/app/events/service.py?collapse_prequel"} theme={null}
payments = restate.Service("Payments")
@payments.handler()
async def process(ctx: restate.Context, req: PaymentRequest) -> PaymentResult:
# Create awakeable to wait for webhook payment confirmation
confirmation_id, confirmation_promise = ctx.awakeable(type_hint=PaymentResult)
# Initiate payment with external provider (Stripe, PayPal, etc.)
payment_id = str(ctx.uuid())
await ctx.run_typed(
"pay",
init_payment,
req=req,
payment_id=payment_id,
confirmation_id=confirmation_id,
)
# Wait for external payment provider to call our webhook
return await confirmation_promise
@payments.handler()
async def confirm(ctx: restate.Context, confirmation: ConfirmationRequest) -> None:
# Resolve the awakeable to continue the payment flow
ctx.resolve_awakeable(confirmation.id, confirmation.result)
```
Awakeables are like promises or futures that can be recovered after a crash.
Restate persists the awakeable in its log and can recover it on another process when needed.
There is no limit to how long you can persist an awakeable, so you can wait for external events that may take hours, or even months to arrive.
You can also use awakeables to implement human-in-the-loop interactions, such as waiting for user input or approvals.
Initiate a payment by calling the `process` handler. Add `/send` to call the handler without waiting for the response:
```bash theme={null}
curl localhost:8080/Payments/process/send \
--json '{"amount": 100, "currency": "USD", "customerId": "cust-123", "orderId": "order-456"}'
```
```bash theme={null}
curl localhost:8080/Payments/process/send \
--json '{"amount": 100, "currency": "USD", "customerId": "cust-123", "orderId": "order-456"}'
```
```bash theme={null}
curl localhost:8080/Payments/Process/send \
--json '{"amount": 100, "currency": "USD", "customerId": "cust-123", "orderId": "order-456"}'
```
```bash theme={null}
curl localhost:8080/Payments/process/send \
--json '{"amount": 100, "currency": "USD", "customerId": "cust-123", "orderId": "order-456"}'
```
In the UI, you can see that the payment is waiting for confirmation.
You can restart the service to see how Restate continues waiting for the payment confirmation.
Simulate approving the payment by executing the **curl request that was printed in the service logs**, similar to:
```bash theme={null}
curl localhost:8080/Payments/confirm \
--json '{"id": "sign_1PrDkbECjgdsBmMfEUQyCnioCP-csLbd2AAAAEQ", "result": {"success": true, "transactionId": "txn-123"}}'
```
```bash theme={null}
curl localhost:8080/Payments/confirm \
--json '{"id": "sign_1PrDkbECjgdsBmMfEUQyCnioCP-csLbd2AAAAEQ", "result": {"success": true, "transactionId": "txn-123"}}'
```
```bash theme={null}
curl localhost:8080/Payments/Confirm \
--json '{"id": "sign_1PrDkbECjgdsBmMfEUQyCnioCP-csLbd2AAAAEQ", "result": {"success": true, "transactionId": "txn-123"}}'
```
```bash theme={null}
curl localhost:8080/Payments/confirm \
--json '{"id": "sign_1PrDkbECjgdsBmMfEUQyCnioCP-csLbd2AAAAEQ", "result": {"success": true, "transactionId": "txn-123"}}'
```
You can see in the UI that the payment was processed successfully and the awakeable was resolved:
## Durable Timers
Waiting on external events might take a long time, and you might want to add timeouts to operations like this.
The Restate SDK offers durable timer implementations that you can use to limit waiting for an action.
Restate tracks these timers so they survive crashes and do not restart from the beginning.
Let's extend our payment service to automatically cancel payments that don't complete within a reasonable time:
```ts src/timers/service.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-orchestration-typescript/src/timers/service.ts?collapse_prequel"} theme={null}
export const paymentsWithTimeout = restate.service({
name: "PaymentsWithTimeout",
handlers: {
process: async (ctx: Context, req: PaymentRequest) => {
const confirmation = ctx.awakeable();
const paymentId = ctx.rand.uuidv4();
const payRef = await ctx.run("pay", () =>
initPayment(req, paymentId, confirmation.id),
);
// Race between payment confirmation and timeout
try {
return await confirmation.promise.orTimeout({ seconds: 30 });
} catch (e) {
if (e instanceof TimeoutError) {
// Cancel the payment with external provider
await ctx.run("cancel-payment", () => cancelPayment(payRef));
return {
success: false,
errorMessage: "Payment timeout",
};
}
throw e;
}
},
confirm: async (
ctx: Context,
confirmation: { id: string; result: PaymentResult },
) => {
ctx.resolveAwakeable(confirmation.id, confirmation.result);
},
},
});
```
```java timers/PaymentsWithTimeout.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-orchestration-java/src/main/java/my/example/timers/PaymentsWithTimeout.java?collapse_prequel"} theme={null}
@Service
public class PaymentsWithTimeout {
@Handler
public PaymentResult process(Context ctx, PaymentRequest req) {
var confirmation = ctx.awakeable(PaymentResult.class);
var paymentId = ctx.random().nextUUID().toString();
String payRef =
ctx.run("pay", String.class, () -> initPayment(req, paymentId, confirmation.id()));
try {
return confirmation.await(Duration.ofSeconds(30));
} catch (TimeoutException e) {
ctx.run("cancel-payment", () -> cancelPayment(payRef));
return new PaymentResult(false, null, "Payment timeout");
}
}
@Handler
public void confirm(Context ctx, ConfirmationRequest confirmation) {
ctx.awakeableHandle(confirmation.id()).resolve(PaymentResult.class, confirmation.result());
}
}
```
```go timers.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-orchestration-go/examples/timers.go?collapse_prequel"} theme={null}
type PaymentsWithTimeout struct{}
func (PaymentsWithTimeout) Process(ctx restate.Context, req PaymentRequest) (PaymentResult, error) {
confirmation := restate.Awakeable[PaymentResult](ctx)
paymentId := restate.Rand(ctx).UUID().String()
payRef, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
return InitPayment(req, paymentId, confirmation.Id())
}, restate.WithName("pay"))
if err != nil {
return PaymentResult{}, err
}
// Race between payment confirmation and timeout
timeout := restate.After(ctx, 30*time.Second)
selector := restate.Select(ctx, confirmation, timeout)
switch selector.Select() {
case confirmation:
return confirmation.Result()
default:
if err := timeout.Done(); err != nil {
return PaymentResult{}, err
}
// Cancel the payment with external provider
_, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return CancelPayment(payRef)
}, restate.WithName("cancel-payment"))
if err != nil {
return PaymentResult{}, err
}
return PaymentResult{
Success: false,
ErrorMessage: "Payment timeout",
}, nil
}
}
func (PaymentsWithTimeout) Confirm(ctx restate.Context, confirmation ConfirmationRequest) error {
restate.ResolveAwakeable(ctx, confirmation.Id, confirmation.Result)
return nil
}
```
```python app/timers/service.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-orchestration-python/app/timers/service.py?collapse_prequel"} theme={null}
payments_with_timeout = restate.Service("PaymentsWithTimeout")
@payments_with_timeout.handler()
async def process(ctx: restate.Context, req: PaymentRequest) -> PaymentResult:
confirmation_id, confirmation_promise = ctx.awakeable(type_hint=PaymentResult)
payment_id = str(ctx.uuid())
pay_ref = await ctx.run_typed(
"pay",
init_payment,
req=req,
payment_id=payment_id,
confirmation_id=confirmation_id,
)
# Race between payment confirmation and timeout
match await restate.select(
confirmation=confirmation_promise, timeout=ctx.sleep(timedelta(seconds=30))
):
case ["confirmation", result]:
return result
case _:
# Cancel the payment with external provider
await ctx.run_typed("cancel-payment", cancel_payment, pay_ref=pay_ref)
return PaymentResult(
success=False, transaction_id=None, error_message="Payment timeout"
)
@payments_with_timeout.handler()
async def confirm(ctx: restate.Context, confirmation: ConfirmationRequest) -> None:
ctx.resolve_awakeable(confirmation.id, confirmation.result)
```
You can also set timeouts for RPC calls or other asynchronous operations with the Restate SDK.
Initiate a payment by calling the `process` handler. Add `/send` to call the handler without waiting for the response:
```bash theme={null}
curl localhost:8080/PaymentsWithTimeout/process/send \
--json '{"amount": 100, "currency": "USD", "customerId": "cust-123", "orderId": "order-456"}'
```
```bash theme={null}
curl localhost:8080/PaymentsWithTimeout/process/send \
--json '{"amount": 100, "currency": "USD", "customerId": "cust-123", "orderId": "order-456"}'
```
```bash theme={null}
curl localhost:8080/PaymentsWithTimeout/Process/send \
--json '{"amount": 100, "currency": "USD", "customerId": "cust-123", "orderId": "order-456"}'
```
```bash theme={null}
curl localhost:8080/PaymentsWithTimeout/process/send \
--json '{"amount": 100, "currency": "USD", "customerId": "cust-123", "orderId": "order-456"}'
```
Wait for 30 seconds without confirming the payment.
Try restarting the service while the payment is waiting for confirmation to see how Restate continues waiting for the timer and the confirmation.
In the UI, you can see that the payment times out and cancels the payment:
## Concurrent Tasks
When you are waiting on an awakeable or a timer, you are effectively running concurrent tasks and waiting for one of them to complete.
Restate allows more advanced concurrency patterns to run tasks in parallel and wait for their results.
Let's extend our subscription service to process all subscriptions concurrently and handle failures gracefully:
```ts src/concurrenttasks/service.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-orchestration-typescript/src/concurrenttasks/service.ts?collapse_prequel"} theme={null}
export const parallelSubscriptionService = restate.service({
name: "ParallelSubscriptionService",
handlers: {
add: async (ctx: Context, req: SubscriptionRequest) => {
const paymentId = ctx.rand.uuidv4();
const payRef = await ctx.run("pay", () =>
createRecurringPayment(req.creditCard, paymentId),
);
// Start all subscriptions in parallel
const subscriptionPromises = [];
for (const subscription of req.subscriptions) {
subscriptionPromises.push(
ctx.run(`add-${subscription}`, () =>
createSubscription(req.userId, subscription, payRef),
),
);
}
// Wait for all subscriptions to complete
await RestatePromise.all(subscriptionPromises);
return { success: true, paymentRef: payRef };
},
},
});
```
```java concurrenttasks/ParallelSubscriptionService.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-orchestration-java/src/main/java/my/example/concurrenttasks/ParallelSubscriptionService.java?collapse_prequel"} theme={null}
@Service
public class ParallelSubscriptionService {
@Handler
public SubscriptionResult add(Context ctx, SubscriptionRequest req) {
var paymentId = ctx.random().nextUUID().toString();
var payRef =
ctx.run("pay", String.class, () -> createRecurringPayment(req.creditCard(), paymentId));
// Start all subscriptions in parallel
List> subscriptionFutures = new ArrayList<>();
for (String subscription : req.subscriptions()) {
subscriptionFutures.add(
ctx.runAsync(
"add-" + subscription, () -> createSubscription(req.userId(), subscription, payRef)));
}
// Wait for all subscriptions to complete
DurableFuture.all(subscriptionFutures).await();
return new SubscriptionResult(true, payRef);
}
}
```
```go concurrenttasks.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-orchestration-go/examples/concurrenttasks.go?collapse_prequel"} theme={null}
type ParallelSubscriptionService struct{}
func (ParallelSubscriptionService) Add(ctx restate.Context, req SubscriptionRequest) (SubscriptionResult, error) {
paymentId := restate.Rand(ctx).UUID().String()
payRef, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
return CreateRecurringPayment(req.CreditCard, paymentId)
}, restate.WithName("pay"))
if err != nil {
return SubscriptionResult{}, err
}
// Process all subscriptions sequentially
var subscriptionFutures []restate.Selectable
for _, subscription := range req.Subscriptions {
future := restate.RunAsync(ctx, func(ctx restate.RunContext) (string, error) {
return CreateSubscription(req.UserId, subscription, payRef)
}, restate.WithName(fmt.Sprintf("add-%s", subscription)))
subscriptionFutures = append(subscriptionFutures, future)
}
selector := restate.Select(ctx, subscriptionFutures...)
for selector.Remaining() {
_, err := selector.Select().(restate.RunAsyncFuture[string]).Result()
if err != nil {
return SubscriptionResult{}, err
}
}
return SubscriptionResult{
Success: true,
PaymentRef: payRef,
}, nil
}
```
```python app/concurrenttasks/service.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-orchestration-python/app/concurrenttasks/service.py?collapse_prequel"} theme={null}
parallel_subscription_service = restate.Service("ParallelSubscriptionService")
@parallel_subscription_service.handler()
async def add(ctx: restate.Context, req: SubscriptionRequest) -> SubscriptionResult:
payment_id = str(ctx.uuid())
pay_ref = await ctx.run_typed(
"pay",
create_recurring_payment,
credit_card=req.credit_card,
payment_id=payment_id,
)
# Start all subscriptions in parallel
subscription_tasks = []
for subscription in req.subscriptions:
task = ctx.run_typed(
f"add-{subscription}",
create_subscription,
user_id=req.user_id,
subscription=subscription,
payment_ref=pay_ref,
)
subscription_tasks.append(task)
# Wait for all subscriptions to complete
await restate.gather(*subscription_tasks)
return SubscriptionResult(success=True, payment_ref=pay_ref)
```
Restate retries all parallel tasks until they all complete and can deterministically replay the order of completion.
Add a few subscriptions for some users.
```bash theme={null}
curl localhost:8080/ParallelSubscriptionService/add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime", "YouTube"]}'
```
```bash theme={null}
curl localhost:8080/ParallelSubscriptionService/add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime", "YouTube"]}'
```
```bash theme={null}
curl localhost:8080/ParallelSubscriptionService/Add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime", "YouTube"]}'
```
```bash theme={null}
curl localhost:8080/ParallelSubscriptionService/add \
--json '{"userId": "user-123", "creditCard": "4111111111111111", "subscriptions": ["Hulu", "Prime", "YouTube"]}'
```
In the UI, you can see that all subscriptions are processed in parallel:
You can extend this to include the saga pattern and run all compensations in parallel as well.
Have a look at the Concurrent Tasks docs for your SDK to learn more ([TS](/develop/ts/concurrent-tasks) / [Java / Kotlin](/develop/java/concurrent-tasks) / [Python](/develop/python/concurrent-tasks) / [Go](/develop/go/concurrent-tasks)).
## Summary
Restate simplifies microservice orchestration with:
* **Durable Execution**: Automatic failure recovery without complex retry logic
* **Sagas**: Distributed transactions with resilient compensation
* **Service Communication**: Reliable RPC and messaging between services
* **Stateful Processing**: Consistent state management without external stores
* **Advanced Patterns**: Fault-tolerant timers, awakeables, and parallel execution
Build resilient distributed systems without the typical complexity.
# Tour of Restate for Agents with OpenAI SDK
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/tour/openai-agents
Build stateful, observable AI agents that recover from failures.
export const GitHubLink = ({url}) =>
;
AI agents are long-running processes that combine LLMs with tools and external APIs to complete complex tasks. With Restate, you can build agents that are **resilient to failures**, **stateful across conversations**, and **observable** without managing complex retry logic or external state stores.
In this guide, you'll learn how to:
* Build durable AI agents that recover automatically from crashes and API failures
* Integrate Restate with the OpenAI Agent SDK for Python
* Observe and debug agent executions with detailed traces
* Implement resilient human-in-the-loop workflows with approvals and timeouts
* Manage conversation history and state across multi-turn interactions
* Orchestrate multiple agents working together on complex tasks
## Getting Started
A Restate AI application has two main components:
* **Restate Server**: The core engine that takes care of the orchestration and resiliency of your agents
* **Agent Services**: Your agent or AI workflow logic using the Restate SDK for durability
Restate works with how you already deploy your agents, whether that's in Docker, on Kubernetes, or via serverless platforms (Modal, AWS Lambda...). You don't need to run your agents in any special way.
Let's run an example locally to get a better feel for how it works.
### Run the agent
[Install Restate](/installation) and launch it:
```bash theme={null}
restate-server
```
Get the example:
```bash theme={null}
git clone git@github.com:restatedev/ai-examples.git
cd ai-examples/openai-agents/tour-of-agents
```
Export your [OpenAI API key](https://platform.openai.com/api-keys) and run the agent:
```bash theme={null}
export OPENAI_API_KEY=sk-...
uv run .
```
Then, tell Restate where your agent is running via the UI (`http://localhost:9070`) or CLI:
```bash theme={null}
restate deployments register http://localhost:9080
```
This registers a set of agents that we will be covering in this tutorial.
To test your setup, invoke the weather agent, either via the UI playground by clicking on the `run` handler of the `WeatherAgent` in the overview:
Or via `curl`:
```bash theme={null}
curl localhost:8080/WeatherAgent/run \
--json '{"message": "What is the weather like in San Francisco?"}'
```
You should see the weather information printed in the terminal.
Let's have a look at what happened under the hood to make your agents resilient.
## Durable Execution
AI agents make multiple LLM calls and tool executions that can fail due to rate limits, network issues, or service outages.
Restate uses Durable Execution to make your agents withstand failures without losing progress.
The Restate SDK records the steps the agent executes in a log and replays them if the process crashes or is restarted:
Durable Execution is the basis of how Restate makes your agents resilient to failures.
Restate offers durable execution primitives via its SDK.
## Creating a Durable Agent
To implement a durable agent, you can use the Restate SDK in combination with the OpenAI Agent SDK.
Here's the implementation of the durable weather agent you just invoked:
```python durable_agent.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/openai-agents/tour-of-agents/app/durable_agent.py?collapse_imports"} theme={null}
@function_tool(failure_error_function=raise_restate_errors)
async def get_weather(
wrapper: RunContextWrapper[restate.Context], req: WeatherRequest
) -> WeatherResponse:
"""Get the current weather for a given city."""
# Do durable steps using the Restate context
restate_context = wrapper.context
return await restate_context.run_typed("Get weather", fetch_weather, city=req.city)
weather_agent = Agent[restate.Context](
name="WeatherAgent",
instructions="You are a helpful agent that provides weather updates.",
tools=[get_weather],
)
agent_service = restate.Service("WeatherAgent")
@agent_service.handler()
async def run(restate_context: restate.Context, prompt: WeatherPrompt) -> str:
result = await Runner.run(
weather_agent,
input=prompt.message,
# Pass the Restate context to tools to make tool execution steps durable
context=restate_context,
# Choose any model and let Restate persist your calls
run_config=RunConfig(
model="gpt-4o",
model_provider=DurableModelCalls(restate_context),
model_settings=ModelSettings(parallel_tool_calls=False),
),
)
return result.final_output
```
First, you implement your agent and its tools, similar to how you would do it with the OpenAI Agent SDK.
To serve the agent over HTTP with Restate, you create a Restate Service and define handlers.
Here, the agent logic is called from the `run` handler.
The endpoint that serves the agents of this tour over HTTP is defined in [`__main__.py`](https://github.com/restatedev/ai-examples/blob/main/openai-agents/tour-of-agents/tour-of-agents/__main__.py).
The agent can now be called at `http://localhost:8080/WeatherAgent/run`.
The main difference compared to a standard OpenAI agent is the use of the Restate Context at key points throughout the agent logic.
Any action with the Context is automatically recorded by the Restate Server and survives failures.
We use this for:
1. **Persisting LLM responses**: We use the `DurableModelCalls(restate_context)` model provider in `Runner.run`, so that every LLM response is saved in Restate Server and can be replayed during recovery.
2. **Resilient tool execution**: Tools can make steps durable by using Context actions. Their outcome will then be persisted for recovery and retried until they succeed. `restate_context.run_typed` runs an action durably, retrying it until it succeeds and persisting the result in Restate (e.g. database interaction, API calls, non-deterministic actions).
The Restate Context gets supplied to the `run` handler by the Restate SDK when the handler is invoked.
This context is then propagated to tools via the agent context.
The Restate Context can also be contained in an object together with additional context.
Learn more from [the OpenAI docs](https://openai.github.io/openai-agents-python/context/).
Ask for the weather in Denver:
```bash theme={null}
curl localhost:8080/WeatherAgent/run \
--json '{"message": "What is the weather like in Denver?"}'
```
On the invocation page in the UI, click on the invocation ID of the failing invocation.
You can see that your request is retrying because the weather API is down:
To fix the problem, remove the line `fail_on_denver` from the `fetch_weather` function in the `app/utils/utils.py` file:
```python utils/utils.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/openai-agents/tour-of-agents/app/utils/utils.py#weather"} theme={null}
async def fetch_weather(city: str) -> WeatherResponse:
fail_on_denver(city)
weather_data = await call_weather_api(city)
return parse_weather_data(weather_data)
```
Once you restart the service, the workflow finishes successfully.
## Observing your Agent
As you saw in the previous section, the Restate UI comes in handy when monitoring and debugging your agents.
The Invocations tab shows all agent executions with detailed traces of every LLM call, tool execution, and state change:
Restate supports OpenTelemetry for exporting traces to external systems like Langfuse, DataDog, or Jaeger:
Have a look at the [tracing docs](/server/monitoring/tracing) to set this up.
Now that you know how to build and debug an agent, let's look at more advanced patterns.
## Human-in-the-Loop Agent
Many AI agents need human oversight for high-risk decisions or gathering additional input. Restate makes it easy to pause agent execution and wait for human input.
**Benefits with Restate:**
* If the agent crashes while waiting for human input, Restate continues waiting and recovers the promise on another process.
* If the agent runs on function-as-a-service platforms, the Restate SDK lets the function suspend while its waiting. Once the approval comes in, the Restate Server invokes the function again and lets it resume where it left off. This way, you don't pay for idle waiting time ([Learn more](https://www.restate.dev/blog/resilient-serverless-agents#cost-efficiency-scaling-to-zero-while-waiting)).
Here's an insurance claim agent that asks for human approval for high-value claims:
```python human_approval_agent.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/openai-agents/tour-of-agents/app/human_approval_agent.py#here"} theme={null}
@function_tool(failure_error_function=raise_restate_errors)
async def human_approval(
wrapper: RunContextWrapper[restate.Context], claim: InsuranceClaim
) -> str:
"""Ask for human approval for high-value claims."""
restate_context = wrapper.context
# Create an awakeable for human approval
approval_id, approval_promise = restate_context.awakeable(type_hint=str)
# Request human review
await restate_context.run_typed(
"Request review", request_human_review, claim=claim, awakeable_id=approval_id
)
# Wait for human approval
return await approval_promise
```
To implement human approval steps, you can use Restate's awakeables. An awakeable is a promise that can be resolved externally via an API call by providing its ID.
When you create the awakeable, you get back an ID and a promise. You can send the ID to the human approver, and then wait for the promise to be resolved.
You can also use awakeables outside of tools, for example, to implement human approval steps in between agent iterations.
Start a request for a high-value claim that needs human approval.
Use the playground or `curl` with `/send` to start the claim asynchronously, without waiting for the result.
```bash theme={null}
curl localhost:8080/HumanClaimApprovalAgent/run/send \
--json '{"message": "Process my hospital bill of 3000USD for a broken leg."}'
```
You can restart the service to see how Restate continues waiting for the approval.
If you wait for more than a minute, the invocation will get suspended.
Simulate approving the claim by executing the **curl request that was printed in the service logs**, similar to:
```bash theme={null}
curl localhost:8080/restate/awakeables/sign_1M28aqY6ZfuwBmRnmyP/resolve --json 'true'
```
See in the UI how the workflow resumes and finishes after the approval.
Add timeouts to human approval steps to prevent workflows from hanging indefinitely.
Restate persists the timer and the approval promise, so if the service crashes or is restarted, it will continue waiting with the correct remaining time:
```python human_approval_agent_with_timeout.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/openai-agents/tour-of-agents/app/human_approval_agent_with_timeout.py#here"} theme={null}
# Wait for human approval for at most 3 hours to reach our SLA
match await restate.select(
approval=approval_promise,
timeout=restate_context.sleep(timedelta(hours=3)),
):
case ["approval", approved]:
return "Approved" if approved else "Rejected"
case _:
return "Approval timed out - Evaluate with AI"
```
Try it out by sending a request to the service:
```bash theme={null}
curl localhost:8080/HumanClaimApprovalWithTimeoutsAgent/run/send \
--json '{"message": "Process my hospital bill of 3000USD for a broken leg."}'
```
You restart the service and check in the UI how the process will block for the remaining time without starting over.
You can also lower the timeout to a few seconds to see how the timeout path is taken.
## Resilient workflows as tools
You can pull out complex parts of your tool logic into separate workflows.
This lets you break down complex agents into smaller, reusable components that can be developed, deployed, and scaled independently.
The Restate SDK gives you clients to call other Restate services durably from your agent logic.
All calls are proxied via Restate. Restate persists the call and takes care of retries and recovery.
For example, let's implement the human approval tool as a separate service:
```python sub_workflow_agent.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/openai-agents/tour-of-agents/app/sub_workflow_agent.py#wf"} theme={null}
# Sub-workflow service for human approval
human_approval_workflow = restate.Service("HumanApprovalWorkflow")
@human_approval_workflow.handler("requestApproval")
async def request_approval(
restate_context: restate.Context, claim: InsuranceClaim
) -> str:
"""Request human approval for a claim and wait for response."""
# Create an awakeable that can be resolved via HTTP
approval_id, approval_promise = restate_context.awakeable(type_hint=str)
# Request human review
await restate_context.run_typed(
"Request review", request_human_review, claim=claim, awakeable_id=approval_id
)
# Wait for human approval
return await approval_promise
```
This can now be called from the main agent via a service client:
```python sub_workflow_agent.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/openai-agents/tour-of-agents/app/sub_workflow_agent.py#here"} theme={null}
@function_tool(failure_error_function=raise_restate_errors)
async def human_approval(
wrapper: RunContextWrapper[restate.Context], claim: InsuranceClaim
) -> str:
"""Ask for human approval for high-value claims using sub-workflow."""
restate_context = wrapper.context
# Call the human approval sub-workflow
return await restate_context.service_call(request_approval, claim)
```
These workflows have access to all Restate SDK features, including durable execution, state management, awakeables, and observability.
They can be developed, deployed, and scaled independently.
Start a request for a high-value claim that needs human approval.
Use `/send` to start the claim asynchronously, without waiting for the result.
```bash theme={null}
curl localhost:8080/SubWorkflowClaimApprovalAgent/run/send \
--json '{"message": "Process my hospital bill of 3000USD for a broken leg."}'
```
In the UI, you can see that the agent called the workflow service and is waiting for the response.
You can see the trace of the sub-workflow in the timeline.
Once you approve the claim, the workflow returns, and the agent continues.
Follow the [Tour of Workflows](/tour/workflows) to learn more about implementing resilient workflows with Restate.
## Durable Sessions
The next ingredient we need to build AI agents is the ability to maintain context and memory across multiple interactions.
The OpenAI SDK allows plugging in custom [session providers](https://openai.github.io/openai-agents-python/sessions/) to manage conversation history.
This integrates very well with Restate's stateful entities, called Virtual Objects.
Virtual Objects are Restate's way of implementing stateful services with durable state management and built-in concurrency control.
To implement stateful entities like chat sessions, or stateful agents, Restate provides Virtual Objects.
Each Virtual Object instance maintains isolated state and is identified by a unique key.
### Virtual Objects as OpenAI Session Providers
The Restate OpenAI middleware includes a SessionProvider that automatically persists the agent's conversation history in the Virtual Object state.
Here is an example of a stateful, durable agent represented as a Virtual Object:
```python chat.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/openai-agents/tour-of-agents/app/chat.py?collapse_imports"} theme={null}
chat = VirtualObject("Chat")
@chat.handler()
async def message(restate_context: ObjectContext, chat_message: ChatMessage) -> dict:
restate_session = await RestateSession.create(
session_id=restate_context.key(), ctx=restate_context
)
result = await Runner.run(
Agent(name="Assistant", instructions="You are a helpful assistant."),
input=chat_message.message,
run_config=RunConfig(
model="gpt-4o",
model_provider=DurableModelCalls(restate_context),
model_settings=ModelSettings(parallel_tool_calls=False),
),
session=restate_session,
)
return result.final_output
@chat.handler(kind="shared")
async def get_history(ctx: ObjectSharedContext):
return await ctx.get("items") or []
```
Virtual Objects are ideal for implementing any entity with mutable state:
* **Long-lived state**: K/V state is stored permanently. It has no automatic expiry. Clear it via `ctx.clear()`.
* **Durable state changes**: State changes are logged with Durable Execution, so they survive failures and are consistent with code execution
* **State is queryable** via the state tab in the UI.
* **Built-in concurrency control**: Restate’s Virtual Objects have built-in queuing and consistency guarantees per object key. Handlers either have read-write access (`ObjectContext`) or read-only access (shared object context).
* Only one handler with write access can run at a time per object key to prevent concurrent/lost writes or race conditions (for example `message()`).
* Handlers with read-only access can run concurrently to the write-access handlers (for example `get_history()`).
**Stateful Chat Agent:**
Ask the agent to do some task:
```bash theme={null}
curl localhost:8080/Chat/session123/message \
--json '{"message": "Make a poem about durable execution."}'
```
Continue the conversation - the agent remembers previous context:
```bash theme={null}
curl localhost:8080/Chat/session123/message \
--json '{"message": "Shorten it to 2 lines."}'
```
Get conversation history or view it in the UI:
```bash theme={null}
curl localhost:8080/Chat/session123/get_history
```
**Seeing concurrency control in action:**
In the chat service, the `message` handler is an exclusive handler, while the `getHistory` handler is a shared handler.
Let's send some messages to a chat session:
```bash theme={null}
curl localhost:8080/Chat/session123/message/send --json '{"message": "make a poem about durable execution"}' &
curl localhost:8080/Chat/session456/message/send --json '{"message": "what are the benefits of durable execution?"}' &
curl localhost:8080/Chat/session789/message/send --json '{"message": "how does workflow orchestration work?"}' &
curl localhost:8080/Chat/session123/message/send --json '{"message": "can you make it rhyme better?"}' &
curl localhost:8080/Chat/session456/message/send --json '{"message": "what about fault tolerance in distributed systems?"}' &
curl localhost:8080/Chat/session789/message/send --json '{"message": "give me a practical example"}' &
curl localhost:8080/Chat/session101/message/send --json '{"message": "explain event sourcing in simple terms"}' &
curl localhost:8080/Chat/session202/message/send --json '{"message": "what is the difference between async and sync processing?"}'
```
The UI shows how Restate queues the requests per session to ensure consistency:
You can run Virtual Objects on serverless platforms like Modal, Render, or AWS Lambda.
When the request comes in, Restate attaches the correct state to the request, so your handler can access it locally.
This way, you can implement [stateful, serverless agents without managing any external state store](https://www.restate.dev/blog/resilient-serverless-agents#cost-efficiency-scaling-to-zero-while-waiting) and without worrying about concurrency issues.
### Virtual Objects for storing context
You can store any context information in Virtual Objects, for example, user preferences or the last agent they interacted with.
Use `ctx.set` and `ctx.get` in your handler to store and retrieve state.
We will show an example of this in the next section when we orchestrate multiple agents.
## Resilient multi-agent coordination
As your agents grow more complex, you may want to break them down into smaller, specialized agents that can delegate tasks to each other.
Similar to sub-workflows, you can break down complex agents into multiple specialized agents.
All agents can run in the same process or be deployed independently.
### Agents as tools/handoffs
If you want to share context between agents, run the agents in the same process and use handoffs or tools.
You don't need to do anything special to make this work with Restate.
Use Virtual Object state to maintain context between runs. For example, store the last agent that was called in the object state, so the user can connect back seamlessly on the next interaction:
```python multi_agent.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/openai-agents/tour-of-agents/app/multi_agent.py?collapse_imports"} theme={null}
intake_agent = Agent[restate.ObjectContext](
name="IntakeAgent",
instructions="Route insurance claims to the appropriate specialist: medical, auto, or property.",
)
medical_specialist = Agent[restate.ObjectContext](
name="MedicalSpecialist",
handoff_description="I handle medical insurance claims from intake to final decision.",
instructions="Review medical claims for coverage and necessity. Approve/deny up to $50,000.",
)
auto_specialist = Agent[restate.ObjectContext](
name="AutoSpecialist",
handoff_description="I handle auto insurance claims from intake to final decision.",
instructions="Assess auto claims for liability and damage. Approve/deny up to $25,000.",
)
# Configure handoffs so intake agent can route to specialists
intake_agent.handoffs = [medical_specialist, auto_specialist]
agent_dict = {
"IntakeAgent": intake_agent,
"MedicalSpecialist": medical_specialist,
"AutoSpecialist": auto_specialist,
}
agent_service = restate.VirtualObject("MultiAgentClaimApproval")
@agent_service.handler()
async def run(restate_context: restate.ObjectContext, claim: InsuranceClaim) -> str:
# Store context in Restate's key-value store
last_agent_name = (
await restate_context.get("last_agent_name", type_hint=str) or "IntakeAgent"
)
last_agent = agent_dict.get(last_agent_name, intake_agent)
restate_session = await RestateSession.create(
session_id=restate_context.key(), ctx=restate_context
)
result = await Runner.run(
last_agent,
input=f"Claim: {claim.model_dump_json()}",
context=restate_context,
run_config=RunConfig(
model="gpt-4o",
model_provider=DurableModelCalls(restate_context),
model_settings=ModelSettings(parallel_tool_calls=False),
),
session=restate_session,
)
restate_context.set("last_agent_name", result.last_agent.name)
return result.final_output
```
The execution trace in the Restate UI will allow you to see the full chain of calls between agents and their individual steps.
Start a request for a claim that needs to be analyzed by multiple agents.
```bash theme={null}
curl localhost:8080/MultiAgentClaimApproval/session123/run --json '{
"date":"2024-10-01",
"category":"orthopedic",
"reason":"hospital bill for a broken leg",
"amount":3000,
"placeOfService":"General Hospital"
}'
```
In the UI, you can see that the agent called the sub-agents and is waiting for their responses.
You can see the trace of the sub-agents in the timeline.
Once all sub-agents return, the main agent continues and makes a decision.
The state now contains the last agent that was called, so you can continue the conversation directly with the same agent:
### Remote agents as tools
If you want to run agents independently, for example, to scale them separately, run them on different platforms, or let them get developed by different teams, then you can call them as tools via service calls.
Restate will proxy all calls, persist them, and will guarantee that they complete successfully.
Your main agent can suspend and save resources while waiting for the remote agent to finish.
Restate invokes your main agent again once the remote agent returns.
```python multi_agent.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/openai-agents/tour-of-agents/app/multi_agent_remote.py#here"} theme={null}
# Durable service call to the fraud agent; persisted and retried by Restate
@function_tool(failure_error_function=raise_restate_errors)
async def check_fraud(
wrapper: RunContextWrapper[restate.Context], claim: InsuranceClaim
) -> str:
"""Analyze the probability of fraud."""
restate_context = wrapper.context
return await restate_context.service_call(run_fraud_agent, claim)
claim_approval_coordinator = Agent[restate.Context](
name="ClaimApprovalCoordinator",
instructions="You are a claim approval engine. Analyze the claim and use your tools to decide whether to approve it.",
tools=[check_fraud, check_eligibility],
)
agent_service = restate.Service("RemoteMultiAgentClaimApproval")
@agent_service.handler()
async def run(restate_context: restate.Context, claim: InsuranceClaim) -> str:
result = await Runner.run(
claim_approval_coordinator,
input=f"Claim: {claim.model_dump_json()}",
context=restate_context,
run_config=RunConfig(
model="gpt-4o",
model_provider=DurableModelCalls(restate_context),
model_settings=ModelSettings(parallel_tool_calls=False),
),
)
return result.final_output
```
Note, any shared context between agents needs to be passed explicitly via the input.
The execution trace in the Restate UI will allow you to see the full chain of calls between agents and their individual steps.
You cannot put both agents within the same Virtual Object, because this leads to deadlocks.
The main agent would block on the call to the sub-agent, preventing the sub-agent from executing, cause only one handler can run at a time per object key.
Start a request for a claim that needs to be analyzed by multiple agents.
```bash theme={null}
curl localhost:8080/RemoteMultiAgentClaimApproval/run --json '{
"date":"2024-10-01",
"category":"orthopedic",
"reason":"hospital bill for a broken leg",
"amount":3000,
"placeOfService":"General Hospital"
}'
```
In the UI, you can see that the agent called the sub-agents and is waiting for their responses.
You can see the trace of the sub-agents in the timeline.
Once all sub-agents return, the main agent continues and makes a decision.
## Parallel Work
Now that our agents are broken down into smaller parts, let's have a look at how to run different parts of our agent logic in parallel to speed up execution.
When using the OpenAI Agent SDK with Restate, tool calls are executed sequentially by default to ensure deterministic execution during replays.
When multiple tools execute in parallel and use the Restate Context, the order of operations might differ between the original execution and the replay, leading to inconsistencies.
Restate provides primitives that allow you to run tasks concurrently while maintaining deterministic execution during replays.
Most actions on the Restate Context can be composed using `restate.gather` to gather their results or `restate.select` to wait for the first one to complete.
### Parallel Tool Steps
To parallelize tool steps, implement an orchestrator tool that uses durable execution to run multiple steps in parallel.
Here is an insurance claim agent tool that runs multiple analyses in parallel:
```python parallel_tools_agent.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/openai-agents/tour-of-agents/app/parallel_tools_agent.py#here"} theme={null}
@function_tool(failure_error_function=raise_restate_errors)
async def calculate_metrics(
wrapper: RunContextWrapper[restate.Context], claim: InsuranceClaim
) -> list[str]:
"""Calculate claim metrics."""
restate_context = wrapper.context
# Run tools/steps in parallel with durable execution
results_done = await restate.gather(
restate_context.run_typed("eligibility", check_eligibility, claim=claim),
restate_context.run_typed("cost", compare_to_standard_rates, claim=claim),
restate_context.run_typed("fraud", check_fraud, claim=claim),
)
return [await result for result in results_done]
```
Restate makes sure that all parallel tasks are retried and recovered until they succeed.
If you want to allow the LLM to call multiple tools in parallel, then you need to [manually implement the agent tool execution loop](#advanced-patterns) using `restate.select` and durable promises.
Start a request for a claim that needs to be analyzed by multiple tools in parallel.
```bash theme={null}
curl localhost:8080/ParallelToolClaimAgent/run --json '{
"date":"2024-10-01",
"category":"orthopedic",
"reason":"hospital bill for a broken leg",
"amount":3000,
"placeOfService":"General Hospital"
}'
```
In the UI, you can see that the agent ran the tool steps in parallel.
Their traces all start at the same time.
Once all tools return, the agent continues and makes a decision.
### Parallel Agents
You can use the same durable execution primitives to run multiple agents in parallel.
For example, to race agents against each other and [use the first result that returns](/develop/ts/concurrent-tasks#wait-for-the-first-successful-completion), while [cancelling the others](/foundations/invocations#cancelling-invocations).
Or to let a main orchestrator agent combine the results of multiple specialized agents in parallel:
```python parallel_agents.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/openai-agents/tour-of-agents/app/parallel_agents.py#here"} theme={null}
@agent_service.handler()
async def run(restate_context: restate.Context, claim: InsuranceClaim) -> str:
# Start multiple agents in parallel with auto retries and recovery
eligibility = restate_context.service_call(run_eligibility_agent, claim)
cost = restate_context.service_call(run_rate_comparison_agent, claim)
fraud = restate_context.service_call(run_fraud_agent, claim)
# Wait for all responses
await restate.gather(eligibility, cost, fraud)
# Run decision agent on outputs
result = await Runner.run(
Agent(
name="ClaimApprovalAgent", instructions="You are a claim decision engine."
),
input=f"Decide about claim: {claim.model_dump_json()}. "
"Base your decision on the following analyses:"
f"Eligibility: {await eligibility} Cost {await cost} Fraud: {await fraud}",
run_config=RunConfig(
model="gpt-4o",
model_provider=DurableModelCalls(restate_context),
model_settings=ModelSettings(parallel_tool_calls=False),
),
)
return result.final_output
```
Start a request for a claim that needs to be analyzed by multiple agents in parallel.
```bash theme={null}
curl localhost:8080/ParallelAgentClaimApproval/run --json '{
"date":"2024-10-01",
"category":"orthopedic",
"reason":"hospital bill for a broken leg",
"amount":3000,
"placeOfService":"General Hospital"
}'
```
In the UI, you can see that the handler called the sub-agents in parallel.
Once all sub-agents return, the main agent makes a decision.
## Error Handling
LLM calls are costly, so you can configure retry behavior in both Restate and your AI SDK to avoid infinite loops and high costs.
Restate distinguishes between two types of errors:
* **Transient errors**: Temporary issues like network failures or rate limits. Restate automatically retries these until they succeed or the retry policy is exhausted.
* **Terminal errors**: Permanent failures like invalid input or business rule violations. Restate does not retry these. The invocation fails permanently. You can catch these errors and handle them gracefully.
You can throw a terminal error via:
```python theme={null}
from restate import TerminalError
raise TerminalError("This tool is not allowed to run for this input.")
```
You can catch and handle terminal errors in your agent logic if needed.
Many AI SDKs also have their own retry behavior for LLM calls and tool executions, so let's look at how these interact.
### Retries of LLM calls
Restate's `DurableModelCalls` provider lets you specify the maximum number of retries for LLM calls.
```python theme={null}
run_config=RunConfig(
model="gpt-4o",
model_provider=DurableModelCalls(restate_context, max_retries=3),
model_settings=ModelSettings(parallel_tool_calls=False)
),
```
By default, the middleware retries three times. Once Restate's retries are exhausted, the invocation fails with a `TerminalError` and won't be retried further.
### Tool execution errors
By default, the OpenAI Agent SDK will convert any error in tool execution into a message to the LLM, and the LLM will decide how to proceed.
This is often desirable, as the LLM can decide to retry the tool call, use a different tool, or provide a fallback answer.
#### Surfacing suspensions and terminal errors
There are some Restate errors that should not be handled by the LLM, for example, if a tool execution is suspended waiting for human input.
Restate lets tool executions suspend if they need to wait for a long time. This suspension is started by raising an artificial error.
These errors should be re-raised by the agent, instead of ingested.
Therefore, you should always set your tool's `failure_error_function` to raise Restate errors like suspensions.
```python error_handling.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/openai-agents/tour-of-agents/app/error_handling.py#here"} theme={null}
@function_tool(failure_error_function=raise_restate_errors)
async def get_weather(
wrapper: RunContextWrapper[restate.Context], req: WeatherRequest
) -> WeatherResponse:
"""Get the current weather for a given city."""
restate_context = wrapper.context
return await restate_context.run_typed("Get weather", fetch_weather, city=req.city)
```
This error function will also re-raise any terminal errors that happen during tool execution.
You can then handle the terminal error in your agent logic if needed, or let it fail the invocation:
```python error_handling.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/openai-agents/tour-of-agents/app/error_handling.py#handle"} theme={null}
try:
result = await Runner.run(
weather_agent,
input=prompt.message,
context=restate_context,
run_config=RunConfig(
model="gpt-4o",
model_provider=DurableModelCalls(restate_context, max_retries=2),
model_settings=ModelSettings(parallel_tool_calls=False),
),
)
except restate.TerminalError as e:
# Handle terminal errors gracefully
return "The agent couldn't complete the request."
```
The OpenAI Agent SDK also allows setting `failure_error_function` to `None`, which will rethrow any error in the agent execution as-is.
Also for example invalid LLM responses (e.g. tool call with invalid arguments or to a tool that doesn't exist).
The error will then lead to Restate retries. Restate will recover the invocation by replaying the journal entries.
This can lead to infinite retries if the error is not transient.
Therefore, be careful when using this option and handle errors appropriately in your agent logic.
You also might want to set [a retry policy at the service or handler level](/services/configuration#how-to-configure) to avoid infinite retries.
#### Retry-ing transient errors
If you use Restate Context actions like `ctx.run` in your tool execution, Restate will retry any transient errors in these actions until they succeed.
So for all operations that might suffer from transient errors (like network calls, database interactions, etc.), you should use Context actions to make them resilient.
Here is a small practical example:
```typescript {"CODE_LOAD::ts/src/tour/agents/inline-tool-errors.ts#here"} theme={null}
// Without ctx.run - error goes straight to agent
async function myTool() {
const result = await fetch('/api/data'); // Might fail due to network
// If this fails, agent gets the error immediately
}
// With ctx.run - Restate handles retries
async function myToolWithRestate(ctx: restate.Context) {
const result = await ctx.run('fetch-data', () =>
fetch('/api/data')
);
// Network failures get retried automatically
// Only terminal errors reach the AI
}
```
You can set [custom retry policies](/guides/error-handling#at-the-run-block-level) for `ctx.run` steps in your tool executions.
## Advanced patterns
If you need more control over the agent loop, you can implement it manually using Restate's durable primitives.
This allows you to:
* Parallelize tool calls with [`restate.select`](/develop/python/concurrent-tasks#select) and [`restate.gather`](/develop/python/concurrent-tasks#waiting-for-all-tasks-to-complete)
* Implement custom stopping conditions
* Implement custom logic between steps (e.g. human approval)
* Interact with external systems between steps
* Handle errors in a custom way
Here is an example of a manual agent loop:
```python advanced/manual_loop_agent.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/openai-agents/tour-of-agents/app/advanced/manual_loop_agent.py?collapse_imports"} theme={null}
client = OpenAI()
# Tool definitions
TOOLS = [
{
"type": "function",
"function": {
"name": "get_weather",
"parameters": WeatherRequest.model_json_schema(),
"description": "Get the current weather in a given location"
}
}
]
manual_loop_agent = restate.Service("ManualLoopAgent")
class MultiWeatherPrompt(BaseModel):
message: str = "What is the weather like in New York and San Francisco?"
@manual_loop_agent.handler()
async def run(ctx: Context, prompt: MultiWeatherPrompt) -> str:
"""Main agent loop with tool calling"""
messages = [{"role": "user", "content": prompt.message}]
while True:
# Call OpenAI with durable execution
response = await ctx.run_typed(
"llm-call",
client.chat.completions.create,
restate.RunOptions(max_attempts=3,
type_hint=ChatCompletion), # To avoid using too many credits on infinite retries during development
model="gpt-4o",
tools=TOOLS,
messages=messages,
)
# Save function call outputs for subsequent requests
assistant_message = response.choices[0].message
messages.append(assistant_message)
if not assistant_message.tool_calls:
return assistant_message.content
# Check if we need to call tools
for tool_call in assistant_message.tool_calls:
if tool_call.function.name == "get_weather":
req = WeatherRequest.model_validate_json(tool_call.function.arguments)
tool_output = await ctx.run_typed("Get weather", fetch_weather, city=req.city)
# Add tool response to messages
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": tool_output.model_dump_json()
})
```
This can be extended to include any custom control flow you need: persistent state, parallel tool calls, custom stopping conditions, or custom error handling.
Try it out by sending a request to the service:
```bash theme={null}
curl localhost:8080/ManualLoopAgent/run \
--json '{"message": "What is the weather like in New York and San Francisco?"}'
```
In the UI, you can see how the agent runs multiple iterations and calls tools.
Sometimes you need to undo previous agent actions when a later step fails. Restate makes it easy to implement compensation patterns (Sagas) for AI agents.
Just track the rollback actions as you go, let the agent rethrow terminal tool errors, and execute the rollback actions in reverse order.
Here is an example of a travel booking agent that first reserves a hotel, flight and car, and then either confirms them or rolls back if any step fails with a terminal error (e.g. car type not available).
We let tools add rollback actions to the agent context for each booking step the do.
The `run` handler catches any terminal errors and runs all the rollback actions.
```python advanced/rollback_agent.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/openai-agents/tour-of-agents/app/advanced/rollback_agent.py?collapse_imports"} theme={null}
class BookingContext(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
booking_id: str
restate_context: restate.Context
on_rollback: list[Callable] = Field(default=[])
# Functions raise terminal errors instead of feeding them back to the agent
@function_tool(failure_error_function=raise_restate_errors)
async def book_hotel(
wrapper: RunContextWrapper[BookingContext], booking: HotelBooking
) -> BookingResult:
"""Book a hotel"""
booking_context = wrapper.context
# Register a rollback action for each step, in case of failures further on in the workflow
booking_context.on_rollback.append(
lambda: booking_context.restate_context.run_typed(
"Cancel hotel", cancel_hotel, booking_id=booking_context.booking_id
)
)
# Execute the workflow step
return await booking_context.restate_context.run_typed(
"Book hotel", reserve_hotel, booking_id=booking_context.booking_id, booking=booking
)
@function_tool(failure_error_function=raise_restate_errors)
async def book_flight(
wrapper: RunContextWrapper[BookingContext], booking: FlightBooking
) -> BookingResult:
"""Book a flight"""
booking_context = wrapper.context
booking_context.on_rollback.append(
lambda: booking_context.restate_context.run_typed(
"Cancel flight", cancel_flight, booking_id=booking_context.booking_id
)
)
return await booking_context.restate_context.run_typed(
"Book flight", reserve_flight, booking_id=booking_context.booking_id, booking=booking
)
# ... Do the same for cars ...
agent_service = restate.Service("BookingWithRollbackAgent")
@agent_service.handler()
async def book(restate_context: restate.Context, prompt: BookingPrompt) -> str:
booking_context = BookingContext(
booking_id=prompt.booking_id, restate_context=restate_context
)
booking_agent = Agent[BookingContext](
name="BookingWithRollbackAgent",
instructions="Book a complete travel package with the requirements in the prompt."
"Use tools to first book the hotel, then the flight.",
tools=[book_hotel, book_flight],
)
try:
result = await Runner.run(
booking_agent,
input=prompt.message,
context=booking_context,
run_config=RunConfig(
model="gpt-4o", model_provider=DurableModelCalls(restate_context)
),
)
except TerminalError as e:
# Run all the rollback actions on terminal errors
for compensation in reversed(booking_context.on_rollback):
await compensation()
raise e
return result.final_output
```
Try it out by sending the following request:
```bash theme={null}
curl localhost:8080/BookingWithRollbackAgent/book \
--json '{
"booking_id": "booking_123",
"message": "I need to book a business trip to San Francisco from March 15-17. Flying from JFK, need a hotel downtown for 1 guest."
}'
```
Have a look at the UI to see how the flight booking fails, and the bookings are rolled back.
Check out the [sagas guide](/guides/sagas) for more details.
Restate supports implementing scheduling and timer logic in your agents.
This allows you to build agents that run periodically, wait for specific times, or implement complex scheduling logic.
Agents can either be long-running or reschedule themselves for later execution.
Have a look at the [scheduling docs](/develop/python/durable-timers) to learn more.
Have a look at the [pub-sub example](https://github.com/igalshilman/agent47/tree/main/packages/pubsub).
Have a look at the [interruptible coding agent](https://github.com/igalshilman/agent47/tree/main/packages/agent).
## Summary
Durable Execution, paired with your existing SDKs, gives your agents a powerful upgrade:
* **Durable Execution**: Automatic recovery from failures without losing progress
* **Persistent memory and context**: Persistent conversation history and context
* **Observability** by default across your agents and workflows
* **Human-in-the-Loop**: Seamless approval workflows with timeouts
* **Multi-Agent Coordination**: Reliable orchestration of specialized agents
* **Suspensions** to save costs on function-as-a-service platforms when agents need to wait
* **Advanced Patterns**: Real-time progress updates, interruptions, and long-running workflows
## Next Steps
* Learn more about how to implement resilient tools with Restate in the [Tour of Workflows](/tour/workflows)
* Check out the [other Restate AI examples on GitHub](https://github.com/restatedev/ai-examples)
* Sign up for [Restate Cloud](https://restate.dev/cloud/) and start building agents without managing infrastructure
# Tour of Restate for Agents with Vercel AI SDK
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/tour/vercel-ai-agents
Build stateful, observable AI agents that recover from failures.
export const GitHubLink = ({url}) =>
;
AI agents are long-running processes that combine LLMs with tools and external APIs to complete complex tasks. With Restate, you can build agents that are **resilient to failures**, **stateful across conversations**, and **observable** without managing complex retry logic or external state stores.
In this guide, you'll learn how to:
* Build durable AI agents that recover automatically from crashes and API failures
* Integrate Restate and the Vercel AI SDK
* Observe and debug agent executions with detailed traces
* Implement resilient human-in-the-loop workflows with approvals and timeouts
* Manage conversation history and state across multi-turn interactions
* Orchestrate multiple agents working together on complex tasks
## Getting Started
A Restate AI application has two main components:
* **Restate Server**: The core engine that takes care of the orchestration and resiliency of your agents
* **Agent Services**: Your agent or AI workflow logic using the Restate SDK for durability
Restate works with how you already deploy your agents, whether that's in Docker, on Kubernetes, or via serverless platforms (Vercel, Modal, Cloudflare Workers,...). You don't need to run your agents in any special way.
Let's run an example locally to get a better feel for how it works.
### Run the agent
[Install Restate](/installation) and launch it:
```bash theme={null}
npm install --global @restatedev/restate-server@latest @restatedev/restate@latest
restate-server
```
Get the example:
```bash theme={null}
git clone git@github.com:restatedev/ai-examples.git
cd ai-examples/vercel-ai/tour-of-agents
npm install
```
Export your [OpenAI API key](https://platform.openai.com/api-keys) and run the agent:
```bash theme={null}
export OPENAI_API_KEY=sk-...
npm run dev
```
Then, tell Restate where your agent is running via the UI (`http://localhost:9070`) or CLI:
```bash theme={null}
restate deployments register http://localhost:9080
```
This registers a set of agents that we will be covering in this tutorial.
To test your setup, invoke the weather agent, either via the UI playground (by clicking on the service) or curl:
```bash theme={null}
curl localhost:8080/WeatherAgent/run \
--json '{"prompt": "What is the weather like in San Francisco?"}'
```
You should see the weather information printed in the terminal.
Let's have a look at what happened under the hood to make your agents resilient.
## Durable Execution
AI agents make multiple LLM calls and tool executions that can fail due to rate limits, network issues, or service outages.
Restate uses Durable Execution to make your agents withstand failures without losing progress.
The Restate SDK records the steps the agent executes in a log and replays them if the process crashes or is restarted:
Durable Execution is the basis of how Restate makes your agents resilient to failures.
Restate offers durable execution primitives via its SDK.
## Creating a Durable Agent
To implement a durable agent, you can use the Restate SDK in combination with existing AI frameworks like the Vercel AI SDK.
Here's the implementation of the durable weather agent you just invoked:
```typescript durableexecution/agent.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/vercel-ai/tour-of-agents/src/durableexecution/agent.ts?collapse_prequel"} theme={null}
export default restate.service({
name: "WeatherAgent",
handlers: {
run: async (ctx: restate.Context, { prompt }: { prompt: string }) => {
const model = wrapLanguageModel({
model: openai("gpt-4o"),
middleware: durableCalls(ctx, { maxRetryAttempts: 3 }),
});
const { text } = await generateText({
model,
system: "You are a helpful agent that provides weather updates.",
prompt,
tools: {
getWeather: tool({
description: "Get the current weather for a given city.",
inputSchema: z.object({ city: z.string() }),
execute: async ({ city }) =>
ctx.run("get weather", () => fetchWeather(city)),
}),
},
stopWhen: [stepCountIs(5)],
providerOptions: { openai: { parallelToolCalls: false } },
});
return text;
},
},
});
```
The agent logic is implemented in a handler of a Restate service, here the `run` handler.
The endpoint that serves the agents of this tour over HTTP is defined in `src/app.ts`. The agent can now be called at `http://localhost:8080/WeatherAgent/run`.
The main difference compared to a standard Vercel AI agent is the use of the Restate Context at key points throughout the agent logic.
Any action with the Context is automatically recorded by the Restate Server and survives failures.
We use this for:
1. **Persisting LLM responses**: We wrap the model with the `durableCalls(ctx)` middleware, so that every LLM response is saved in Restate Server and can be replayed during recovery. The middleware is provided via the package [`@restatedev/vercel-ai-middleware`](https://github.com/restatedev/vercel-ai-middleware).
2. **Resilient tool execution**: Tools can make steps durable by using Context actions. Their outcome will then be persisted for recovery and retried until they succeed. `ctx.run` runs an action durably, retrying it until it succeeds and persisting the result in Restate (e.g. database interaction, API calls, non-deterministic actions).
Ask for the weather in Denver:
```bash theme={null}
curl localhost:8080/WeatherAgent/run \
--json '{"prompt": "What is the weather like in Denver?"}'
```
On the invocation page in the UI, click on the invocation ID of the failing invocation.
You can see that your request is retrying because the weather API is down:
To fix the problem, remove the line `failOnDenver` from the `fetchWeather` function in the `utils.ts` file:
```ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/vercel-ai/tour-of-agents/src/utils.ts#weather"} theme={null}
export async function fetchWeather(city: string) {
failOnDenver(city);
const output = await fetchWeatherFromAPI(city);
return parseWeatherResponse(output);
}
```
Once you restart the service, the workflow finishes successfully.
## Observing your Agent
As you saw in the previous section, the Restate UI comes in handy when monitoring and debugging your agents.
The Invocations tab shows all agent executions with detailed traces of every LLM call, tool execution, and state change:
Restate supports OpenTelemetry for exporting traces to external systems like Langfuse, DataDog, or Jaeger:
Have a look at the [tracing docs](/server/monitoring/tracing) to set this up.
Now that you know how to build and debug an agent, let's look at more advanced patterns.
## Human-in-the-Loop Agent
Many AI agents need human oversight for high-risk decisions or gathering additional input. Restate makes it easy to pause agent execution and wait for human input.
**Benefits with Restate:**
* If the agent crashes while waiting for human input, Restate continues waiting and recovers the promise on another process.
* If the agent runs on function-as-a-service platforms, the Restate SDK lets the function suspend while its waiting. Once the approval comes in, the Restate Server invokes the function again and lets it resume where it left off. This way, you don't pay for idle waiting time ([Learn more](https://www.restate.dev/blog/resilient-serverless-agents#cost-efficiency-scaling-to-zero-while-waiting)).
Here's an insurance claim agent that asks for human approval for high-value claims:
```typescript humanintheloop/agent.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/vercel-ai/tour-of-agents/src/humanintheloop/agent.ts#here"} theme={null}
const { text } = await generateText({
model,
system:
"You are an insurance claim evaluation agent. Use these rules: " +
"* if the amount is more than 1000, ask for human approval, " +
"* if the amount is less than 1000, decide by yourself",
prompt,
tools: {
humanApproval: tool({
description: "Ask for human approval for high-value claims.",
inputSchema: InsuranceClaimSchema,
execute: async (claim: InsuranceClaim): Promise => {
const approval = ctx.awakeable();
await ctx.run("request-review", () =>
requestHumanReview(claim, approval.id),
);
return approval.promise;
},
}),
},
stopWhen: [stepCountIs(5)],
providerOptions: { openai: { parallelToolCalls: false } },
});
```
To implement human approval steps, you can use Restate's awakeables. An awakeable is a promise that can be resolved externally via an API call by providing its ID.
When you create the awakeable, you get back an ID and a promise. You can send the ID to the human approver, and then wait for the promise to be resolved.
You can also use awakeables outside of tools, for example, to implement human approval steps in between agent iterations.
Start a request for a high-value claim that needs human approval, by clicking on the `run` handler of the `HumanClaimApprovalAgent`, and sending the default request via the playground.
Or use `curl` with `/send` to start the claim asynchronously, without waiting for the result.
```bash theme={null}
curl localhost:8080/HumanClaimApprovalAgent/run/send \
--json '{"prompt": "Process my hospital bill of 3000USD for a broken leg."}'
```
You can restart the service to see how Restate continues waiting for the approval.
If you wait for more than a minute, the invocation will get suspended.
Simulate approving the claim by executing the **curl request that was printed in the service logs**, similar to:
```bash theme={null}
curl localhost:8080/restate/awakeables/sign_1M28aqY6ZfuwBmRnmyP/resolve --json 'true'
```
See in the UI how the workflow resumes and finishes after the approval.
Add timeouts to human approval steps to prevent workflows from hanging indefinitely.
Restate persists the timer and the approval promise, so if the service crashes or is restarted, it will continue waiting with the correct remaining time:
```typescript humanintheloop/agent-with-timeout.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/vercel-ai/tour-of-agents/src/humanintheloop/agent-with-timeout.ts#here"} theme={null}
try {
// At most 3 hours, to reach our SLA
const approved = await approval.promise.orTimeout({ hours: 3 });
return { approved };
} catch (e) {
if (e instanceof TimeoutError) {
return {
approved: false,
reason: "Approval timed out - Evaluate with AI",
};
}
throw e;
}
```
Try it out by sending a request to the service:
```bash theme={null}
curl localhost:8080/HumanClaimApprovalWithTimeoutsAgent/run/send \
--json '{"prompt": "Process my hospital bill of 3000USD for a broken leg."}'
```
You restart the service and check in the UI how the process will block for the remaining time without starting over.
You can also lower the timeout to a few seconds to see how the timeout path is taken.
## Chat Agent with Memory
The next ingredient we need to build AI agents is the ability to maintain context and memory across multiple interactions.
To implement stateful entities like chat sessions, or stateful agents, Restate provides Virtual Objects.
Each Virtual Object instance maintains isolated state and is identified by a unique key.
Here is an example of a Virtual Object that represents chat sessions:
```typescript chat/agent.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/vercel-ai/tour-of-agents/src/chat/agent.ts?collapse_prequel"} theme={null}
export default restate.object({
name: "Chat",
handlers: {
message: async (ctx: restate.ObjectContext, req: { message: string }) => {
const model = wrapLanguageModel({
model: openai("gpt-4o"),
middleware: durableCalls(ctx, { maxRetryAttempts: 3 }),
});
const messages =
(await ctx.get("messages", superJson)) ?? [];
messages.push({ role: "user", content: req.message });
const res = await generateText({
model,
system: "You are a helpful assistant.",
messages,
});
ctx.set("messages", [...messages, ...res.response.messages], superJson);
return { answer: res.text };
},
getHistory: shared(async (ctx: restate.ObjectSharedContext) =>
ctx.get("messages", superJson),
),
},
});
```
Virtual Objects are ideal for implementing any entity with mutable state:
* **Long-lived state**: K/V state is stored permanently. It has no automatic expiry. Clear it via `ctx.clear()`.
* **Durable state changes**: State changes are logged with Durable Execution, so they survive failures and are consistent with code execution
* **State is queryable** via the state tab in the UI.
* **Built-in concurrency control**: Restate’s Virtual Objects have built-in queuing and consistency guarantees per object key. Handlers either have read-write access (`ObjectContext`) or read-only access (shared object context).
* Only one handler with write access can run at a time per object key to prevent concurrent/lost writes or race conditions (for example `message()`).
* Handlers with read-only access can run concurrently to the write-access handlers (for example `getHistory()`).
**Stateful Chat Agent:**
Ask the agent to do some task:
```bash theme={null}
curl localhost:8080/Chat/session123/message \
--json '{"message": "make a poem about durable execution"}'
```
Continue the conversation - the agent remembers previous context:
```bash theme={null}
curl localhost:8080/Chat/session123/message \
--json '{"message": "shorten it to 2 lines"}'
```
Get conversation history or view it in the UI:
```bash theme={null}
curl localhost:8080/Chat/session123/getHistory
```
**Seeing concurrency control in action:**
In the chat service, the `message` handler is an exclusive handler, while the `getHistory` handler is a shared handler.
Let's send some messages to a chat session:
```bash theme={null}
curl localhost:8080/Chat/session123/message/send --json '{"message": "make a poem about durable execution"}' &
curl localhost:8080/Chat/session456/message/send --json '{"message": "what are the benefits of durable execution?"}' &
curl localhost:8080/Chat/session789/message/send --json '{"message": "how does workflow orchestration work?"}' &
curl localhost:8080/Chat/session123/message/send --json '{"message": "can you make it rhyme better?"}' &
curl localhost:8080/Chat/session456/message/send --json '{"message": "what about fault tolerance in distributed systems?"}' &
curl localhost:8080/Chat/session789/message/send --json '{"message": "give me a practical example"}' &
curl localhost:8080/Chat/session101/message/send --json '{"message": "explain event sourcing in simple terms"}' &
curl localhost:8080/Chat/session202/message/send --json '{"message": "what is the difference between async and sync processing?"}'
```
The UI shows how Restate queues the requests per session to ensure consistency:
You can run Virtual Objects on serverless platforms like Vercel, Modal, Cloudflare Workers, or AWS Lambda.
When the request comes in, Restate attaches the correct state to the request, so your handler can access it locally.
This way, you can implement [stateful, serverless agents without managing any external state store](https://www.restate.dev/blog/resilient-serverless-agents#cost-efficiency-scaling-to-zero-while-waiting) and without worrying about concurrency issues.
## Agent Orchestration
As your agents grow more complex, you may want to break them down into smaller, specialized sub-workflows and sub-agents.
Each of these can then be developed, deployed, and scaled independently.
### Tools as sub-workflows
You can pull out complex parts of your tool logic into separate workflows.
The Restate SDK gives you clients to call other Restate services durably from your agent logic.
All calls are proxied via Restate. Restate persists the call and takes care of retries and recovery.
For example, let's implement the human approval tool as a separate service:
```typescript orchestration/sub-workflow-agent.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/vercel-ai/tour-of-agents/src/orchestration/sub-workflow-agent.ts#wf"} theme={null}
export const humanApprovalWorfklow = restate.service({
name: "HumanApprovalWorkflow",
handlers: {
requestApproval: async (ctx: restate.Context, claim: InsuranceClaim) => {
const approval = ctx.awakeable();
await ctx.run("request-review", () =>
requestHumanReview(claim, approval.id),
);
return approval.promise;
},
},
});
```
This can now be called from the main agent via a service client:
```typescript orchestration/sub-workflow-agent.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/vercel-ai/tour-of-agents/src/orchestration/sub-workflow-agent.ts#here"} theme={null}
humanApproval: tool({
description: "Ask for human approval for high-value claims.",
inputSchema: InsuranceClaimSchema,
execute: async (claim: InsuranceClaim) =>
ctx.serviceClient(humanApprovalWorfklow).requestApproval(claim),
}),
```
These workflows have access to all Restate SDK features, including durable execution, state management, awakeables, and observability.
They can be developed, deployed, and scaled independently.
Start a request for a high-value claim that needs human approval.
Use `/send` to start the claim asynchronously, without waiting for the result.
```bash theme={null}
curl localhost:8080/SubWorkflowClaimApprovalAgent/run/send \
--json '{"prompt": "Process my hospital bill of 3000USD for a broken leg."}'
```
In the UI, you can see that the agent called the workflow service and is waiting for the response.
You can see the trace of the sub-workflow in the timeline.
Once you approve the claim, the workflow returns, and the agent continues.
Follow the [Tour of Workflows](/tour/workflows) to learn more about implementing resilient workflows with Restate.
### Multi-agent Systems
Similar to sub-workflows, you can break down complex agents into multiple specialized agents.
You can let your agent hand off tasks to other agents by calling them from tools:
```typescript orchestration/multi-agent.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/vercel-ai/tour-of-agents/src/orchestration/multi-agent.ts#here"} theme={null}
const { text } = await generateText({
model,
prompt: `Claim: ${JSON.stringify(claim)}`,
system:
"You are a claim approval engine. Analyze the claim and use your tools to decide whether to approve.",
tools: {
analyzeEligibility: tool({
description: "Analyze claim eligibility.",
inputSchema: InsuranceClaimSchema,
execute: async (claim: InsuranceClaim) =>
ctx.serviceClient(eligibilityAgent).run(claim),
}),
analyzeFraud: tool({
description: "Analyze probability of fraud.",
inputSchema: InsuranceClaimSchema,
execute: async (claim: InsuranceClaim) =>
ctx.serviceClient(fraudCheckAgent).run(claim),
}),
},
stopWhen: [stepCountIs(10)],
providerOptions: { openai: { parallelToolCalls: false } },
});
```
Start a request for a claim that needs to be analyzed by multiple agents.
```bash theme={null}
curl localhost:8080/MultiAgentClaimApproval/run --json '{
"date":"2024-10-01",
"category":"orthopedic",
"reason":"hospital bill for a broken leg",
"amount":3000,
"placeOfService":"General Hospital"
}'
```
In the UI, you can see that the agent called the sub-agents and is waiting for their responses.
You can see the trace of the sub-agents in the timeline.
Once all sub-agents return, the main agent continues and makes a decision.
## Parallel Work
Now that our agents are broken down into smaller parts, let's have a look at how to run different parts of our agent logic in parallel to speed up execution.
You might have noticed that all example agents set `parallelToolCalls: false` in the OpenAI provider options.
This is required to ensure deterministic execution during replays.
When multiple tools execute in parallel and use the Context, the order of operations might differ between the original execution and the replay, leading to inconsistencies.
Restate provides primitives that allow you to run tasks concurrently while maintaining deterministic execution during replays.
Most actions on the Restate Context return a RestatePromise. These can be composed using `RestatePromise.all`, `RestatePromise.allSettled`, and `RestatePromise.race` to gather their results.
### Parallel Tool Steps
To parallelize tool steps, implement an orchestrator tool that uses `RestatePromise` to run multiple steps in parallel.
Here is an insurance claim agent that runs multiple analyses in parallel:
```typescript parallelwork/parallel-tools-agent.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/vercel-ai/tour-of-agents/src/parallelwork/parallel-tools-agent.ts#here"} theme={null}
const { text } = await generateText({
model,
prompt: `Analyze the claim ${JSON.stringify(claim)}.
Use your tools to calculate key metrics and decide whether to approve.`,
tools: {
calculateMetrics: tool({
description: "Calculate claim metrics.",
inputSchema: InsuranceClaimSchema,
execute: async (claim: InsuranceClaim) => {
// Execute each calculation as a parallel durable step
return RestatePromise.all([
ctx.run("eligibility", () => checkEligibility(claim)),
ctx.run("cost", () => compareToStandardRates(claim)),
ctx.run("fraud", () => checkFraud(claim)),
]);
},
}),
},
stopWhen: [stepCountIs(10)],
providerOptions: { openai: { parallelToolCalls: false } },
});
```
Restate makes sure that all parallel tasks are retried and recovered until they succeed.
If you want to allow the LLM to call multiple tools in parallel with `parallelToolCalls: true`, then you need to [manually implement the agent tool execution loop](#advanced-patterns) using `RestatePromise`.
Start a request for a claim that needs to be analyzed by multiple tools in parallel.
```bash theme={null}
curl localhost:8080/ParallelToolClaimAgent/run --json '{
"date":"2024-10-01",
"category":"orthopedic",
"reason":"hospital bill for a broken leg",
"amount":3000,
"placeOfService":"General Hospital"
}'
```
In the UI, you can see that the agent ran the tool steps in parallel.
Their traces all start at the same time.
Once all tools return, the agent continues and makes a decision.
### Parallel Agents
You can use the same `RestatePromise` primitives to run multiple agents in parallel.
For example, to race agents against each other and [use the first result that returns](/develop/ts/concurrent-tasks#wait-for-the-first-successful-completion), while [cancelling the others](/foundations/invocations#cancelling-invocations).
Or to let a main orchestrator agent combine the results of multiple specialized agents in parallel:
```typescript parallelwork/parallel-agents.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/vercel-ai/tour-of-agents/src/parallelwork/parallel-agents.ts?collapse_prequel"} theme={null}
export default restate.service({
name: "ParallelAgentClaimApproval",
handlers: {
run: async (ctx: restate.Context, claim: InsuranceClaim) => {
const [eligibility, rateComparison, fraudCheck] =
await RestatePromise.all([
ctx.serviceClient(eligibilityAgent).run(claim),
ctx.serviceClient(rateComparisonAgent).run(claim),
ctx.serviceClient(fraudCheckAgent).run(claim),
]);
const model = wrapLanguageModel({
model: openai("gpt-4o"),
middleware: durableCalls(ctx, { maxRetryAttempts: 3 }),
});
const { text } = await generateText({
model,
system: "You are a claim decision engine.",
prompt: `Decide about claim ${JSON.stringify(claim)}.
Base your decision on the following analyses:
Eligibility: ${eligibility}, Cost: ${rateComparison} Fraud: ${fraudCheck}`,
});
return text;
},
},
});
```
Start a request for a claim that needs to be analyzed by multiple agents in parallel.
```bash theme={null}
curl localhost:8080/ParallelAgentClaimApproval/run --json '{
"date":"2024-10-01",
"category":"orthopedic",
"reason":"hospital bill for a broken leg",
"amount":3000,
"placeOfService":"General Hospital"
}'
```
In the UI, you can see that the handler called the sub-agents in parallel.
Once all sub-agents return, the main agent makes a decision.
## Error Handling
LLM calls are costly, so you can configure retry behavior in both Restate and your AI SDK to avoid infinite loops and high costs.
Restate distinguishes between two types of errors:
* **Transient errors**: Temporary issues like network failures or rate limits. Restate automatically retries these until they succeed or the retry policy is exhausted.
* **Terminal errors**: Permanent failures like invalid input or business rule violations. Restate does not retry these. The invocation fails permanently. You can catch these errors and handle them gracefully.
You can throw a terminal error via:
```typescript {"CODE_LOAD::ts/src/tour/agents/terminal_error.ts#terminal_error"} theme={null}
throw new TerminalError("This tool is not allowed to run for this input.");
```
You can catch and handle terminal errors in your agent logic if needed.
Many AI SDKs also have their own retry behavior for LLM calls and tool executions, so let's look at how these interact.
### Retries of LLM calls
In the Vercel AI SDK, set `maxRetries` on `generateText` (default: 2) to retry failed calls due to rate limits or transient errors.
After retries are exhausted, the agent throws an error.
Restate then retries the invocation with exponential backoff to handle longer outages or network issues.
You can limit Restate’s retries with the `maxRetryAttempts` option in `durableCalls` middleware:
```typescript errorhandling/fail-on-terminal-tool-agent.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/vercel-ai/tour-of-agents/src/errorhandling/fail-on-terminal-tool-agent.ts#max_attempts_example"} theme={null}
const model = wrapLanguageModel({
model: openai("gpt-4o"),
middleware: durableCalls(ctx, { maxRetryAttempts: 3 }),
});
```
Each Restate retry triggers up to `maxRetries` SDK attempts.
For example, with `maxRetryAttempts`: 3 and `maxRetries`: 2, a call may be attempted 6 times.
Once Restate’s retries are exhausted, the invocation fails with a `TerminalError` and won’t be retried further.
### Tool execution errors
By default, the Vercel AI SDK will convert any errors in tool executions into a message to the LLM, and the agent will decide how to proceed.
This is often desirable, as the LLM can decide to use a different tool or provide a fallback answer.
However, if you use Restate Context actions like `ctx.run` in your tool execution, Restate will retry any transient errors in these actions until they succeed.
So for all operations that might suffer from transient errors (like network calls, database interactions, etc.), you should use Context actions to make them resilient.
Here is a small practical example:
```typescript {"CODE_LOAD::ts/src/tour/agents/inline-tool-errors.ts#here"} theme={null}
// Without ctx.run - error goes straight to agent
async function myTool() {
const result = await fetch('/api/data'); // Might fail due to network
// If this fails, agent gets the error immediately
}
// With ctx.run - Restate handles retries
async function myToolWithRestate(ctx: restate.Context) {
const result = await ctx.run('fetch-data', () =>
fetch('/api/data')
);
// Network failures get retried automatically
// Only terminal errors reach the AI
}
```
If your tool calls other Restate handlers or workflows (for example sub-workflow or sub-agent), then these are also Restate Context actions that get retried until they succeed.
Terminal errors thrown from Restate Context actions are not retried by Restate, and get processed by the Vercel AI SDK.
Also here, the Vercel AI SDK will convert the error into a message to the LLM, and the agent will decide how to proceed.
In some cases, you might want to treat terminal tool execution errors as permanent failures and stop the agent instead of letting the LLM decide how to proceed.
The Restate middleware provides two utilities to help with this:
**To fail the agent on terminal tool errors**, rethrow the error in `onStepFinish`:
```typescript errorhandling/fail-on-terminal-tool-agent.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/vercel-ai/tour-of-agents/src/errorhandling/fail-on-terminal-tool-agent.ts#option2"} theme={null}
const { text } = await generateText({
model,
tools: {
getWeather: tool({
description: "Get the current weather for a given city.",
inputSchema: z.object({ city: z.string() }),
execute: async ({ city }) => {
return await ctx.run("get weather", () => fetchWeather(city));
},
}),
},
stopWhen: [stepCountIs(5)],
onStepFinish: rethrowTerminalToolError,
system: "You are a helpful agent that provides weather updates.",
messages: [{ role: "user", content: prompt }],
});
```
To stop the agent on terminal tool errors and handle it after the agent finishes, you can use `hasTerminalToolError` in `stopWhen` and then inspect the steps for errors:
```typescript errorhandling/stop-on-terminal-tool-agent.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/vercel-ai/tour-of-agents/src/errorhandling/stop-on-terminal-tool-agent.ts#option3"} theme={null}
const { steps, text } = await generateText({
model,
tools: {
getWeather: tool({
description: "Get the current weather for a given city.",
inputSchema: z.object({ city: z.string() }),
execute: async ({ city }) => {
return await ctx.run("get weather", () => fetchWeather(city));
},
}),
},
stopWhen: [stepCountIs(5), hasTerminalToolError],
system: "You are a helpful agent that provides weather updates.",
messages: [{ role: "user", content: prompt }],
});
const terminalSteps = getTerminalToolSteps(steps);
if (terminalSteps.length > 0) {
// Do something with the terminal tool error steps
}
```
You can catch and handle terminal errors in your agent logic if needed.
Have a look at the advanced patterns section for an example of rolling back previous tool executions on failure.
You can set [custom retry policies](/guides/error-handling#at-the-run-block-level) for `ctx.run` steps in your tool executions.
## Advanced patterns
If you need more control over the agent loop, you can implement it manually using Restate's durable primitives.
This allows you to:
* Parallelize tool calls with `RestatePromise`
* Implement custom stopping conditions
* Implement custom logic between steps (e.g. human approval)
* Interact with external systems between steps
* Handle errors in a custom way
Here is an example of a manual agent loop:
```typescript advanced/manual-loop-agent.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/vercel-ai/tour-of-agents/src/advanced/manual-loop-agent.ts?collapse_prequel"} theme={null}
export default restate.service({
name: "ManualLoopAgent",
handlers: {
run: async (ctx: restate.Context, { prompt }: { prompt: string }) => {
const messages = [{ role: "user", content: prompt } as ModelMessage];
while (true) {
const model = wrapLanguageModel({
model: openai("gpt-4o"),
middleware: durableCalls(ctx, { maxRetryAttempts: 3 }),
});
const result = await generateText({
model,
messages,
tools: {
getWeather: tool({
name: "getWeather",
description: "Get the current weather in a given location",
inputSchema: z.object({
city: z.string(),
}),
}),
// add more tools here, omitting the execute function so you handle it yourself
},
});
messages.push(...result.response.messages);
if (result.finishReason === "tool-calls") {
// Handle all tool call execution here
for (const toolCall of result.toolCalls) {
if (toolCall.toolName === "getWeather") {
const toolOutput = await getWeather(
ctx,
toolCall.input as { city: string },
);
messages.push({
role: "tool",
content: [
{
toolName: toolCall.toolName,
toolCallId: toolCall.toolCallId,
type: "tool-result",
output: { type: "json", value: toolOutput },
},
],
});
}
// Handle other tool calls
}
} else {
return result.text;
}
}
},
},
});
```
This can be extended to include any custom control flow you need: persistent state, parallel tool calls, custom stopping conditions, or custom error handling.
Try it out by sending a request to the service:
```bash theme={null}
curl localhost:8080/ManualLoopAgent/run \
--json '{"prompt": "What is the weather like in New York and San Francisco?"}'
```
In the UI, you can see how the agent runs multiple iterations and calls tools.
Sometimes you need to undo previous agent actions when a later step fails. Restate makes it easy to implement compensation patterns (Sagas) for AI agents.
Just track the rollback actions as you go, let the agent rethrow terminal tool errors, and execute the rollback actions in reverse order.
Here is an example of a travel booking agent that first reserves a hotel, flight and car, and then either confirms them or rolls back if any step fails with a terminal error (e.g. car type not available):
```typescript advanced/rollback-agent.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/ai-examples/refs/heads/main/vercel-ai/tour-of-agents/src/advanced/rollback-agent.ts#here"} theme={null}
const book = async (ctx: restate.Context, { bookingId, prompt }: { bookingId: string, prompt: string }) => {
const on_rollback: { (): restate.RestatePromise }[] = [];
const model = wrapLanguageModel({
model: openai("gpt-4o"),
middleware: durableCalls(ctx, { maxRetryAttempts: 3 }),
});
try {
const { text } = await generateText({
model,
system: `Book a complete travel package with the requirements in the prompt.
Use tools to first book the hotel, then the flight.`,
prompt,
tools: {
bookHotel: tool({
description: "Book a hotel reservation",
inputSchema: HotelBookingSchema,
execute: async (req: HotelBooking) => {
on_rollback.push(() =>
ctx.run("cancel-hotel", () => cancelHotel(bookingId)),
);
return ctx.run("book-hotel", () => reserveHotel(bookingId, req));
},
}),
bookFlight: tool({
description: "Book a flight",
inputSchema: FlightBookingSchema,
execute: async (req: FlightBooking) => {
on_rollback.push(() =>
ctx.run("cancel-flight", () => cancelFlight(bookingId)),
);
return ctx.run("book-flight", () => reserveFlight(bookingId, req));
},
}),
// ... similar for car rental ...
},
stopWhen: [stepCountIs(10)],
onStepFinish: rethrowTerminalToolError,
providerOptions: { openai: { parallelToolCalls: false } },
});
return text;
} catch (error) {
console.log("Error occurred, rolling back all bookings...");
for (const rollback of on_rollback.reverse()) {
await rollback();
}
throw error;
}
};
```
Try it out by sending the following request:
```bash theme={null}
curl localhost:8080/BookingWithRollbackAgent/book \
--json '{
"bookingId": "booking_123",
"prompt": "I need to book a business trip to San Francisco from March 15-17. Flying from JFK, need a hotel downtown for 1 guest."
}'
```
Have a look at the UI to see how the flight booking fails, and the bookings are rolled back.
Check out the [sagas guide](/guides/sagas) for more details.
Restate supports implementing scheduling and timer logic in your agents.
This allows you to build agents that run periodically, wait for specific times, or implement complex scheduling logic.
Agents can either be long-running or reschedule themselves for later execution.
Have a look at the [scheduling docs](/develop/ts/durable-timers) to learn more.
Have a look at the [pub-sub example](https://github.com/igalshilman/agent47/tree/main/packages/pubsub).
Have a look at the [interruptible coding agent](https://github.com/igalshilman/agent47/tree/main/packages/agent).
## Summary
Durable Execution, paired with your existing SDKs, gives your agents a powerful upgrade:
* **Durable Execution**: Automatic recovery from failures without losing progress
* **Persistent memory and context**: Persistent conversation history and context
* **Observability** by default across your agents and workflows
* **Human-in-the-Loop**: Seamless approval workflows with timeouts
* **Multi-Agent Coordination**: Reliable orchestration of specialized agents
* **Suspensions** to save costs on function-as-a-service platforms when agents need to wait
* **Advanced Patterns**: Real-time progress updates, interruptions, and long-running workflows
## Next Steps
* Learn more about how to implement resilient tools with Restate in the [Tour of Workflows](/tour/workflows)
* Check out the [other Restate AI examples on GitHub](https://github.com/restatedev/ai-examples)
* Sign up for [Restate Cloud](https://restate.dev/cloud/) and start building agents without managing infrastructure
# Workflows
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/tour/workflows
Build resilient workflows with familiar programming patterns.
export const GitHubLink = ({url}) =>
;
};
Workflows orchestrate complex business processes that span multiple steps, services, and time periods. Restate workflows are written as regular functions in your programming language, with automatic durability, state management, and event handling built in.
In this guide, you'll learn how to:
* Write workflows as regular functions with automatic durability
* Handle long-running processes with state and event patterns
* Deploy steps inline or services that can scale independently
* Build resilient, observable workflows without external dependencies
Select your SDK:
## Getting Started
A Restate application is composed of two main components:
* **Restate Server**: The core engine that manages durable execution and orchestrates services. It acts as a message broker or reverse proxy in front of your services.
* **Your Services**: Your workflows and business logic, implemented as service handlers using the Restate SDK to perform durable operations.
A basic signup workflow looks like this:
```typescript signup-workflow.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-workflows-typescript/src/workflows/signup-workflow.ts?collapse_prequel"} theme={null}
export const signupWorkflow = restate.workflow({
name: "SignupWorkflow",
handlers: {
run: async (ctx: WorkflowContext, user: User) => {
const userId = ctx.key; // workflow ID = user ID
// Write to database
const success = await ctx.run("create", () => createUser(userId, user));
if (!success) return { success };
// Call APIs
await ctx.run("activate", () => activateUser(userId));
await ctx.run("welcome", () => sendWelcomeEmail(user));
return { success };
},
},
});
```
```java SignupWorkflow.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-workflows-java/src/main/java/my/example/workflows/SignupWorkflow.java?collapse_prequel"} theme={null}
@Workflow
public class SignupWorkflow {
@Workflow
public boolean run(WorkflowContext ctx, User user) {
String userId = ctx.key(); // workflow ID = user ID
// Write to database
boolean success = ctx.run("create", Boolean.class, () -> createUser(userId, user));
if (!success) {
return false;
}
// Call APIs
ctx.run("activate", () -> activateUser(userId));
ctx.run("welcome", () -> sendWelcomeEmail(user));
return true;
}
}
```
```go getstarted.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-workflows-go/examples/getstarted.go?collapse_prequel"} theme={null}
type SignupWorkflow struct{}
func (SignupWorkflow) Run(ctx restate.WorkflowContext, user User) (bool, error) {
userID := restate.Key(ctx) // workflow ID = user ID
// Write to database
success, err := restate.Run(ctx, func(ctx restate.RunContext) (bool, error) {
return CreateUser(userID, user)
}, restate.WithName("create"))
if err != nil || !success {
return false, err
}
// Call APIs
_, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return ActivateUser(userID)
}, restate.WithName("activate"))
if err != nil {
return false, err
}
_, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return SendWelcomeEmail(user)
}, restate.WithName("welcome"))
if err != nil {
return false, err
}
return true, nil
}
```
```python signup_workflow.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-workflows-python/app/workflows/signup_workflow.py?collapse_prequel"} theme={null}
signup_workflow = restate.Workflow("SignupWorkflow")
@signup_workflow.main()
async def run(ctx: WorkflowContext, user: User) -> bool:
user_id = ctx.key() # workflow ID = user ID
# Write to database
success = await ctx.run_typed("create", create_user, user_id=user_id, user=user)
if not success:
return False
# Call APIs
await ctx.run_typed("activate", activate_user, user_id=user_id)
await ctx.run_typed("welcome", send_welcome_email, user=user)
return True
```
A workflow has handlers that can be called over HTTP.
The `run` handler is the main entry point that executes the workflow logic.
An execution of the workflow is identified by a unique key (in this case, the user ID)
and uses Restate's `WorkflowContext` to make steps durable.
You don't need to run your services in any special way. Restate works with how you already deploy your code, whether that's in Docker, on Kubernetes, or via AWS Lambda.
The endpoint that serves the workflows of this tour over HTTP is defined in `src/app.ts`.
The endpoint that serves the workflows of this tour over HTTP is defined in `AppMain.java`.
The endpoint that serves the workflows of this tour over HTTP is defined in `main.go`.
The endpoint that serves the workflows of this tour over HTTP is defined in `__main__.py`.
### Run the example
[Install Restate](/installation) and launch it:
```bash theme={null}
restate-server
```
Get the example:
```bash theme={null}
restate example typescript-tour-of-workflows && cd typescript-tour-of-workflows
npm install
```
Run the example:
```bash theme={null}
npm run dev
```
Then, tell Restate where your workflow is running via the UI (`http://localhost:9070`) or CLI:
```bash theme={null}
restate deployments register http://localhost:9080
```
Get the example:
```bash theme={null}
restate example java-tour-of-workflows && cd java-tour-of-workflows
```
Run the example:
```bash theme={null}
./gradlew run
```
Then, tell Restate where your services are running via the UI (`http://localhost:9070`) or CLI:
```bash theme={null}
restate deployments register http://localhost:9080
```
Get the example:
```bash theme={null}
restate example go-tour-of-workflows && cd go-tour-of-workflows
```
Run the example:
```bash theme={null}
go run .
```
Then, tell Restate where your services are running via the UI (`http://localhost:9070`) or CLI:
```bash theme={null}
restate deployments register http://localhost:9080
```
Get the example:
```bash theme={null}
restate example python-tour-of-workflows && cd python-tour-of-workflows
```
Run the example:
```bash theme={null}
uv run .
```
Then, tell Restate where your services are running via the UI (`http://localhost:9070`) or CLI:
```bash theme={null}
restate deployments register http://localhost:9080
```
This registers a set of workflows that we will be covering in this tutorial.
## Submitting Workflows
The workflow can be submitted over HTTP, Kafka, programmatically, or via the UI.
To submit the workflow via HTTP send the request to `restate-ingress/workflow-name/key/run`, in our case:
```bash theme={null}
curl localhost:8080/SignupWorkflow/johndoe/run \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWorkflow/johndoe/run \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWorkflow/johndoe/Run \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWorkflow/johndoe/run \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
Restate deduplicates workflow executions on the key, here `johndoe`.
Resubmission of the same workflow will fail with "Previously accepted". The invocation ID can be found in the request header `x-restate-id` (add `-v` to your request).
To try out a workflow multiple times during the tour, use a different key.
You can invoke a workflow programmatically with the Restate SDK:
```ts client.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-workflows-typescript/src/client.ts#submit"} theme={null}
const restateClient = clients.connect({ url: "http://localhost:8080" });
const handle = await restateClient
.workflowClient(signupWorkflow, id)
.workflowSubmit({ name, email });
const result = await restateClient.result(handle);
```
The workflow gets submitted and afterwards you can retrieve the result by attaching to it.
Run the client script via:
```bash theme={null}
npm run client
```
You can invoke a workflow programmatically with the Restate SDK:
```java WorkflowSubmitter.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-workflows-java/src/main/java/my/example/WorkflowSubmitter.java#submit"} theme={null}
Client restateClient = Client.connect("http://localhost:8080");
boolean result =
SignupWorkflowClient.fromClient(restateClient, "user-123").submit(user).attach().response();
```
The workflow gets submitted and afterwards you can retrieve the result by attaching to it.
Run the client script via:
```bash theme={null}
./gradlew -PmainClass=my.example.WorkflowSubmitter run
```
You can invoke a workflow programmatically with the Restate SDK:
```go client.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-workflows-go/client/client.go#submit"} theme={null}
restateClient := restateingress.NewClient("http://localhost:8080")
result, err := restateingress.Workflow[utils.User, bool](
restateClient, "SignupWorkflow", "user-123", "Run").
Request(context.Background(), user)
```
The workflow gets submitted and afterwards you can retrieve the result by attaching to it.
Run the client script via:
```bash theme={null}
go run ./client
```
You can invoke a workflow programmatically by sending an HTTP request:
```python client.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-workflows-python/client.py#submit"} theme={null}
key = "user-123"
url = "http://127.0.0.1:8080/SignupWorkflow/" + key + "/run"
payload = {"name": "John Doe", "email": "john@mail.com"}
headers = {"Content-Type": "application/json", "Accept": "application/json"}
response = requests.post(url, json=payload, headers=headers)
```
Run the client script via:
```bash theme={null}
uv run client.py
```
You can schedule a workflow to run at a later time by specifying a delay:
```bash theme={null}
curl "localhost:8080/SignupWorkflow/petewhite/run/send?delay=5m" \
--json '{"name": "Pete White", "email": "pete@mail.com"}'
```
```bash theme={null}
curl "localhost:8080/SignupWorkflow/petewhite/run/send?delay=5m" \
--json '{"name": "Pete White", "email": "pete@mail.com"}'
```
```bash theme={null}
curl "localhost:8080/SignupWorkflow/petewhite/Run/send?delay=5m" \
--json '{"name": "Pete White", "email": "pete@mail.com"}'
```
```bash theme={null}
curl "localhost:8080/SignupWorkflow/petewhite/run/send?delay=5m" \
--json '{"name": "Pete White", "email": "pete@mail.com"}'
```
There is no limit to how long you can delay a workflow (works for months, even years).
Have a look at the SDK docs to learn how to schedule workflows programmatically ([TS](/services/invocation/clients/typescript-sdk) / [Java](/services/invocation/clients/java-sdk) / [Go](/services/invocation/clients/go-sdk)).
If a workflow is already ongoing, you can also attach to it to get the result once it finishes:
```bash theme={null}
curl localhost:8080/restate/workflow/SignupWorkflow/johndoe/attach
```
Have a look at the SDK docs to learn how to attach to workflows programmatically ([TS](/services/invocation/clients/typescript-sdk) / [Java](/services/invocation/clients/java-sdk) / [Go](/services/invocation/clients/go-sdk)).
## Durable Execution
Restate uses Durable Execution to ensure your business logic survives any failure and resumes exactly where it left off. Unlike traditional workflow systems that require separate orchestrator infrastructure and worker management, Restate lets you deploy your workflows the same way you deploy your application code.
You write a workflow as a regular function. You use the Restate SDK to persist the steps your workflow completes in the Restate Server.
If your workflow crashes or restarts, the execution replays from the journal to restore state and continue processing:
To persist a workflow step, you use the `WorkflowContext` actions:
* **Durable Steps**: Restate's run actions ensures non-deterministic operations like database writes or external API calls are persisted
* **Progress Recovery**: If the workflow crashes after user creation, it resumes at the email step
* **Observability**: Full execution traces for debugging and monitoring
Send a request for Alice:
```bash theme={null}
curl localhost:8080/SignupWorkflow/alicedoe/run \
--json '{"name": "Alice", "email": "alice@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWorkflow/alicedoe/run \
--json '{"name": "Alice", "email": "alice@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWorkflow/alicedoe/Run \
--json '{"name": "Alice", "email": "alice@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWorkflow/alicedoe/run \
--json '{"name": "Alice", "email": "alice@mail.com"}'
```
Go to the UI at `http://localhost:9070`, on the invocations page, and click on the invocation ID of the retrying invocation:
You see how the invocation went through the steps of the workflow, and how it is stuck on retrying to send the welcome email.
To fix the problem, remove the line `failOnAlice` from the `sendWelcomeEmail` function in the `utils.ts` file:
```ts utils.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-workflows-typescript/src/utils.ts#here"} theme={null}
export function sendWelcomeEmail(user: User) {
failOnAlice(user.name, "send welcome email");
console.log(`Welcome email sent: ${user.email}`);
}
```
To fix the problem, remove the line `failOnAlice` from the `sendWelcomeEmail` function in the `Utils.java` file:
```java Utils.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-workflows-java/src/main/java/my/example/utils/Utils.java#here"} theme={null}
private static void terminalErrorOnAlice(String name, String action) {
if ("Alice".equals(name)) {
String message =
"[👻 SIMULATED] Failed to " + action + " for " + name + ": not available in this country";
System.err.println(message);
throw new TerminalException(message);
}
}
```
To fix the problem, remove the line `failOnAlice` from the `sendWelcomeEmail` function in the `utils.go` file:
```go utils.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-workflows-go/examples/utils.go#here"} theme={null}
func SendWelcomeEmail(user User) (restate.Void, error) {
if err := failOnAlice(user.Name, "send welcome email"); err != nil {
return restate.Void{}, err
}
fmt.Printf("Welcome email sent: %s\n", user.Email)
return restate.Void{}, nil
}
```
To fix the problem, remove the line `fail_on_alice` from the `send_welcome_email` function in the `utils.py` file:
```python utils.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-workflows-python/app/utils.py#here"} theme={null}
def send_welcome_email(user: User):
fail_on_alice(user.name, "send welcome email")
print(f"Welcome email sent: {user.email}")
```
Once you restart the service, the workflow finishes successfully:
## In-line Steps vs. Separate Activities
Restate workflows can execute operations inline or delegate to separate services, giving you flexibility in how you structure your applications.
* **In-line Steps** - Execute directly in the workflow, for example a run block.
* **Separate Activities** - Call dedicated services for independent scaling, separation of concerns, or different concurrency requirements.
```ts signup-with-activities.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-workflows-typescript/src/workflows/signup-with-activities.ts#activities"} theme={null}
// Move user DB interaction to dedicated service
const success = await ctx
.serviceClient(userService)
.createUser({ userId, user });
if (!success) return { success };
// Execute other steps inline
await ctx.run("activate", () => activateUser(userId));
await ctx.run("welcome", () => sendWelcomeEmail(user));
```
```java SignupWithActivitiesWorkflow.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-workflows-java/src/main/java/my/example/workflows/SignupWithActivitiesWorkflow.java#activities"} theme={null}
// Move user DB interaction to dedicated service
boolean success = true;
UserServiceClient.fromContext(ctx).createUser(new CreateUserRequest(userId, user)).await();
if (!success) {
return false;
}
// Execute other steps inline
ctx.run("activate", () -> activateUser(userId));
ctx.run("welcome", () -> sendWelcomeEmail(user));
```
```go activities.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-workflows-go/examples/activities.go#activities"} theme={null}
// Move user DB interaction to dedicated service
success, err := restate.Service[bool](ctx, "UserService", "CreateUser").
Request(CreateUserRequest{UserID: userID, User: user})
if err != nil || !success {
return false, err
}
// Execute other steps inline
_, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return ActivateUser(userID)
}, restate.WithName("activate"))
if err != nil {
return false, err
}
_, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return SendWelcomeEmail(user)
}, restate.WithName("welcome"))
if err != nil {
return false, err
}
```
```python signup_with_activities.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-workflows-python/app/workflows/signup_with_activities.py#activities"} theme={null}
# Move user DB interaction to dedicated service
success = await ctx.service_call(
create_user_handler, arg=CreateUserRequest(user_id=user_id, user=user)
)
if not success:
return False
# Execute other steps inline
await ctx.run_typed("activate", activate_user, user_id=user_id)
await ctx.run_typed("welcome", send_welcome_email, user=user)
```
You can also use this to nest workflows. The main workflow can call other workflows as activities.
Workflows are just one of the service types Restate supports. The other service types are:
* [Services](/foundations/services): collections of independent handlers which get executed with Durable Execution.
* [Virtual Objects](/foundations/services): stateful services that can be used to manage state and concurrency across multiple invocations.
To learn more, follow at the [Microservice Orchestration Tour](/tour/microservice-orchestration).
Submit the workflow:
```bash theme={null}
curl localhost:8080/SignupWithActivitiesWorkflow/carl/run \
--json '{"name": "Carl", "email": "carl@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWithActivitiesWorkflow/carl/run \
--json '{"name": "Carl", "email": "carl@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWithActivitiesWorkflow/carl/Run \
--json '{"name": "Carl", "email": "carl@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWithActivitiesWorkflow/carl/run \
--json '{"name": "Carl", "email": "carl@mail.com"}'
```
In the UI, you can see how the invocation called another service called user service:
## Workflow Patterns
Restate provides powerful patterns for building complex workflows using familiar programming constructs.
### Querying Workflow State
Workflows can store state in Restate, which can be queried later by other handlers:
```ts signup-with-queries.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-workflows-typescript/src/workflows/signup-with-queries.ts?collapse_prequel"} theme={null}
export const signupWithQueries = restate.workflow({
name: "SignupWithQueriesWorkflow",
handlers: {
run: async (ctx: WorkflowContext, user: User) => {
const userId = ctx.key;
ctx.set("user", user);
const success = await ctx.run("create", () => createUser(userId, user));
if (!success) {
ctx.set("status", "failed");
return { success };
}
ctx.set("status", "created");
await ctx.run("activate", () => activateUser(userId));
await ctx.run("welcome", () => sendWelcomeEmail(user));
return { success };
},
getStatus: async (ctx: WorkflowSharedContext) => {
return {
status: await ctx.get("status"),
user: await ctx.get("user"),
};
},
},
});
```
```java SignupWithQueriesWorkflow.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-workflows-java/src/main/java/my/example/workflows/SignupWithQueriesWorkflow.java?collapse_prequel"} theme={null}
@Workflow
public class SignupWithQueriesWorkflow {
private static final StateKey USER = StateKey.of("user", User.class);
private static final StateKey STATUS = StateKey.of("status", String.class);
@Workflow
public boolean run(WorkflowContext ctx, User user) {
String userId = ctx.key();
ctx.set(USER, user);
boolean success = ctx.run("create", Boolean.class, () -> createUser(userId, user));
if (!success) {
ctx.set(STATUS, "failed");
return false;
}
ctx.set(STATUS, "created");
ctx.run("activate", () -> activateUser(userId));
ctx.run("welcome", () -> sendWelcomeEmail(user));
return true;
}
@Shared
public StatusResponse getStatus(SharedWorkflowContext ctx) {
String status = ctx.get(STATUS).orElse("unknown");
User user = ctx.get(USER).orElse(null);
return new StatusResponse(status, user);
}
}
```
```go queries.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-workflows-go/examples/queries.go?collapse_prequel"} theme={null}
type SignupWithQueriesWorkflow struct{}
func (SignupWithQueriesWorkflow) Run(ctx restate.WorkflowContext, user User) (bool, error) {
userID := restate.Key(ctx)
restate.Set(ctx, "user", user)
success, err := restate.Run(ctx, func(ctx restate.RunContext) (bool, error) {
return CreateUser(userID, user)
}, restate.WithName("create"))
if err != nil || !success {
restate.Set(ctx, "status", "failed")
return false, err
}
restate.Set(ctx, "status", "created")
_, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return ActivateUser(userID)
}, restate.WithName("activate"))
if err != nil {
return false, err
}
_, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return SendWelcomeEmail(user)
}, restate.WithName("welcome"))
if err != nil {
return false, err
}
return success, nil
}
func (SignupWithQueriesWorkflow) GetStatus(ctx restate.WorkflowSharedContext) (StatusResponse, error) {
status, err := restate.Get[string](ctx, "status")
if err != nil {
return StatusResponse{}, err
}
user, err := restate.Get[User](ctx, "user")
if err != nil {
return StatusResponse{}, err
}
return StatusResponse{Status: &status, User: &user}, nil
}
```
```python signup_with_queries.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-workflows-python/app/workflows/signup_with_queries.py?collapse_prequel"} theme={null}
signup_with_queries = restate.Workflow("SignupWithQueriesWorkflow")
@signup_with_queries.main()
async def run(ctx: WorkflowContext, user: User) -> bool:
user_id = ctx.key()
ctx.set("user", user.model_dump())
success = await ctx.run_typed("create", create_user, user_id=user_id, user=user)
if not success:
ctx.set("status", "failed")
return False
ctx.set("status", "created")
await ctx.run_typed("activate", activate_user, user_id=user_id)
await ctx.run_typed("welcome", send_welcome_email, user=user)
return True
@signup_with_queries.handler("getStatus")
async def get_status(ctx: WorkflowSharedContext) -> StatusResponse:
return StatusResponse(status=await ctx.get("status"), user=await ctx.get("user"))
```
Key characteristics:
* State is isolated per workflow execution.
* State lives up to the duration of the workflow retention ([default one day](/services/configuration)).
* State is queryable from other handlers or the Restate UI.
Submit the workflow:
```bash theme={null}
curl localhost:8080/SignupWithQueriesWorkflow/janedoe/run \
--json '{"name": "Jane Doe", "email": "jane@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWithQueriesWorkflow/janedoe/run \
--json '{"name": "Jane Doe", "email": "jane@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWithQueriesWorkflow/janedoe/Run \
--json '{"name": "Jane Doe", "email": "jane@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWithQueriesWorkflow/janedoe/run \
--json '{"name": "Jane Doe", "email": "jane@mail.com"}'
```
In the UI, look at the state tab and filter on the `SignupWithQueriesWorkflow`.
### Signaling
Pause workflow execution waiting for external events using durable promises:
```ts signup-with-signals.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-workflows-typescript/src/workflows/signup-with-signals.ts?collapse_prequel"} theme={null}
export const signupWithSignals = restate.workflow({
name: "SignupWithSignalsWorkflow",
handlers: {
run: async (ctx: WorkflowContext, user: User) => {
const userId = ctx.key;
// Generate verification secret and send email
const secret = ctx.rand.uuidv4();
await ctx.run("verify", () =>
sendVerificationEmail(userId, user, secret),
);
// Wait for user to click verification link
const clickedSecret = await ctx.promise("email-verified");
return { success: clickedSecret === secret };
},
verifyEmail: async (
ctx: WorkflowSharedContext,
req: { secret: string },
) => {
// Resolve the promise to continue the main workflow
await ctx.promise("email-verified").resolve(req.secret);
},
},
});
```
```java SignupWithSignalsWorkflow.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-workflows-java/src/main/java/my/example/workflows/SignupWithSignalsWorkflow.java?collapse_prequel"} theme={null}
@Workflow
public class SignupWithSignalsWorkflow {
private static final DurablePromiseKey EMAIL_VERIFIED_PROMISE =
DurablePromiseKey.of("email-verified", String.class);
@Workflow
public boolean run(WorkflowContext ctx, User user) {
String userId = ctx.key();
// Generate verification secret and send email
String secret = ctx.random().nextUUID().toString();
ctx.run("verify", () -> sendVerificationEmail(userId, user, secret));
// Wait for user to click verification link
String clickedSecret = ctx.promise(EMAIL_VERIFIED_PROMISE).future().await();
return secret.equals(clickedSecret);
}
@Shared
public void verifyEmail(SharedWorkflowContext ctx, VerifyEmailRequest req) {
ctx.promiseHandle(EMAIL_VERIFIED_PROMISE).resolve(req.secret());
}
}
```
```go signals.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-workflows-go/examples/signals.go?collapse_prequel"} theme={null}
type SignupWithSignalsWorkflow struct{}
func (SignupWithSignalsWorkflow) Run(ctx restate.WorkflowContext, user User) (bool, error) {
userID := restate.Key(ctx)
// Generate verification secret and send email
secret := restate.Rand(ctx).UUID().String()
_, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return SendVerificationEmail(userID, user, secret)
}, restate.WithName("verify"))
if err != nil {
return false, err
}
// Wait for user to click verification link
clickedSecret, err := restate.Promise[string](ctx, "email-verified").Result()
if err != nil {
return false, err
}
return clickedSecret == secret, nil
}
func (SignupWithSignalsWorkflow) VerifyEmail(ctx restate.WorkflowSharedContext, req VerifyEmailRequest) error {
// Resolve the promise to continue the main workflow
return restate.Promise[string](ctx, "email-verified").Resolve(req.Secret)
}
```
```python signup_with_signals.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-workflows-python/app/workflows/signup_with_signals.py?collapse_prequel"} theme={null}
signup_with_signals = restate.Workflow("SignupWithSignalsWorkflow")
@signup_with_signals.main()
async def run(ctx: WorkflowContext, user: User) -> bool:
user_id = ctx.key()
# Generate verification secret and send email
secret = str(ctx.uuid())
await ctx.run_typed(
"verify",
send_verification_email,
user_id=user_id,
user=user,
verification_secret=secret,
)
# Wait for user to click verification link
clicked_secret = await ctx.promise("email-verified").value()
return clicked_secret == secret
@signup_with_signals.handler("verifyEmail")
async def verify_email(ctx: WorkflowSharedContext, req: VerifyEmailRequest) -> None:
# Resolve the promise to continue the main workflow
await ctx.promise("email-verified").resolve(req.secret)
```
The promise can survive restarts and crashes, and can be recovered on another process.
You can use Restate's Durable Promises to handle asynchronous events without complex message queues or external state management.
Promises can be resolved before the workflow waits for them, avoiding complex synchronization issues.
Submit the workflow asynchronously with `/send`:
```bash theme={null}
curl localhost:8080/SignupWithSignalsWorkflow/johndoe/run/send \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWithSignalsWorkflow/johndoe/run/send \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWithSignalsWorkflow/johndoe/Run/send \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWithSignalsWorkflow/johndoe/run/send \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
In the UI, you can see the workflow waiting for the `email-verified` promise to be resolved.
Try killing the service and restarting it. The workflow will continue waiting for the promise to be resolved.
To resolve the promise, **copy over the curl request from the service logs**, which looks like this:
```bash theme={null}
curl localhost:8080/SignupWithSignalsWorkflow/johndoe/verifyEmail \
--json '{"secret": "the-secret-from-email"}'
```
```bash theme={null}
curl localhost:8080/SignupWithSignalsWorkflow/johndoe/verifyEmail \
--json '{"secret": "the-secret-from-email"}'
```
```bash theme={null}
curl localhost:8080/SignupWithSignalsWorkflow/johndoe/VerifyEmail \
--json '{"secret": "the-secret-from-email"}'
```
```bash theme={null}
curl localhost:8080/SignupWithSignalsWorkflow/johndoe/verifyEmail \
--json '{"secret": "the-secret-from-email"}'
```
Now the UI will show the workflow completed successfully.
### Workflow Events
You can also use promises the other way around: to send events from the workflow and wait on them in one of the other handlers:
```ts signup-with-events.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-workflows-typescript/src/workflows/signup-with-events.ts?collapse_prequel"} theme={null}
export const signupWithEvents = restate.workflow({
name: "SignupWithEventsWorkflow",
handlers: {
run: async (ctx: WorkflowContext, user: User) => {
const userId = ctx.key;
const success = await ctx.run("create", () => createUser(userId, user));
if (!success) {
await ctx.promise("user-created").reject("Creation failed.");
return { success };
}
await ctx.promise("user-created").resolve("User created.");
await ctx.run("activate", () => activateUser(userId));
await ctx.run("welcome", () => sendWelcomeEmail(user));
return { success };
},
waitForUserCreation: async (ctx: WorkflowSharedContext) => {
return ctx.promise("user-created");
},
},
});
```
```java SignupWithEventsWorkflow.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-workflows-java/src/main/java/my/example/workflows/SignupWithEventsWorkflow.java?collapse_prequel"} theme={null}
@Workflow
public class SignupWithEventsWorkflow {
private static final DurablePromiseKey USER_CREATED_PROMISE =
DurablePromiseKey.of("user-created", String.class);
@Workflow
public boolean run(WorkflowContext ctx, User user) {
String userId = ctx.key();
boolean success = ctx.run("create", Boolean.class, () -> createUser(userId, user));
if (!success) {
ctx.promiseHandle(USER_CREATED_PROMISE).reject("Creation failed.");
return false;
}
ctx.promiseHandle(USER_CREATED_PROMISE).resolve("User created.");
ctx.run("activate", () -> activateUser(userId));
ctx.run("welcome", () -> sendWelcomeEmail(user));
return true;
}
@Shared
public String waitForUserCreation(SharedWorkflowContext ctx) {
return ctx.promise(USER_CREATED_PROMISE).future().await();
}
}
```
```go events.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-workflows-go/examples/events.go?collapse_prequel"} theme={null}
type SignupWithEventsWorkflow struct{}
func (SignupWithEventsWorkflow) Run(ctx restate.WorkflowContext, user User) (bool, error) {
userID := restate.Key(ctx)
success, err := restate.Run(ctx, func(ctx restate.RunContext) (bool, error) {
return CreateUser(userID, user)
}, restate.WithName("create"))
if err != nil || !success {
err = restate.Promise[string](ctx, "user-created").Reject(fmt.Errorf("creation failed"))
return false, err
}
if err := restate.Promise[string](ctx, "user-created").Resolve("User created."); err != nil {
return false, err
}
_, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return ActivateUser(userID)
}, restate.WithName("activate"))
if err != nil {
return false, err
}
_, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return SendWelcomeEmail(user)
}, restate.WithName("welcome"))
if err != nil {
return false, err
}
return true, nil
}
func (SignupWithEventsWorkflow) WaitForUserCreation(ctx restate.WorkflowSharedContext) (string, error) {
return restate.Promise[string](ctx, "user-created").Result()
}
```
```python signup_with_events.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-workflows-python/app/workflows/signup_with_events.py?collapse_prequel"} theme={null}
signup_with_events = restate.Workflow("SignupWithEventsWorkflow")
@signup_with_events.main()
async def run(ctx: WorkflowContext, user: User) -> bool:
user_id = ctx.key()
success = await ctx.run_typed("create", create_user, user_id=user_id, user=user)
if not success:
await ctx.promise("user-created").reject("Creation failed.")
return False
await ctx.promise("user-created").resolve("User created.")
await ctx.run_typed("activate", activate_user, user_id=user_id)
await ctx.run_typed("welcome", send_welcome_email, user=user)
return True
@signup_with_events.handler("waitForUserCreation")
async def wait_for_user_creation(ctx: WorkflowSharedContext) -> str:
return await ctx.promise("user-created").value()
```
Here, external clients can wait for the user to be created in the database.
These handlers can be called up to the workflow's retention period ([default one day](/services/configuration#workflow-retention).
Submit the workflow asynchronously with `/send`:
```bash theme={null}
curl localhost:8080/SignupWithEventsWorkflow/johndoe/run/send \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
Then wait for the user creation event:
```bash theme={null}
curl localhost:8080/SignupWithEventsWorkflow/johndoe/waitForUserCreation
```
```bash theme={null}
curl localhost:8080/SignupWithEventsWorkflow/johndoe/run/send \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
Then wait for the user creation event:
```bash theme={null}
curl localhost:8080/SignupWithEventsWorkflow/johndoe/waitForUserCreation
```
```bash theme={null}
curl localhost:8080/SignupWithEventsWorkflow/johndoe/Run/send \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
Then wait for the user creation event:
```bash theme={null}
curl localhost:8080/SignupWithEventsWorkflow/johndoe/WaitForUserCreation
```
```bash theme={null}
curl localhost:8080/SignupWithEventsWorkflow/johndoe/run/send \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
Then wait for the user creation event:
```bash theme={null}
curl localhost:8080/SignupWithEventsWorkflow/johndoe/waitForUserCreation
```
You will get a response like `"User created."`. If the promise hadn't been resolved yet, the request will wait until it is.
### Timers and Scheduling
Use durable timers for long-running workflows with timeouts and retries:
```ts signup-with-timers.ts expandable {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-workflows-typescript/src/workflows/signup-with-timers.ts?collapse_prequel"} theme={null}
export const signupWithTimers = restate.workflow({
name: "SignupWithTimersWorkflow",
handlers: {
run: async (ctx: WorkflowContext, user: User) => {
const userId = ctx.key;
const secret = ctx.rand.uuidv4();
await ctx.run("verify", () =>
sendVerificationEmail(userId, user, secret),
);
const clickedPromise = ctx.promise("email-verified").get();
const verificationTimeout = ctx.sleep({ days: 1 });
while (true) {
const reminderTimer = ctx.sleep({ seconds: 15 });
// Wait for email verification, reminder timer or timeout
const result = await RestatePromise.race([
clickedPromise.map(() => "verified"),
reminderTimer.map(() => "reminder"),
verificationTimeout.map(() => "timeout"),
]);
switch (result) {
case "verified":
const clickedSecret = await clickedPromise;
return { success: clickedSecret === secret };
case "reminder":
await ctx.run("send reminder", () =>
sendReminderEmail(userId, user, secret),
);
break;
case "timeout":
throw new TerminalError(
"Email verification timed out after 24 hours",
);
}
}
},
verifyEmail: async (
ctx: WorkflowSharedContext,
req: { secret: string },
) => {
await ctx.promise("email-verified").resolve(req.secret);
},
},
});
```
```java SignupWithTimersWorkflow.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-workflows-java/src/main/java/my/example/workflows/SignupWithTimersWorkflow.java?collapse_prequel"} theme={null}
@Workflow
public class SignupWithTimersWorkflow {
private static final DurablePromiseKey EMAIL_VERIFIED_PROMISE =
DurablePromiseKey.of("email-verified", String.class);
@Workflow
public boolean run(WorkflowContext ctx, User user) {
String userId = ctx.key();
var confirmationFuture = ctx.promise(EMAIL_VERIFIED_PROMISE).future();
var secret = ctx.random().nextUUID().toString();
ctx.run("verify", () -> sendVerificationEmail(userId, user, secret));
var verificationTimeout = ctx.timer(Duration.ofDays(1));
while (true) {
var reminderTimer = ctx.timer(Duration.ofSeconds(10));
var selected =
Select.select()
.when(confirmationFuture, res -> "verified")
.when(reminderTimer, unused -> "reminder")
.when(verificationTimeout, unused -> "timeout")
.await();
switch (selected) {
case "verified":
var clickedSecret = confirmationFuture.await();
return secret.equals(clickedSecret);
case "reminder":
ctx.run("send reminder", () -> sendReminderEmail(userId, user, secret));
break;
case "timeout":
throw new TerminalException("Verification timed out");
}
}
}
@Shared
public void verifyEmail(SharedWorkflowContext ctx, VerifyEmailRequest req) {
ctx.promiseHandle(EMAIL_VERIFIED_PROMISE).resolve(req.secret());
}
}
```
```go timers.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-workflows-go/examples/timers.go?collapse_prequel"} theme={null}
type SignupWithTimersWorkflow struct{}
func (SignupWithTimersWorkflow) Run(ctx restate.WorkflowContext, user User) (bool, error) {
userID := restate.Key(ctx)
secret := restate.Rand(ctx).UUID().String()
_, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return SendVerificationEmail(userID, user, secret)
}, restate.WithName("verify"))
if err != nil {
return false, err
}
clickedPromise := restate.Promise[string](ctx, "email-verified")
verificationTimeoutFuture := restate.After(ctx, 24*time.Hour)
for {
reminderTimerFuture := restate.After(ctx, 15*time.Second)
// Create futures for racing
selector := restate.Select(ctx,
clickedPromise,
reminderTimerFuture,
verificationTimeoutFuture,
)
switch selector.Select() {
case clickedPromise:
clickedSecret, err := clickedPromise.Result()
if err != nil {
return false, err
}
return clickedSecret == secret, nil
case reminderTimerFuture:
_, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return SendReminderEmail(userID, user, secret)
}, restate.WithName("send reminder"))
if err != nil {
return false, err
}
break // Break out of selector loop to continue main loop
case verificationTimeoutFuture:
return false, restate.TerminalError(fmt.Errorf("email verification timed out after 24 hours"))
}
}
}
func (SignupWithTimersWorkflow) VerifyEmail(ctx restate.WorkflowSharedContext, req VerifyEmailRequest) error {
return restate.Promise[string](ctx, "email-verified").Resolve(req.Secret)
}
```
```python signup_with_timers.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-workflows-python/app/workflows/signup_with_timers.py?collapse_prequel"} theme={null}
signup_with_timers = restate.Workflow("SignupWithTimersWorkflow")
@signup_with_timers.main()
async def run(ctx: WorkflowContext, user: User) -> bool:
user_id = ctx.key()
secret = str(ctx.uuid())
await ctx.run_typed(
"verify",
send_verification_email,
user_id=user_id,
user=user,
verification_secret=secret,
)
clicked_promise = ctx.promise("email-verified")
verification_timeout = ctx.sleep(timedelta(days=1))
while True:
reminder_timer = ctx.sleep(timedelta(seconds=15))
# Wait for email verification, reminder timer or timeout
result = await restate.select(
verification=clicked_promise.value(),
reminder=reminder_timer,
timeout=verification_timeout,
)
match result:
case ["verification", clicked_secret]:
return clicked_secret == secret
case ["reminder", _]:
await ctx.run_typed(
"remind",
send_reminder_email,
user_id=user_id,
user=user,
verification_secret=secret,
)
case ["timeout", _]:
raise TerminalError("Email verification timed out after 24 hours")
@signup_with_timers.handler("verifyEmail")
async def verify_email(ctx: WorkflowSharedContext, req: VerifyEmailRequest) -> None:
await ctx.promise("email-verified").resolve(req.secret)
```
Because Restate lets you write workflows as regular functions, you can use your language's native constructs like `while`/`for` loops, `if` statements, and `switch` cases to control flow.
This makes it easy to implement complex logic with timers, loops, and conditional execution.
Submit the workflow asynchronously with `/send`:
```bash theme={null}
curl localhost:8080/SignupWithTimersWorkflow/johndoe/run/send \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWithTimersWorkflow/johndoe/run/send \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWithTimersWorkflow/johndoe/Run/send \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWithTimersWorkflow/johndoe/run/send \
--json '{"name": "John Doe", "email": "john@mail.com"}'
```
See in the UI how the workflow is waiting for the email verification to be resolved, and sends reminder emails every 15 seconds:
Try killing the service and restarting it. The workflow will continue sending reminders as if it never stopped.
To resolve the promise, **copy over the curl request from the service logs**, which looks like this:
```bash theme={null}
curl localhost:8080/SignupWithTimersWorkflow/johndoe/verifyEmail \
--json '{"secret": "the-secret-from-email"}'
```
```bash theme={null}
curl localhost:8080/SignupWithTimersWorkflow/johndoe/verifyEmail \
--json '{"secret": "the-secret-from-email"}'
```
```bash theme={null}
curl localhost:8080/SignupWithTimersWorkflow/johndoe/verifyEmail \
--json '{"secret": "the-secret-from-email"}'
```
```bash theme={null}
curl localhost:8080/SignupWithTimersWorkflow/johndoe/verifyEmail \
--json '{"secret": "the-secret-from-email"}'
```
Now the UI will show the workflow completed successfully.
### Parallel Execution
The timers example ran three operations in parallel: two timers and awaiting a promise.
Restate supports different ways of waiting for parallel operations to complete and takes care of retries and recovery for you.
Have a look at the Concurrent Tasks docs for your SDK to learn more ([TS](/develop/ts/concurrent-tasks) / [Java / Kotlin](/develop/java/concurrent-tasks) / [Python](/develop/python/concurrent-tasks) / [Go](/develop/go/concurrent-tasks)).
## Error Handling
By default, Restate retries failures infinitely with an exponential backoff strategy.
For some failures, you might not want to retry or only retry a limited number of times.
For these cases, Restate distinguishes between two types of errors: transient errors and terminal errors.
### Transient vs Terminal Errors
* **Transient errors**: These are temporary issues that can be retried, such as network timeouts or service unavailability. Restate automatically retries these errors.
* **Terminal errors**: These indicate a failure that will not be retried, such as invalid input or business logic violations. Restate stops execution and allows you to handle these errors gracefully.
Throw a terminal error in your handler to indicate a terminal failure:
```typescript {"CODE_LOAD::ts/src/tour/workflows/terminal_error.ts#terminal_error"} theme={null}
throw new TerminalError("Subscription plan not available");
```
```java {"CODE_LOAD::java/src/main/java/tour/workflows/WorkflowErrorHandler.java#here"} theme={null}
throw new TerminalException("Subscription plan not available");
```
```go {"CODE_LOAD::go/tour/workflows/errorhandling.go#here"} theme={null}
return restate.TerminalError(fmt.Errorf("subscription plan not available"))
```
```python {"CODE_LOAD::python/src/tour/workflows/terminal_error.py#here"} theme={null}
from restate.exceptions import TerminalError
raise TerminalError("Invalid credit card")
```
### Configuring Retry Behavior
You can limit the number of retries of a run block:
```ts signup-with-retries.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-workflows-typescript/src/workflows/signup-with-retries.ts#retries"} theme={null}
try {
const retryPolicy = {
maxRetryAttempts: 3,
initialRetryInterval: { seconds: 1 },
};
await ctx.run("welcome", () => sendWelcomeEmail(user), retryPolicy);
} catch (error) {
// This gets hit on retry exhaustion with a terminal error
// Log and continue; without letting the workflow fail
console.error("Failed to send welcome email after retries:", error);
}
```
```java SignupWithRetriesWorkflow.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-workflows-java/src/main/java/my/example/workflows/SignupWithRetriesWorkflow.java#retries"} theme={null}
try {
RetryPolicy myRunRetryPolicy =
RetryPolicy.defaultPolicy()
.setInitialDelay(Duration.ofMillis(500))
.setExponentiationFactor(2)
.setMaxDelay(Duration.ofSeconds(10))
.setMaxAttempts(3)
.setMaxDuration(Duration.ofSeconds(30));
ctx.run("welcome", myRunRetryPolicy, () -> sendWelcomeEmail(user));
} catch (TerminalException error) {
// This gets hit on retry exhaustion with a terminal error
// Log and continue; without letting the workflow fail
System.err.println("Failed to send welcome email after retries: " + error.getMessage());
}
```
```go retries.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-workflows-go/examples/retries.go#retries"} theme={null}
_, err = restate.Run(ctx,
func(ctx restate.RunContext) (restate.Void, error) {
return SendWelcomeEmail(user)
},
restate.WithName("welcome"),
restate.WithMaxRetryAttempts(3),
restate.WithInitialRetryInterval(1000),
)
if err != nil {
// This gets hit on retry exhaustion with a terminal error
// Log and continue; without letting the workflow fail
fmt.Printf("Couldn't send the email due to terminal error %s", err)
}
```
```python signup_with_retries.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-workflows-python/app/workflows/signup_with_retries.py?collapse_prequel"} theme={null}
signup_with_retries = restate.Workflow("SignupWithRetriesWorkflow")
@signup_with_retries.main()
async def run(ctx: WorkflowContext, user: User) -> bool:
user_id = ctx.key()
success = await ctx.run_typed("create", create_user, user_id=user_id, user=user)
if not success:
return False
await ctx.run_typed("activate", activate_user, user_id=user_id)
# Configure retry policy
try:
await ctx.run_typed(
"welcome",
send_welcome_email,
restate.RunOptions(
max_attempts=3, max_retry_duration=timedelta(seconds=30)
),
user=user,
)
except TerminalError as error:
# This gets hit on retry exhaustion with a terminal error
# Log and continue; without letting the workflow fail
print(f"Failed to send welcome email after retries: {error}")
return True
```
When the retries are exhausted, the run block will throw a terminal error, that you can handle in your handler logic.
Submit the workflow for Alice:
```bash theme={null}
curl localhost:8080/SignupWithRetriesWorkflow/alice/run \
--json '{"name": "Alice", "email": "alice@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWithRetriesWorkflow/alice/run \
--json '{"name": "Alice", "email": "alice@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWithRetriesWorkflow/alice/Run \
--json '{"name": "Alice", "email": "alice@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWithRetriesWorkflow/alice/run \
--json '{"name": "Alice", "email": "alice@mail.com"}'
```
In the UI, you can see how the workflow is waiting for the `welcome` step to finish, and how it retries sending the welcome email up to 3 times across three seconds.
After three attempts, the workflow continues without failing:
Learn more with the [Error Handling Guide](/guides/error-handling).
## Sagas and rollback
On a terminal failure, Restate stops the execution of the handler.
You might, however, want to roll back the changes made by the workflow to keep your system in a consistent state.
This is where Sagas come in.
Sagas are a pattern for rolling back changes made by a workflow when it fails.
In Restate, you can implement a saga by building a list of compensating actions for each step of the workflow.
On a terminal failure, you execute them in reverse order:
```ts signup-with-sagas.ts {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/typescript/tutorials/tour-of-workflows-typescript/src/workflows/signup-with-sagas.ts?collapse_prequel"} theme={null}
export const signupWithSagas = restate.workflow({
name: "SignupWithSagasWorkflow",
handlers: {
run: async (ctx: WorkflowContext, user: User) => {
const userId = ctx.key;
const compensations = [];
try {
compensations.push(() => ctx.run("delete", () => deleteUser(userId)));
await ctx.run("create", () => createUser(userId, user));
compensations.push(() =>
ctx.run("deactivate", () => deactivateUser(userId)),
);
await ctx
.run("activate", () => activateUser(userId))
.orTimeout({ minutes: 5 });
compensations.push(() =>
ctx.run("unsubscribe", () => cancelSubscription(user)),
);
await ctx.run("subscribe", () => subscribeToPaidPlan(user));
} catch (e) {
if (e instanceof restate.TerminalError) {
for (const compensation of compensations.reverse()) {
await compensation();
}
}
return { success: false };
}
return { success: true };
},
},
});
```
```java SignupWithSagasWorkflow.java {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/java/tutorials/tour-of-workflows-java/src/main/java/my/example/workflows/SignupWithSagasWorkflow.java?collapse_prequel"} theme={null}
@Workflow
public class SignupWithSagasWorkflow {
@Workflow
public boolean run(WorkflowContext ctx, User user) {
String userId = ctx.key();
List compensations = new ArrayList<>();
try {
compensations.add(() -> ctx.run("delete", () -> deleteUser(userId)));
ctx.run("create", () -> createUser(userId, user));
compensations.add(() -> ctx.run("deactivate", () -> deactivateUser(userId)));
ctx.run("activate", () -> activateUser(userId));
compensations.add(() -> ctx.run("unsubscribe", () -> cancelSubscription(user)));
ctx.run("subscribe", () -> subscribeToPaidPlan(user));
} catch (TerminalException e) {
// Run compensations in reverse order
Collections.reverse(compensations);
for (Runnable compensation : compensations) {
compensation.run();
}
return false;
}
return true;
}
}
```
```go sagas.go {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/go/tutorials/tour-of-workflows-go/examples/sagas.go?collapse_prequel"} theme={null}
type SagasWorkflow struct{}
func (SagasWorkflow) Run(ctx restate.WorkflowContext, user User) (res bool, err error) {
userID := restate.Key(ctx)
var compensations []func() error
defer func() {
// All errors that end up here are terminal errors, so run compensations
// (Retry-able errors got returned by the SDK without ending up here)
if err != nil {
for _, compensation := range slices.Backward(compensations) {
if compErr := compensation(); compErr != nil {
err = compErr
}
}
}
}()
// Add compensation for user creation
compensations = append(compensations, func() error {
_, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return DeleteUser(userID)
})
return err
})
_, err = restate.Run(ctx, func(ctx restate.RunContext) (bool, error) {
return CreateUser(userID, user)
}, restate.WithName("create"))
if err != nil {
return false, err
}
// Add compensation for user activation
compensations = append(compensations, func() error {
_, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return DeactivateUser(userID)
})
return err
})
_, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return ActivateUser(userID)
})
if err != nil {
return false, err
}
// Add compensation for subscription
compensations = append(compensations, func() error {
_, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return CancelSubscription(user)
})
return err
})
_, err = restate.Run(ctx, func(ctx restate.RunContext) (bool, error) {
return SubscribeToPaidPlan(user)
})
if err != nil {
return false, err
}
return true, nil
}
```
```python signup_with_sagas.py {"CODE_LOAD::https://raw.githubusercontent.com/restatedev/examples/refs/heads/main/python/tutorials/tour-of-workflows-python/app/workflows/signup_with_sagas.py?collapse_prequel"} theme={null}
signup_with_sagas = restate.Workflow("SignupWithSagasWorkflow")
@signup_with_sagas.main()
async def run(ctx: WorkflowContext, user: User) -> bool:
user_id = ctx.key()
compensations = []
try:
compensations.append(
lambda: ctx.run_typed("delete", delete_user, user_id=user_id)
)
await ctx.run_typed("create", create_user, user_id=user_id, user=user)
compensations.append(
lambda: ctx.run_typed("deactivate", deactivate_user, user_id=user_id)
)
await ctx.run_typed("activate", activate_user, user_id=user_id)
compensations.append(
lambda: ctx.run_typed("unsubscribe", cancel_subscription, user=user)
)
await ctx.run_typed("subscribe", subscribe_to_paid_plan, user=user)
except TerminalError:
# Run compensations in reverse order
for compensation in reversed(compensations):
await compensation()
return False
return True
```
**Benefits with Restate:**
* The list of compensations can be recovered after a crash, and Restate knows which compensations still need to be run.
* Sagas always run till completion (success or complete rollback)
* Full trace of all operations and compensations
* No complex state machines needed
Submit the workflow for Alice:
```bash theme={null}
curl localhost:8080/SignupWithSagasWorkflow/alice/run \
--json '{"name": "Alice", "email": "alice@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWithSagasWorkflow/alice/run \
--json '{"name": "Alice", "email": "alice@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWithSagasWorkflow/alice/Run \
--json '{"name": "Alice", "email": "alice@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWithSagasWorkflow/alice/run \
--json '{"name": "Alice", "email": "alice@mail.com"}'
```
Alice is not able to get a subscription, so the workflow will fail and run compensations:
Learn more with the [Sagas Guide](/guides/sagas).
## Cancellation
You can cancel user signup workflows via HTTP, CLI, UI, or programmatically from other services.
When you cancel a workflow, Restate stops the execution by throwing a Terminal Error.
This allows your handler to run compensating actions or clean up resources.
First, the cancellation gets propagated to the leaf nodes of the call tree (in case the workflow called other services or workflows).
Then, the cancellation propagates back up the tree, allowing each handler to run its compensations.
Start the workflow asynchronously with `/send`:
```bash theme={null}
curl localhost:8080/SignupWithSignalsWorkflow/eve/run/send \
--json '{"name": "Eve", "email": "eve@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWithSignalsWorkflow/eve/run/send \
--json '{"name": "Eve", "email": "eve@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWithSignalsWorkflow/eve/Run/send \
--json '{"name": "Eve", "email": "eve@mail.com"}'
```
```bash theme={null}
curl localhost:8080/SignupWithSignalsWorkflow/eve/run/send \
--json '{"name": "Eve", "email": "eve@mail.com"}'
```
This returns the invocation ID, which you can use to cancel the workflow later via the UI, CLI or HTTP:
```bash CLI theme={null}
restate invocations cancel inv_1gdJBtdVEcM942bjcDmb1c1khoaJe11Hbz
```
```bash curl theme={null}
curl -X PATCH http://localhost:9070/invocations/inv_1gdJBtdVEcM942bjcDmb1c1khoaJe11Hbz/cancel
```
You can see the cancellation in the UI:
Check out the SDK docs, to learn how to programmatically cancel a workflow from another service ([TS](/develop/ts/service-communication#cancel-an-invocation) / [Java / Kotlin](/develop/java/service-communication#cancel-an-invocation) / [Python](/develop/python/service-communication#cancel-an-invocation)/ [Go](/develop/go/service-communication#cancel-an-invocation)).
## Serverless Deployment
Restate lets you run your workflows and services on serverless platforms like AWS Lambda or Google Cloud Run.
Restate automatically suspends workflows when they are waiting for events or timers, and resumes them when the event occurs or the timer expires.
This means you can run long-running workflows on function-as-a-service platforms without paying for the wait time.
Turning your signup workflow into a serverless function is as simple as adapting the endpoint:
```typescript {"CODE_LOAD::ts/src/tour/workflows/serving_lambda.ts#lambda"} theme={null}
import * as restate from "@restatedev/restate-sdk/lambda";
export const handler = restate.createEndpointHandler({
services: [signupWorkflow],
});
```
Learn more from the [serving docs](/develop/ts/serving).
```java {"CODE_LOAD::java/src/main/java/tour/workflows/WorkflowServingLambda.java#here"} theme={null}
import dev.restate.sdk.endpoint.Endpoint;
import dev.restate.sdk.lambda.BaseRestateLambdaHandler;
class MyLambdaHandler extends BaseRestateLambdaHandler {
@Override
public void register(Endpoint.Builder builder) {
builder.bind(new SignupWorkflow());
}
}
```
Learn more from the [serving docs](/develop/java/serving).
```go {"CODE_LOAD::go/tour/workflows/servinglambda.go#here"} theme={null}
handler, err := server.NewRestate().
Bind(restate.Reflect(SignupWorkflow{})).
Bidirectional(false).
LambdaHandler()
if err != nil {
log.Fatal(err)
}
lambda.Start(handler)
```
Learn more from the [serving docs](/develop/go/serving).
```python {"CODE_LOAD::python/src/tour/workflows/lambda_handler.py#here"} theme={null}
handler = restate.app(services=[signup_workflow])
```
Learn more from the [serving docs](/develop/python/serving).
## Summary
Restate workflows provide:
* **Natural Programming**: Write workflows as regular functions in your preferred language
* **Automatic Durability**: Built-in resilience without infrastructure complexity
* **Flexible Patterns**: State management, events, timers, and parallel execution
* **Modern Deployment**: Strong serverless support and simple single-binary deployment
With Restate, you can build complex, long-running workflows using familiar programming patterns while getting the durability you need.
# AI Agents
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/use-cases/ai-agents
Build resilient, observable AI agents that recover from failures and handle complex multi-step tasks.
## Durable Agents and Workflows
Restate automatically handles the reliability challenges of AI agents:
* **Automatically retry transient errors** like rate limits and network failures
* **Persist steps** (LLM calls, tools) and recover previous progress after failures
* **Suspend long-running agents** when idle to save costs
## Plugs into Popular SDKs
Restate works independently of any SDK and specific AI stack, but its lightweight programming abstraction integrates easily into many popular SDKs. A few lines turn your agent into a durable agent.
```typescript {"CODE_LOAD::ts/src/usecases/agents/weather-agent.ts#here"} theme={null}
const model = wrapLanguageModel({
model: openai("gpt-4o"),
middleware: durableCalls(restateContext, { maxRetryAttempts: 3 }),
});
```
Works with Vercel AI SDK, OpenAI, and others.
## Human-in-the-Loop and Workflow Patterns
Restate's workflows-as-code and building blocks make it easy to reliably implement:
Durable waiting for human decisions with crash-proof timeouts
Speed up multi-step workflows with recoverable parallel tasks
Break complex agents into smaller, specialized workflows
Coordinate specialized agents with reliable communication
Automatically undo previous actions when later steps fail
Build agents that can be paused, modified, and resumed during execution
## Observability and Debugging
See all ongoing executions with detailed journals of agent steps:
* **Complete execution timeline**: Every LLM call and tool execution
* **Debug failed agents**: Inspect exactly where and why agents failed
* **Agent control**: Pause, resume, restart agents during development and production
## End-to-End Resilient Applications
Agents are just a part of your application. Restate covers the plumbing around your agents:
* **Queuing, state, session management**: Built-in primitives for reliable agent coordination
* **Deterministic workflows**: Complement agents with structured business logic
* **Reliable asynchronous tasks**: Handle background work and inter-service communication
## Flexible Deployments and Scalability
Restate's durable execution runtime lets you run your durable code where you want at the scale you want:
* **Scale to millions** of concurrent agent executions
* **Deploy your agents** on FaaS or containers
* **You own the infrastructure**: Run on Restate Cloud or self-host
## Getting Started
Set up Restate and run your first agent
Build durable agents, chatbots, and multi-agent systems
Explore templates, examples, and SDK integrations
Questions? Join our community on [Discord](https://discord.gg/skW3AZ6uGd) or [Slack](https://join.slack.com/t/restatecommunity/shared_invite/zt-2v9gl005c-WBpr167o5XJZI1l7HWKImA).
# Event Processing
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/use-cases/event-processing
Build lightweight, transactional event handlers with built-in resiliency.
Kafka event processing requires managing Kafka consumers, handling retries, maintaining state stores, and coordinating complex workflows. Restate eliminates this complexity by providing **lightweight, transactional event processing** with zero consumer management and built-in state.
## Workflows from Kafka
Build event handlers with complex control flow, loops, timers, and transactional guarantees:
```typescript TypeScript {"CODE_LOAD::ts/src/usecases/eventprocessing/user-feed.ts#here"} theme={null}
export default restate.object({
name: "userFeed",
handlers: {
processPost: async (ctx: restate.ObjectContext, post: SocialMediaPost) => {
const userId = ctx.key;
// Durable side effect: persisted and replayed on retries
const postId = await ctx.run(() => createPost(userId, post));
// Wait for processing to complete with durable timers
while ((await ctx.run(() => getPostStatus(postId))) === PENDING) {
await ctx.sleep({ seconds: 5 });
}
await ctx.run(() => updateUserFeed(userId, postId));
},
},
});
```
```java Java {"CODE_LOAD::java/src/main/java/usecases/eventprocessing/eventtransactions/UserFeed.java#here"} theme={null}
@VirtualObject
public class UserFeed {
@Handler
public void processPost(ObjectContext ctx, SocialMediaPost post) {
String userId = ctx.key();
String postId = ctx.run(String.class, () -> createPost(userId, post));
while (ctx.run(String.class, () -> getPostStatus(postId)).equals("PENDING")) {
ctx.sleep(Duration.ofSeconds(5));
}
ctx.run(() -> updateUserFeed(userId, postId));
}
}
```
```python Python {"CODE_LOAD::python/src/usecases/eventprocessing/user_feed.py#here"} theme={null}
user_feed = restate.VirtualObject("UserFeed")
@user_feed.handler()
async def process_post(ctx: restate.ObjectContext, post: SocialMediaPost):
user_id = ctx.key()
post_id = await ctx.run_typed(
"create post", create_post, user_id=user_id, post=post
)
while (
await ctx.run_typed("get status", get_post_status, post_id=post_id)
== Status.PENDING
):
await ctx.sleep(timedelta(seconds=5))
await ctx.run_typed("update feed", update_user_feed, user=user_id, post_id=post_id)
```
```go Go {"CODE_LOAD::go/usecases/eventprocessing/userfeed.go#here"} theme={null}
type UserFeed struct{}
func (UserFeed) ProcessPost(ctx restate.ObjectContext, post SocialMediaPost) error {
var userId = restate.Key(ctx)
postId, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
return CreatePost(userId, post)
})
if err != nil {
return err
}
for {
status, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {
return GetPostStatus(postId), nil
})
if err != nil {
return err
}
if status != PENDING {
break
}
if err = restate.Sleep(ctx, 5*time.Second); err != nil {
//
return err
}
}
if _, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return UpdateUserFeed(userId, postId)
}); err != nil {
return err
}
return nil
}
```
**Key Benefits:**
* **Push-based delivery**: Events delivered directly to handlers with zero consumer management
* **Durable execution**: Failed handlers are retried with exponential backoff until they succeed. Handlers replay previously completed steps and resume exactly where they left off.
* **Parallel processing**: Events for different keys process concurrently, like a queue per object key
* **Timers and scheduling**: Timers and delays that survive crashes and restarts for **long-running workflows**
## Stateful Event Handlers
Maintain K/V state across events:
```typescript TypeScript {"CODE_LOAD::ts/src/usecases/eventprocessing/delivery-tracker.ts#here"} theme={null}
export default restate.object({
name: "delivery-tracker",
handlers: {
register: async (ctx: restate.ObjectContext, delivery: Delivery) =>
ctx.set("delivery", delivery),
setLocation: async (ctx: restate.ObjectContext, location: Location) => {
const delivery = await ctx.get("delivery");
if (!delivery) {
throw new TerminalError(`Delivery not found`);
}
delivery.locations.push(location);
ctx.set("delivery", delivery);
},
getDelivery: shared(async (ctx: restate.ObjectSharedContext) =>
ctx.get("delivery")
),
},
});
```
```java Java {"CODE_LOAD::java/src/main/java/usecases/eventprocessing/eventenrichment/DeliveryTracker.java#here"} theme={null}
@VirtualObject
public class DeliveryTracker {
private static final StateKey DELIVERY = StateKey.of("delivery", Delivery.class);
@Handler
public void register(ObjectContext ctx, Delivery packageInfo) {
ctx.set(DELIVERY, packageInfo);
}
@Handler
public void setLocation(ObjectContext ctx, Location location) {
var delivery = ctx.get(DELIVERY).orElseThrow(() -> new TerminalException("Delivery not found"));
delivery.addLocation(location);
ctx.set(DELIVERY, delivery);
}
@Shared
public Delivery getDelivery(SharedObjectContext ctx) {
return ctx.get(DELIVERY).orElseThrow(() -> new TerminalException("Delivery not found"));
}
}
```
```python Python {"CODE_LOAD::python/src/usecases/eventprocessing/delivery_tracker.py#here"} theme={null}
delivery_tracker = restate.VirtualObject("DeliveryTracker")
@delivery_tracker.handler()
async def register(ctx: restate.ObjectContext, delivery: Delivery):
ctx.set("delivery", delivery)
@delivery_tracker.handler()
async def set_location(ctx: restate.ObjectContext, location: Location):
delivery = await ctx.get("delivery", type_hint=Delivery)
if delivery is None:
raise TerminalError(f"Delivery {ctx.key()} not found")
delivery.locations.append(location)
ctx.set("delivery", delivery)
@delivery_tracker.handler(kind="shared")
async def get_delivery(ctx: restate.ObjectSharedContext) -> Delivery:
delivery = await ctx.get("delivery", type_hint=Delivery)
if delivery is None:
raise TerminalError(f"Delivery {ctx.key()} not found")
return delivery
```
```go Go {"CODE_LOAD::go/usecases/eventprocessing/packagetracker.go#here"} theme={null}
type DeliveryTracker struct{}
func (DeliveryTracker) Register(ctx restate.ObjectContext, delivery Delivery) error {
restate.Set[Delivery](ctx, "delivery", delivery)
return nil
}
func (DeliveryTracker) SetLocation(ctx restate.ObjectContext, location Location) error {
packageInfo, err := restate.Get[*Delivery](ctx, "delivery")
if err != nil {
return err
}
if packageInfo == nil {
return restate.TerminalError(errors.New("delivery not found"))
}
packageInfo.Locations = append(packageInfo.Locations, location)
restate.Set[Delivery](ctx, "delivery", *packageInfo)
return nil
}
func (DeliveryTracker) GetDelivery(ctx restate.ObjectSharedContext) (*Delivery, error) {
return restate.Get[*Delivery](ctx, "delivery")
}
```
**Key Benefits**:
* **Persistent state**: Store and retrieve state directly in handlers without external stores
* **Built-in consistency**: State operations are always consistent with execution
* **Agents, actors, digital twins**: Model stateful entities that react to events
## When to Choose Restate
**✅ Choose Restate when you need:**
* **Kafka integration**: Process Kafka events with zero consumer management
* **Reliable processing**: Automatic retry and recovery for failed event handlers
* **Transactional processing**: Execute side effects with durable execution guarantees
* **Stateful event processing**: Maintain state across events without external stores
* **Event-driven workflows**: Build complex flows with loops, timers, and conditions
Processing events with Restate? Join our community on [Discord](https://discord.com/invite/skW3AZ6uGd) or [Slack](https://join.slack.com/t/restatecommunity/shared_invite/zt-2v9gl005c-WBpr167o5XJZI1l7HWKImA) to discuss your use case.
## Comparison with Other Solutions
| Feature | Restate | Traditional Kafka Processing | Stream Processing Frameworks |
| ----------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------ |
| **Event Delivery** | Push-based to durable handlers | Polling-based consumer groups | Built-in sources and sinks |
| **Consumer Management** | Zero configuration | Manual offset and consumer group management | Zero configuration |
| **Failure Recovery** | Fine-grained progress persistence with durable side effects | Coarse-grained offset commits with potential reprocessing | Coarse-grained checkpointing with potential duplicate side effects |
| **State Management** | Built-in durable state | External state store required | Built-in state store |
| **Queuing Semantics** | Queue-per-key with ordering guarantees | Queue-per-partition with ordering guarantees | Queue-per-partition (inherited from Kafka) |
| **Complex Workflows** | Unlimited control flow: loops, timers, conditions | Long-running logic blocks consumer loop; requires external services | DAG-based processing with limited control flow |
| **Best For** | Event-driven state machines, transactional processing, complex workflows | Simple ETL and message processing | High-throughput analytics, aggregations, joins |
## Getting Started
Ready to build event processing systems with Restate? Here are your next steps:
Set up Restate and process your first events
Follow the quickstart to implement your first durable event handler
Explore templates, patterns, and end-to-end applications
Evaluating Restate and missing a feature? Contact us on [Discord](https://discord.com/invite/skW3AZ6uGd) or [Slack](https://join.slack.com/t/restatecommunity/shared_invite/zt-2v9gl005c-WBpr167o5XJZI1l7HWKImA).
# Microservice Orchestration
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/use-cases/microservice-orchestration
Build resilient, distributed microservices with durable execution, sagas, and reliable service communication.
Restate provides **durable execution primitives** that make distributed systems resilient by default, without the operational overhead.
## Resilient Orchestration
Build microservices that automatically recover from failures without losing progress:
```typescript TypeScript {"CODE_LOAD::ts/src/usecases/microservices/order-service.ts#here"} theme={null}
export const orderService = restate.service({
name: "OrderService",
handlers: {
process: async (ctx: restate.Context, order: Order) => {
// Each step is automatically durable and resumable
const paymentId = ctx.rand.uuidv4();
await ctx.run(() => chargePayment(order.creditCard, paymentId));
for (const item of order.items) {
await ctx.run(() => reserveInventory(item.id, item.quantity));
}
return { success: true, paymentId };
},
},
});
```
```java Java {"CODE_LOAD::java/src/main/java/usecases/microservices/OrderService.java#here"} theme={null}
@Service
public class OrderService {
@Handler
public OrderResult process(Context ctx, Order order) {
// Each step is automatically durable and resumable
String paymentId = UUID.randomUUID().toString();
ctx.run(() -> chargePayment(order.creditCard, paymentId));
for (var item : order.items) {
ctx.run(() -> reserveInventory(item.id, item.quantity));
}
return new OrderResult(true, paymentId);
}
}
```
```python Python {"CODE_LOAD::python/src/usecases/microservices/order_service.py#here"} theme={null}
order_service = restate.Service("OrderService")
@order_service.handler()
async def process(ctx: restate.Context, order: Order):
# Each step is automatically durable and resumable
payment_id = str(uuid.uuid4())
await ctx.run_typed(
"charge", charge_payment, credit_card=order.credit_card, payment_id=payment_id
)
for item in order.items:
await ctx.run_typed(
f"reserve_{item.id}", reserve, item_id=item.id, amount=item.amount
)
return {"success": True, "payment_id": payment_id}
```
```go Go {"CODE_LOAD::go/usecases/microservices/orderservice.go#here"} theme={null}
type OrderService struct{}
func (OrderService) Process(ctx restate.Context, order Order) (OrderResult, error) {
// Each step is automatically durable and resumable
paymentID := restate.Rand(ctx).UUID().String()
_, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return ChargePayment(order.CreditCard, paymentID)
})
if err != nil {
return OrderResult{}, err
}
for _, item := range order.Items {
_, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return ReserveInventory(item.ID, item.Quantity)
})
if err != nil {
return OrderResult{}, err
}
}
return OrderResult{Success: true, PaymentID: paymentID}, nil
}
```
* **Automatic recovery**: Code resumes exactly where it left off after failures
* **Standard development**: Write services like regular HTTP APIs
* **[Resilient Sagas](/guides/sagas)**: Implement complex multi-step transactions with resilient rollback
## Reliable Communication & Idempotency
Flexible communication patterns with strong delivery guarantees:
* **Zero message loss**: All service communication is durably logged
* **Built-in retries**: Automatic exponential backoff for transient failures
* **Scheduling**: Delay messages for future processing
* **Request deduplication**: Idempotency keys prevent duplicate processing
```typescript TypeScript {"CODE_LOAD::ts/src/usecases/microservices/service-actions.ts#communication"} theme={null}
// Request-response: Wait for result
const payRef = await ctx.serviceClient(paymentService).charge(req);
// Fire-and-forget: Guaranteed delivery without waiting
ctx.serviceSendClient(emailService).emailTicket(req);
// Delayed execution: Schedule for later
ctx
.serviceSendClient(emailService)
.sendReminder(order, sendOpts({ delay: dayBefore(req.concertDate) }));
```
```java Java {"CODE_LOAD::java/src/main/java/usecases/microservices/ServiceActions.java#communication"} theme={null}
// Request-response: Wait for result
var result = InventoryServiceClient.fromContext(ctx).checkStock(item);
// Fire-and-forget: Guaranteed delivery without waiting
EmailServiceClient.fromContext(ctx).send().emailTicket(order);
// Delayed execution: Schedule for later
EmailServiceClient.fromContext(ctx).send().sendReminder(order, Duration.ofDays(21));
```
```python Python {"CODE_LOAD::python/src/usecases/microservices/service_actions.py#communication"} theme={null}
# Request-response: Wait for result
result = await ctx.service_call(charge_payment, req)
# Fire-and-forget: Guaranteed delivery without waiting
ctx.service_send(send_ticket_email, ticket)
# Delayed execution: Schedule for later
ctx.service_send(send_reminder, ticket, send_delay=timedelta(days=7))
```
```go Go {"CODE_LOAD::go/usecases/microservices/serviceactions.go#communication"} theme={null}
// Request-response: Wait for result
result, err := restate.Service[StockResult](ctx, "PaymentService", "Charge").Request(req)
if err != nil {
return err
}
_ = result
// Fire-and-forget: Guaranteed delivery without waiting
restate.ServiceSend(ctx, "EmailService", "EmailTicket").Send(ticket)
// Delayed execution: Schedule for later
restate.ServiceSend(ctx, "EmailService", "SendReminder").Send(ticket, restate.WithDelay(7*24*time.Hour))
```
## Durable Stateful Entities
Manage stateful entities without external databases or complex consistency mechanisms:
* **Durable persistence**: Application state survives crashes and deployments
* **Simple concurrency model**: Single-writer semantics prevent consistency issues and race conditions
* **Horizontal scaling**: Each object has its own message queue. Different entity keys process independently
* **Built-in querying**: Access state via UI and APIs
```typescript TypeScript {"CODE_LOAD::ts/src/usecases/microservices/user-account.ts#here"} theme={null}
export default restate.object({
name: "UserAccount",
handlers: {
updateBalance: async (ctx: restate.ObjectContext, amount: number) => {
const balance = (await ctx.get("balance")) ?? 0;
const newBalance = balance + amount;
if (newBalance < 0) {
throw new TerminalError("Insufficient funds");
}
ctx.set("balance", newBalance);
return newBalance;
},
getBalance: shared(async (ctx: restate.ObjectSharedContext) => {
return (await ctx.get("balance")) ?? 0;
}),
},
});
```
```java Java {"CODE_LOAD::java/src/main/java/usecases/microservices/UserAccount.java#here"} theme={null}
@VirtualObject
public class UserAccount {
private static final StateKey BALANCE = StateKey.of("balance", Double.class);
@Handler
public double updateBalance(ObjectContext ctx, double amount) {
double balance = ctx.get(BALANCE).orElse(0.0);
double newBalance = balance + amount;
if (newBalance < 0) {
throw new TerminalException("Insufficient funds");
}
ctx.set(BALANCE, newBalance);
return newBalance;
}
@Shared
public double getBalance(SharedObjectContext ctx) {
return ctx.get(BALANCE).orElse(0.0);
}
}
```
```python Python {"CODE_LOAD::python/src/usecases/microservices/user_account.py#here"} theme={null}
user_account = restate.VirtualObject("UserAccount")
@user_account.handler()
async def update_balance(ctx: restate.ObjectContext, amount: float):
balance = await ctx.get("balance") or 0.0
new_balance = balance + amount
if new_balance < 0.0:
raise TerminalError("Insufficient funds")
ctx.set("balance", new_balance)
return new_balance
@user_account.handler(kind="shared")
async def get_balance(ctx: restate.ObjectSharedContext):
return await ctx.get("balance") or 0.0
```
```go Go {"CODE_LOAD::go/usecases/microservices/useraccount.go#here"} theme={null}
type UserAccount struct{}
func (UserAccount) UpdateBalance(ctx restate.ObjectContext, amount float64) (float64, error) {
balance, err := restate.Get[float64](ctx, "balance")
if err != nil {
return 0.0, err
}
newBalance := balance + amount
if newBalance < 0.0 {
return 0.0, restate.TerminalError(errors.New("insufficient funds"))
}
restate.Set(ctx, "balance", newBalance)
return newBalance, nil
}
func (UserAccount) GetBalance(ctx restate.ObjectSharedContext) (float64, error) {
return restate.Get[float64](ctx, "balance")
}
```
## Operational simplicity
Reduce infrastructure complexity (no need for queues + state stores + schedulers, etc.). A single binary including everything you need.
## Key Orchestration Patterns
Implement resilient rollback logic for non-transient failures
Use Durable Execution to make database operations resilient and consistent
Execute independent operations concurrently while maintaining durability
Call other services with guaranteed delivery, retries, and deduplication
Wait for external events and webhooks with promises that survive crashes
Implement consistent state machines that survive crashes and restarts
## Comparison with Other Solutions
| Feature | Restate | Traditional Orchestration |
| -------------------------- | ------------------------------- | ------------------------------------------------- |
| **Infrastructure** | Single binary deployment | Message brokers + workflow engines + state stores |
| **Service Communication** | Built-in reliable messaging | External message queues required |
| **State Management** | Integrated durable state | External state stores + locks |
| **Failure Recovery** | Automatic progress recovery | Manual checkpoint/restart logic |
| **Deployment Model** | Standard HTTP services | Standard HTTP services |
| **Development Experience** | Regular code + IDE support | Regular code + IDE support |
| **Observability** | Built-in UI & execution tracing | Manual setup |
## Getting Started
Ready to build resilient microservices with Restate? Here are your next steps:
Run your first Restate service
Learn orchestration patterns with interactive examples
Explore templates, patterns, and end-to-end applications
Evaluating Restate and missing a feature? Contact us on [Discord](https://discord.com/invite/skW3AZ6uGd) or [Slack](https://join.slack.com/t/restatecommunity/shared_invite/zt-2v9gl005c-WBpr167o5XJZI1l7HWKImA).
# Workflows
Source: https://restate-6d46e1dc-update-go-sdk.mintlify.app/use-cases/workflows
Build resilient, low-latency workflows with code.
Restate lets you **write workflows as regular code** in your preferred programming language, with automatic resilience.
## Workflows as code
Write resilient workflows using familiar programming constructs:
* **Automatically retry transient errors** like infrastructure crashes and network failures
* Use **standard language constructs** (if/else, loops) and durable versions of familiar building blocks (e.g., timers, promises)
* **Handle errors naturally** with try/catch blocks and automatic retries
* **Test and debug** with your existing IDE and standard development tools
```typescript TypeScript {"CODE_LOAD::ts/src/usecases/workflows/simple-signup.ts#here"} theme={null}
export const userSignup = restate.workflow({
name: "user-signup",
handlers: {
run: async (ctx: WorkflowContext, user: User) => {
const userId = ctx.key; // unique workflow key
// Use regular if/else, loops, and functions
const success = await ctx.run("create", () => createUser(userId, user));
if (!success) return { success };
// Execute durable steps
await ctx.run("activate", () => activateUser(userId));
await ctx.run("welcome", () => sendWelcomeEmail(user));
return { success: true };
},
},
});
```
```java Java {"CODE_LOAD::java/src/main/java/usecases/workflows/UserSignup.java#here"} theme={null}
@Workflow
public class UserSignup {
@Workflow
public boolean run(WorkflowContext ctx, User user) {
String userId = ctx.key(); // unique workflow key
// Use regular if/else, loops, and functions
boolean success = ctx.run("create", Boolean.class, () -> createUser(userId, user));
if (!success) {
return false;
}
// Execute durable steps
ctx.run("activate", () -> activateUser(userId));
ctx.run("welcome", () -> sendWelcomeEmail(user));
return true;
}
}
```
```python Python {"CODE_LOAD::python/src/usecases/workflows/signup.py#here"} theme={null}
user_signup = restate.Workflow("user-signup")
@user_signup.main()
async def run(ctx: restate.WorkflowContext, user: User) -> Dict[str, bool]:
# Unique workflow key
user_id = ctx.key()
# Use regular if/else, loops, and functions
success = await ctx.run_typed("create", create_user, user_id=user_id, user=user)
if not success:
return {"success": False}
# Execute durable steps
await ctx.run_typed("activate", activate_user, user_id=user_id)
await ctx.run_typed("welcome", send_welcome_email, user=user)
return {"success": True}
```
```go Go {"CODE_LOAD::go/usecases/workflows/signup.go#here"} theme={null}
type UserSignup struct{}
func (UserSignup) Run(ctx restate.WorkflowContext, user User) (bool, error) {
// unique workflow key
userID := restate.Key(ctx)
// Use regular if/else, loops, and functions
success, err := restate.Run(ctx, func(ctx restate.RunContext) (bool, error) {
return CreateUser(userID, user)
})
if err != nil || !success {
return false, err
}
// Execute durable steps
_, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return ActivateUser(userID)
})
if err != nil {
return false, err
}
_, err = restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {
return SendWelcomeEmail(user)
})
if err != nil {
return false, err
}
return true, nil
}
```
## Low-Latency Workflows
Restate is built from the ground up for low-latency workflow execution. Restate workflows can be placed directly in the latency-sensitive path of user interactions:
* **Lightweight execution**: Workflows run like regular functions with minimal overhead
* **Event-driven foundation**: Built in Rust for high-performance, low-latency operations
* **No coordination delays**: Immediate workflow execution via a push-based model
[Benchmark results Restate v1.2](https://restate.dev/blog/building-a-modern-durable-execution-engine-from-first-principles/#some-performance-numbers)
## Simple Deployment Model
**Restate Server**: Restate is packaged as a single binary with built-in persistence and messaging. Run it as a single instance or in a high-availability cluster.
**Service Deployment**: Deploy your workflows using your existing deployment pipeline: containers, Kubernetes, serverless functions, or any HTTP-capable platform.
On FaaS, Restate suspends workflows while they are waiting (e.g. timer) to reduce costs.
## Key Workflow Patterns
Store workflow state that survives crashes and can be queried from external systems
Handle external events and signals without complex event sourcing infrastructure
Long-running processes with built-in timer management and timeout handling
Execute steps inline within the workflow or split them out into separate services
Speed up multi-step workflows with recoverable parallel tasks
Automatically undo previous actions when later steps fail
## Comparison with Other Solutions
| Feature | Restate | Traditional Orchestrators |
| ---------------------- | ------------------------------------------------ | ----------------------------------- |
| **Performance** | Low-latency, lightweight execution | High overhead, poll-for-work delays |
| **Language** | Native code (TS, Python, Go, Java, Kotlin, Rust) | DSLs or YAML |
| **Development** | Standard IDE, testing, debugging | Platform-specific tooling |
| **Infrastructure** | Single binary, no dependencies | Separate databases and queues |
| **Service Deployment** | Any platform (containers, serverless, K8s) | Worker-based deployment models |
| **State Management** | Built-in K/V state store | External state stores required |
## Getting Started
Ready to build workflows with Restate? Here are your next steps:
Run your first Restate service
Explore the APIs to build workflows with Restate
Explore templates, patterns, and end-to-end applications
Evaluating Restate and missing a feature? Contact us on [Discord](https://discord.com/invite/skW3AZ6uGd) or [Slack](https://join.slack.com/t/restatecommunity/shared_invite/zt-2v9gl005c-WBpr167o5XJZI1l7HWKImA).