Async tasks
Flexible, durable scheduling across processes and time.
Write resilient scheduling logic with the flexibility of code: delay execution, re-attach to ongoing tasks, fan out/in, etc.
Restate runs your tasks to completion exactly once.
Durable timers
Message queue
Switch between async and sync
Async tasks with Restate
Schedule tasks for now or later with the Restate SDK.
- TypeScript
- Java
- Kotlin
Execute any handler async
Every handler in Restate is executed asynchronously and can be treated as a reliable asynchronous task. No matter whether it is a simple function, or a complex workflow. Restate persists the requests to this handler and makes sure they run to completion. Restate handles retries and recovery upon failures.
Schedule tasks reliably
Schedule tasks asynchronously, by using Restate as message queue. Restate reliably queues them, also under backpressure/load.
Handlers can be called asynchronously from anywhere. This returns a task handle once the call in enqueued.
Idempotent task submission
Use an idempotency key to ensure that the task is only scheduled once. Restate will deduplicate the request and return the previous response.
Latch on to the task
For requests with an idempotency key, you can use this task handle to latch on to the task later and retrieve the result, or wait for it to finish.
This works across processes, so you can have a separate process latch on to the task later.
const asyncTaskService = restate.service({name: "taskWorker",handlers: {runTask: async (ctx: Context, params: TaskOpts) => {return someHeavyWork(params);},},});export type AsyncTaskService = typeof asyncTaskService;const endpoint = restate.endpoint().bind(asyncTaskService).listen(9080);
Execute any handler async
Every handler in Restate is executed asynchronously and can be treated as a reliable asynchronous task. No matter whether it is a simple function, or a complex workflow. Restate persists the requests to this handler and makes sure they run to completion. Restate handles retries and recovery upon failures.
Schedule tasks reliably
Schedule tasks asynchronously, by using Restate as message queue. Restate reliably queues them, also under backpressure/load.
Handlers can be called asynchronously from anywhere. This returns a task handle once the call in enqueued.
Idempotent task submission
Use an idempotency key to ensure that the task is only scheduled once. Restate will deduplicate the request and return the previous response.
Latch on to the task
For requests with an idempotency key, you can use this task handle to latch on to the task later and retrieve the result, or wait for it to finish.
This works across processes, so you can have a separate process latch on to the task later.
@Servicepublic class AsyncTaskService {@Handlerpublic String runTask(Context ctx, TaskOpts params) {return someHeavyWork(params);}}
Execute any handler async
Every handler in Restate is executed asynchronously and can be treated as a reliable asynchronous task. No matter whether it is a simple function, or a complex workflow. Restate persists the requests to this handler and makes sure they run to completion. Restate handles retries and recovery upon failures.
Schedule tasks reliably
Schedule tasks asynchronously, by using Restate as message queue. Restate reliably queues them, also under backpressure/load.
Handlers can be called asynchronously from anywhere. This returns a task handle once the call in enqueued.
Idempotent task submission
Use an idempotency key to ensure that the task is only scheduled once. Restate will deduplicate the request and return the previous response.
Latch on to the task
For requests with an idempotency key, you can use this task handle to latch on to the task later and retrieve the result, or wait for it to finish.
This works across processes, so you can have a separate process latch on to the task later.
@Serviceclass AsyncTaskService {@Handlersuspend fun runTask(ctx: Context, params: TaskOpts): String {return someHeavyWork(params)}}
LOW-LATENCY
Restate’s event-driven foundation built in Rust lets you queue events. Restate pushes them to your functions at high speed.
DURABLE EXECUTION
Restate makes sure all tasks run to completion. It keeps track of timers, handles retries and recovery upon failures, and ensures that tasks are executed exactly once.
Parallelizing work with Restate
Write flexible scheduling logic via durable building blocks.
- TypeScript
- Java
- Kotlin
Restate makes it easy to parallelize async work by fanning out tasks. Afterwards, you can collect the result by fanning in the partial results. Durable Execution ensures that the fan-out and fan-in steps happen reliably exactly once.
Fan out
Fan out tasks by calling the subtask handler for each subtask. Every handler is an asynchronous task, for which Restate serves as the queue.
The subtasks might run in different processes, if this is deployed in a parallel setup.
Fan in
Invocations produce durable promises that can be awaited and combined. Fan in by simply awaiting the combined promise. Invocation promises recover from failures, re-connect to running subtasks.
Server(less)
Deploy this service on an platform like Kubernetes or AWS Lambda to automatically get parallel scale out.
const workerService = restate.service({name: "worker",handlers: {run: async (ctx: Context, task: Task) => {// Split the task in subtasksconst subtasks: SubTask[] = await ctx.run("split task", () =>split(task));const resultPromises = [];for (const subtask of subtasks) {const subResultPromise = ctx.serviceClient(workerService).runSubtask(subtask);resultPromises.push(subResultPromise);}const results = await CombineablePromise.all(resultPromises);return aggregate(results);},runSubtask: async (ctx: Context, subtask: SubTask) => {// Processing logic goes here ...// Can be moved to a separate service to scale independently},},});export const handler = restate.endpoint().bind(workerService).handler();
Fan out
Fan out tasks by calling the subtask handler for each subtask. Every handler is an asynchronous task, for which Restate serves as the queue.
The subtasks might run in different processes, if this is deployed in a parallel setup.
Fan in
Invocations produce durable promises that can be awaited and combined. Fan in by simply awaiting the combined promise. Invocation promises recover from failures, re-connect to running subtasks.
Server(less)
Deploy this service on an platform like Kubernetes or AWS Lambda to automatically get parallel scale out.
@Servicepublic class FanOutWorker {@Handlerpublic Result run(Context ctx, Task task) {// Split the task in subtasksSubTask[] subTasks = ctx.run(JacksonSerdes.of(new TypeReference<>() {}), () -> split(task));List<Awaitable<?>> resultFutures = new ArrayList<>();for (SubTask subTask : subTasks) {Awaitable<SubTaskResult> subResultFuture =FanOutWorkerClient.fromContext(ctx).runSubtask(subTask);resultFutures.add(subResultFuture);}Awaitable.all(resultFutures).await();SubTaskResult[] results =(SubTaskResult[]) resultFutures.stream().map(Awaitable::await).toArray();return aggregate(results);}@Handlerpublic SubTaskResult runSubtask(Context ctx, SubTask subTask) {// Processing logic goes here ...// Can be moved to a separate service to scale independentlyreturn new SubTaskResult();}}
Restate makes it easy to parallelize async work by fanning out tasks. Afterwards, you can collect the result by fanning in the partial results. Durable Execution ensures that the fan-out and fan-in steps happen reliably exactly once.
Fan out
Fan out tasks by calling the subtask handler for each subtask. Every handler is an asynchronous task, for which Restate serves as the queue.
The subtasks might run in different processes, if this is deployed in a parallel setup.
Fan in
Invocations produce durable promises that can be awaited and combined. Fan in by simply awaiting the combined promise. Invocation promises recover from failures, re-connect to running subtasks.
Server(less)
Deploy this service on an platform like Kubernetes or AWS Lambda to automatically get parallel scale out.
@Serviceclass FanOutWorker {@Handlersuspend fun run(ctx: Context, task: Task): Result {val subTasks = ctx.runBlock { split(task) }val resultFutures: MutableList<Awaitable<SubTaskResult>> = mutableListOf()for (subTask in subTasks) {val subResultFuture = FanOutWorkerClient.fromContext(ctx).runSubtask(subTask)resultFutures.add(subResultFuture)}val results = resultFutures.awaitAll()return aggregate(results)}@Handlersuspend fun runSubtask(ctx: Context?, subTask: SubTask?): SubTaskResult {// Processing logic goes here ...// Can be moved to a separate service to scale independentlyreturn SubTaskResult()}}
Restate as sophisticated task queue
Restate is built as an event-driven foundation, and therefore supports task queues by design.
Async tasks run like any other function in your infrastructure: on K8S, FaaS, or mix-and-match.
No need to spin up extra infrastructure or message queues.
Switch between async and sync with Restate
- TypeScript
- Java
- Kotlin
Imagine a data preparation workflow that creates an S3 bucket, uploads a file to it, and then returns the URL.
Let's now kick off this workflow from another process.
- Connect to the Restate server and create a client for the data preparation workflow.
- Kick off a new data preparation workflow. This is idempotent per workflow ID.
- Wait for the result for 30 seconds.
- If it takes longer, rewire the workflow to send an email instead. If returns within 30 seconds, process the URL directly.
- This is implemented in the data preparation workflow by letting the workflow signal our handler when it's done. It does this by resolving a shared Durable Promise that we then retrieve in our handler to send the email.
const dataPreparationService = restate.workflow({name: "dataPrep",handlers: {run: async (ctx: WorkflowContext, args: { userId: string }) => {const url = await ctx.run(() => createS3Bucket());await ctx.run(() => uploadData(url));await ctx.promise<URL>("url").resolve(url);return url;},resultAsEmail: async (ctx: WorkflowSharedContext,req: { email: string }) => {const url = await ctx.promise<URL>("url");await ctx.run(() => sendEmail(url, req.email));},},});export type DataPrepService = typeof dataPreparationService;
Imagine a data preparation workflow that creates an S3 bucket, uploads a file to it, and then returns the URL.
Let's now kick off this workflow from another process.
- Connect to the Restate server and create a client for the data preparation workflow.
- Kick off a new data preparation workflow. This is idempotent per workflow ID.
- Wait for the result for 30 seconds.
- If it takes longer, rewire the workflow to send an email instead. If returns within 30 seconds, process the URL directly.
- This is implemented in the data preparation workflow by letting the workflow signal our handler when it's done. It does this by resolving a shared Durable Promise that we then retrieve in our handler to send the email.
@Workflowpublic class DataPreparationService {private static final DurablePromiseKey<URL> URL_PROMISE =DurablePromiseKey.of("url", JacksonSerdes.of(URL.class));@Workflowpublic URL run(WorkflowContext ctx, String userId) {URL url = ctx.run(JacksonSerdes.of(URL.class), () -> createS3Bucket());ctx.run(() -> uploadData(url));ctx.promiseHandle(URL_PROMISE).resolve(url);return url;}@Sharedpublic void resultAsEmail(SharedWorkflowContext ctx, Email email) {URL url = ctx.promise(URL_PROMISE).awaitable().await();ctx.run(() -> sendEmail(url, email));}public static void main(String[] args) {RestateHttpEndpointBuilder.builder().bind(new DataPreparationService()).buildAndListen();}}
Imagine a data preparation workflow that creates an S3 bucket, uploads a file to it, and then returns the URL.
Let's now kick off this workflow from another process.
- Connect to the Restate server and create a client for the data preparation workflow.
- Kick off a new data preparation workflow. This is idempotent per workflow ID.
- Wait for the result for 30 seconds.
- If it takes longer, rewire the workflow to send an email instead. If returns within 30 seconds, process the URL directly.
- This is implemented in the data preparation workflow by letting the workflow signal our handler when it's done. It does this by resolving a shared Durable Promise that we then retrieve in our handler to send the email.
@Workflowclass DataPreparationService {companion object {private val URL_PROMISE = DurablePromiseKey.of("url", KtSerdes.json<URL>())}@Workflowsuspend fun run(ctx: WorkflowContext, userId: String): URL {val url: URL = ctx.runBlock { createS3Bucket() }ctx.runBlock { uploadData(url) }ctx.promiseHandle(URL_PROMISE).resolve(url)return url}@Sharedsuspend fun resultAsEmail(ctx: SharedWorkflowContext, email: Email) {val url: URL = ctx.promise(URL_PROMISE).awaitable().await()ctx.runBlock { sendEmail(url, email) }}}
What you can build with Async Tasks and Restate
Payments: Combining sync & async responses
Issue an idempotent payment to Stripe and process the response. The payment provider either responds immediately or notifies us later via a webhook.
Job scheduler
A job scheduler that can handle both immediate and delayed jobs. The scheduler makes sure job submissions are idempotent and that jobs run to completion exactly once. You can attach back to jobs to retrieve their result later.
Building Durable Promises on top of Restate
Promises that get durably stored in Restate, and can be resolved by any process. Implemented as a Restate service, to use for callbacks, signal and communicate between systems. The SDK offers similar constructs with Awakeables and Durable Promises.