---
title: Overview
description: A clock for handler retries and timers.
---

```ts server/orders.ts
import { createServer } from 'experimental-a2/server'
import { redis } from 'experimental-a2/store-redis'
import { vercelQueues } from 'experimental-a2/scheduler-vercel'
import { orders } from '@/contracts'

export const scheduler = vercelQueues()

export const ordersServer = createServer({
  contract: orders,
  store: redis({ url: process.env.REDIS_URL }),
  scheduler,
  handlers: {
    /* ... */
  },
})
```

```ts app/api/a2/scheduler/route.ts
import { billingServer, ordersServer, scheduler } from '@/server'

export const POST = scheduler.handler(ordersServer, billingServer)
```

A scheduler wakes sessions on a clock. It retries a handler that failed or
whose process died, and it delivers [timers](/server/timers), whose delays must
fit the [provider's limit](/server/timers#provider-limits). Share one
scheduler across your servers and mount its route once, with every server it
delivers to. `scheduler.handler(...)` checks those servers when the module
loads, so a bad route fails at startup.

| Scheduler | Use it for |
| --- | --- |
| [Vercel Queues](/schedulers/vercel-queues) | Apps on Vercel (recommended) |
| [QStash](/schedulers/qstash) | A stable public URL, called from separate infrastructure |

## Without a scheduler

If no event type has a handler and nothing calls `session.schedule`, skip it.
Events settle as they're appended, with no queue messages. That's a complete
production setup for history, reducers, and live sync.

With handlers but no scheduler, a session only wakes on a top-level append or
an explicit `server.drain()`. Reads never wake it, so a session nobody writes
to stays stuck. That's fine for internal tools that can heal on the next
write. For production, add a scheduler.

## More than one scheduler

Servers with different scheduler instances need separate routes, even from the
same adapter. Give each QStash instance its own `url`, and each Vercel Queues
instance its own `topic` and trigger.

## Dispatch in a fresh invocation

`append` starts handlers in the same request and uses the scheduler as a
watchdog. `append.dispatch` stores the event and hands its handlers to the
queue instead:

```ts app/api/imports/route.ts
import { ordersServer } from '@/server'

export async function POST(request: Request) {
  const { orderId, shopId, items } = await request.json()
  await ordersServer.session(orderId).append.dispatch({
    id: `created:${orderId}`,
    type: 'created',
    payload: { shopId, items },
  })
  return Response.json({ accepted: true }, { status: 202 })
}
```

It needs a scheduler and waits for the queue to accept the message. If that
send fails, the event may already be stored; its explicit `id` makes a retry
safe.

## Deploys and rollbacks

Queued work can reach a different deployment than the one that sent it. Keep
event schemas compatible across deployments. Each adapter's page explains
where its tasks go: [Vercel Queues](/schedulers/vercel-queues#deployments) and
[QStash](/schedulers/qstash#deployments).

:::warning
Don't roll back to an A2 version without timer delivery while scheduled tasks
are outstanding. An old route can acknowledge a scheduled append without
writing its events. Rolling back past scheduled clears fails too: an old
Vercel Queues route keeps failing the clear until retention ends, and an old
QStash route moves it to QStash's dead-letter queue. Roll forward, or wait for
those tasks to deliver first.
:::

## Send errors

When a provider rejects a send, both adapters keep the provider's original
error.

- **Terminal:** a definite 4xx status or error, except 408, 409, 425, and 429.
  The Vercel adapter also treats the SDK's `BadRequestError`,
  `UnauthorizedError`, and `ForbiddenError` as terminal.
- **Retryable:** 408, 409, 425, 429, 5xx, network failures, and errors with no
  recognized status.
- Vercel Queues' duplicate-message error counts as success.

A2's durable AI tools use this classification. Direct `session.schedule()`
calls still reject with the original error.
