Fetch Service
De✨light✨fully make external API calls from DOs that extend LumenizeDO with reduced wall-clock billing.
Not worth it below 5s average fetch time
There are storage and additional request costs, so there is a break even point below which using this service costs more. In our analysis, that breakeven point is when the average fetch time is approximately 1.6 seconds. To be worth the added complexity and latency, we recommend you only consider using this if the average fetch takes longer than 5 seconds.
- Fire-and-forget — Returns immediately, result arrives via continuation
- Cost-effective — CPU billing for fetches, minimal DO billing
- Result guaranteed — Uses alarms to assure handler always sees response or failure
Quick Start
// ...
fetchUserData(userId: string) {
this.svc.fetch.proxy(
`https://api.example.com/users/${userId}`,
this.ctn().handleResult(userId, this.ctn().$result) // Context + $result placeholder
);
}
@mesh() // Required decorator for continuation handlers
handleResult(userId: string, result: ResponseSync | Error) {
if (result instanceof FetchTimeoutError) {
// Timeout is ambiguous - external API may have processed request
// For non-idempotent operations, check external state before retrying
return;
}
if (result instanceof Error) {
// Definite failure (network error, abort) - safe to retry
console.error(`Failed for ${userId}:`, result);
return;
}
// ResponseSync received - has sync body methods (.json(), .text(), .arrayBuffer())
// Check result.ok, read status codes, use body like any HTTP Response
const data = result.json();
console.log(`User ${userId}:`, data);
}
// ...
Key points:
- Handlers receive
ResponseSync(sync body access) orError - Use
FetchTimeoutErrorto distinguish ambiguous timeouts from definite failures - Pass context in any handler parameter position.
$resultarrives in the parameter position you specify.
Setup
1. Install:
npm install @lumenize/fetch
2. Export entrypoint:
// src/index.ts
export { FetchExecutorEntrypoint } from '@lumenize/fetch';
3. Add binding in wrangler.jsonc:
{
"services": [{
"binding": "FETCH_EXECUTOR",
"service": "my-worker",
"entrypoint": "FetchExecutorEntrypoint"
}]
}
4. Import to side-effect register this.svc.fetch:
import '@lumenize/fetch';
API Reference
this.svc.fetch.proxy(request, continuation, options?)
| Parameter | Type | Description |
|---|---|---|
request | string | RequestSync | URL or RequestSync object |
continuation | Continuation | Handler via this.ctn().method(this.ctn().$result) |
options.timeout | number | Request timeout in ms (default: 30000) |
options.executorBinding | string | Binding name (default: 'FETCH_EXECUTOR') |
options.reqId | string | Request ID (generated if not provided) |
Returns: string — Request ID for logging/debugging.
Retry Pattern
// ...
fetchWithRetry(url: string, attempt: number = 1) {
this.svc.fetch.proxy(url, this.ctn().handleRetryResult(url, attempt, this.ctn().$result));
}
@mesh()
handleRetryResult(url: string, attempt: number, result: ResponseSync | Error) {
if (result instanceof FetchTimeoutError) {
// Timeout is ambiguous - for idempotent GETs, retry is safe
if (attempt < 3) {
this.fetchWithRetry(url, attempt + 1);
return;
}
}
if (result instanceof Error && attempt < 3) {
// Definite failure - safe to retry
this.fetchWithRetry(url, attempt + 1);
return;
}
if (result instanceof Error) {
console.error('All retries failed:', result);
// ...
}
if (!result.ok && result.status >= 500 && attempt < 3) {
this.fetchWithRetry(url, attempt + 1);
return;
}
// ...
console.log('Success:', result.json());
// ...
Architecture
For detailed flow diagrams and failure scenarios, see Architecture & Failure Modes.
Flow summary:
- Origin DO schedules alarm with embedded continuation
- Origin DO dispatches fetch to Executor (returns immediately)
- Executor performs fetch (CPU billing, not DO wall-clock billing)
- Executor delivers result to Origin DO
- Origin DO cancels alarm, executes handler synchronously