# How SyncRetryScheduler Persists Retry State for Failed Post‑Command Pulls

> Learn how SyncRetryScheduler persists retry state for failed post-command pulls using Durable Object KV storage and scheduled alarms for exponential back-off retries.

- Repository: [Cloudflare/computer](https://github.com/cloudflare/computer)
- Tags: internals
- Published: 2026-08-14

---

**The `SyncRetryScheduler` interface persists retry state using Durable Object KV storage with namespaced keys like `retry:${backend}`, combined with `setAlarm()` to schedule wake‑up times for exponential back‑off retries.**

When a post‑command pull fails in the Cloudflare Computer framework, the system must remember to retry later—even across worker restarts. The `SyncRetryScheduler` abstraction provides this durability boundary, separating intent definition from storage mechanics. This article examines the scheduler's design, persistence implementation, and testing patterns based on the [cloudflare/computer](https://github.com/cloudflare/computer) source code.

---

## SyncRetryIntent: The Retry State Structure

Before exploring persistence, understand what gets stored. The `SyncRetryIntent` interface—defined in [[`packages/computer/src/workspace.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/workspace.ts)](https://github.com/cloudflare/computer/blob/main/packages/computer/src/workspace.ts#L54-L61)—encapsulates everything needed to resume a failed pull:

```typescript
interface SyncRetryIntent {
  backend: string;      // Target backend identifier
  runtimeId?: string;   // Optional runtime context
  attempt: number;      // Current retry attempt (0‑based)
  notBefore: number;    // Epoch milliseconds for next retry
}

```

The `notBefore` timestamp is critical: it enables **exponential back‑off** by computing future wake‑up times based on the attempt count.

---

## The SyncRetryScheduler Interface

The scheduler contract—found at [workspace.ts#L69-L73](https://github.com/cloudflare/computer/blob/main/packages/computer/src/workspace.ts#L69-L73)—is intentionally minimal:

```typescript
interface SyncRetryScheduler {
  get(backend: string): Promise<SyncRetryIntent | undefined>;
  schedule(intent: SyncRetryIntent): Promise<void>;
  clear(backend: string): Promise<void>;
}

```

Each method serves a distinct lifecycle purpose:

- **`get(backend)`** – Retrieves any pending intent for a backend, returning `undefined` if no retry is scheduled.
- **`schedule(intent)`** – Persists (or overwrites) the intent and triggers the alarm mechanism.
- **`clear(backend)`** – Removes stored state after success or exhaustion, preventing further retries.

---

## Production Persistence: Durable Object Storage

The production implementation uses **Durable Object KV storage** (`ctx.storage`) for durability. Here's how `SyncRetryScheduler` persists retry state in practice:

```typescript
class DurableObjectRetryScheduler implements SyncRetryScheduler {
  constructor(private readonly storage: DurableObjectStorage) {}

  async get(backend: string): Promise<SyncRetryIntent | undefined> {
    const raw = await this.storage.get(`retry:${backend}`);
    return raw 
      ? JSON.parse(raw as string) as SyncRetryIntent 
      : undefined;
  }

  async schedule(intent: SyncRetryIntent): Promise<void> {
    const key = `retry:${intent.backend}`;
    
    // Persist the retry intent
    await this.storage.put(key, JSON.stringify(intent));
    
    // Schedule wake-up for exponential back-off retry
    await this.storage.setAlarm(intent.notBefore);
  }

  async clear(backend: string): Promise<void> {
    await this.storage.delete(`retry:${backend}`);
    await this.storage.setAlarm(null); // Cancel pending alarm
  }
}

```

Key implementation details:

- **Namespaced keys** – The `retry:${backend}` pattern prevents collisions with other storage usage.
- **Atomic scheduling** – Both the intent and alarm are set in the same `schedule()` call, ensuring consistency.
- **Alarm integration** – `setAlarm(notBefore)` guarantees the Durable Object wakes exactly when needed to invoke `retryPendingSync(backend)`.

---

## Alarm‑Driven Retry Loop

Persistence alone isn't sufficient—the scheduler coordinates with the Durable Object alarm system. When `schedule()` calls `setAlarm(intent.notBefore)`, the following sequence occurs:

1. The Durable Object sleeps until the alarm fires.
2. The runtime invokes `retryPendingSync(backend)`, which:
   - Calls `scheduler.get(backend)` to read the persisted `SyncRetryIntent`.
   - Attempts the post‑command pull again.
   - On failure: increments `attempt`, recomputes `notBefore` with exponential back‑off, and calls `schedule(updatedIntent)`.
   - On success: calls `scheduler.clear(backend)`.

This loop continues until success or `maxAttempts` exhaustion, with all state surviving restarts due to KV storage.

---

## Integrating SyncRetryScheduler with Workspace

To enable retry persistence, pass a scheduler instance when constructing a `Workspace`:

```typescript
const ws = new Workspace({
  storage: ctx.storage,
  backends: [...],
  retryScheduler: new DurableObjectRetryScheduler(ctx.storage),
  retry: {
    initialDelayMs: 2000,
    maxDelayMs: 30000,
    maxAttempts: 5
  },
});

```

The `retry` configuration object controls back‑off behavior, while `retryScheduler` handles the actual state persistence.

---

## Testing: The MemoryRetryScheduler Double

The test suite in [[`packages/computer/src/retry.test.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/retry.test.ts)](https://github.com/cloudflare/computer/blob/main/packages/computer/src/retry.test.ts#L14-L30) provides a **deterministic, in‑memory implementation**:

```typescript
class MemoryRetryScheduler implements SyncRetryScheduler {
  private readonly map = new Map<string, SyncRetryIntent>();

  async get(backend: string): Promise<SyncRetryIntent | undefined> {
    return this.map.get(backend);
  }

  async schedule(intent: SyncRetryIntent): Promise<void> {
    this.map.set(intent.backend, intent);
  }

  async clear(backend: string): Promise<void> {
    this.map.delete(backend);
  }
}

```

This double allows unit tests to verify retry logic without spinning up Durable Objects or handling actual timer delays.

---

## Key Files Reference

| File | Purpose |
|------|---------|
| [`packages/computer/src/workspace.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/workspace.ts) | Defines `SyncRetryIntent`, `SyncRetryScheduler` interface, and `Workspace` configuration |
| [`packages/computer/src/retry.test.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/retry.test.ts) | Contains `MemoryRetryScheduler` test double and retry contract validation |

---

## Summary

- **`SyncRetryScheduler`** provides a minimal, durable interface for recording retry intent after failed post‑command pulls.
- **Production persistence** uses Durable Object KV storage with `retry:${backend}` keys and `setAlarm()` for wake‑up scheduling.
- **State structure** (`SyncRetryIntent`) includes `attempt` counters and `notBefore` timestamps to drive exponential back‑off.
- **Testing** relies on `MemoryRetryScheduler`, a `Map`‑backed double that validates behavior without external dependencies.

---

## Frequently Asked Questions

### What happens to retry state if the Durable Object restarts?

The retry state survives restarts because `SyncRetryScheduler` persists intents to `ctx.storage`, which is backed by Cloudflare's distributed KV storage. When the Durable Object restarts, `scheduler.get(backend)` returns the same intent that was stored before the restart.

### How does exponential back‑off work with the scheduler?

The scheduler itself doesn't compute delays—it stores the result. When `retryPendingSync` detects a failure, it increments `intent.attempt`, calculates a new `notBefore` using `Math.min(initialDelay * 2^attempt, maxDelay)`, and calls `schedule(updatedIntent)` to persist both the new state and alarm time.

### Can I implement a custom SyncRetryScheduler for external storage?

Yes. The interface only requires three async methods. You could implement a Redis‑backed or database‑backed scheduler by adapting `get`, `schedule`, and `clear` to your storage of choice. Ensure `schedule` also handles alarm semantics if you need wake‑up behavior.

### Why separate SyncRetryScheduler from the Workspace class?

This separation follows the **dependency inversion principle**. `Workspace` orchestrates sync logic but delegates durability concerns to an injected scheduler. This enables testing with `MemoryRetryScheduler` and allows alternative persistence strategies without modifying core sync behavior.