How to Add Delayed Jobs and Retries to Cloudflare Workers

Cloudflare Workers are fast, cheap, and globally distributed. But when you need to do something later — send an email in an hour, retry a failed payment, expire a session after 30 minutes — you’re stuck piecing together Queues, Durable Objects, and D1 to build what amounts to a task queue.

Workers are great at handling requests. They’re not great at remembering to send one later.

What Cloudflare Gives You

Cloudflare has built a lot of primitives:

Feature What It Does Limitation
Cron Triggers Run a Worker on a schedule No arbitrary delays, no per-request scheduling
Queues Message queue between Workers You manage consumers, retries, DLQ, backoff
Durable Objects Stateful coordination Complex API, billing per-request + duration
D1 SQLite database No built-in scheduler, need to poll

To build “send this request in 2 hours with 3 retries,” you’d need to:

  1. Write the message to a Queue or D1
  2. Set up a Cron Trigger or Durable Object alarm to poll for due messages
  3. Implement retry logic with exponential backoff
  4. Handle dead letters for permanently failed messages
  5. Build a dashboard to see what’s pending and what failed

That’s a real queue system. You’re now maintaining infrastructure instead of shipping features.

The One-Line Alternative

// worker.js
export default {
  async fetch(request, env) {
    const { userId, email } = await request.json();
    
    // Process signup immediately
    await env.DB.prepare('INSERT INTO users (id, email) VALUES (?, ?)').bind(userId, email).run();
    
    // Send welcome email in 2 hours — one fetch call
    await fetch(
      `https://zeplo.to/https://your-worker.your-domain.workers.dev/api/welcome-email?_delay=7200&_retry=3&_token=${env.ZEPLO_TOKEN}`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ userId, email }),
      }
    );
    
    return Response.json({ success: true });
  }
};

Zeplo holds the request for 2 hours, then calls your Worker endpoint. If it fails, retries 3 times with exponential backoff. Your Worker just handles the request when it arrives — no queue management.

Patterns That Work With Workers

Webhook Processing With Retry

Cloudflare Workers are popular as webhook receivers because of the global edge network. But webhook processing often calls flaky downstream services:

// Receive Stripe webhook on the edge, process reliably via Zeplo
export default {
  async fetch(request, env) {
    const event = await request.json();
    
    // Verify signature at the edge (fast)
    if (!verifyStripeSignature(request, env.STRIPE_WEBHOOK_SECRET)) {
      return new Response('Invalid signature', { status: 401 });
    }
    
    // Queue for reliable processing with retry
    await fetch(
      `https://zeplo.to/https://your-worker.workers.dev/process-webhook?_retry=5&_token=${env.ZEPLO_TOKEN}`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(event),
      }
    );
    
    // Return 200 immediately — Stripe is happy
    return Response.json({ received: true });
  }
};

Stripe gets an instant response from the edge. Your processing Worker gets reliable delivery with retry. No Queues or Durable Objects needed.

Session and Token Expiration

// When creating a magic link, schedule its expiration
async function createMagicLink(email, env) {
  const token = crypto.randomUUID();
  await env.KV.put(`magic:${token}`, email, { expirationTtl: 900 }); // KV expires in 15min
  
  // Also schedule a cleanup job (in case KV TTL isn't enough)
  await fetch(
    `https://zeplo.to/https://your-worker.workers.dev/expire-token?_delay=900&_token=${env.ZEPLO_TOKEN}`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token }),
    }
  );
  
  return `https://your-app.com/login?token=${token}`;
}

Scheduled Reports With Cron

Cloudflare Cron Triggers work for recurring tasks, but they can’t do arbitrary delays. Combine both:

export default {
  // Cron Trigger: runs daily at 8am UTC
  async scheduled(event, env) {
    const report = await generateDailyReport(env);
    
    // Send to Slack immediately
    await sendSlack(report, env);
    
    // Also email the PDF version — delay 5 minutes for PDF generation
    await fetch(
      `https://zeplo.to/https://your-worker.workers.dev/email-report?_delay=300&_retry=3&_token=${env.ZEPLO_TOKEN}`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ reportId: report.id }),
      }
    );
  },
  
  async fetch(request, env) {
    // Handle the delayed email-report request
    const url = new URL(request.url);
    if (url.pathname === '/email-report') {
      const { reportId } = await request.json();
      await generateAndEmailPDF(reportId, env);
      return Response.json({ sent: true });
    }
    // ... other routes
  }
};

Rate-Limited API Calls

Workers often aggregate data from rate-limited APIs. Instead of building a rate limiter:

// Fetch data from a rate-limited API with automatic backoff
async function fetchWithRateLimit(apiUrl, env) {
  // Zeplo automatically respects Retry-After headers
  const response = await fetch(
    `https://zeplo.to/${apiUrl}?_retry=5&_token=${env.ZEPLO_TOKEN}`,
    {
      headers: { 'Authorization': `Bearer ${env.API_KEY}` },
    }
  );
  return response.json();
}

Zeplo vs Cloudflare Queues

Zeplo Cloudflare Queues
Setup One fetch() call Configure producer, consumer, bindings
Delays _delay=seconds Not supported natively
Retry _retry=N (automatic backoff) Manual in consumer code
Cron _cron=... Separate Cron Triggers
Dashboard Full request/response logs Basic metrics
Pricing Free (500/mo), $39/mo Pro $0.40/M messages + operations
Vendor lock-in None (standard HTTP) Cloudflare only

Use Cloudflare Queues when you need high-throughput message passing between Workers. Use Zeplo when you need “do this HTTP request later with retry” without building queue infrastructure.

Getting Started

# wrangler.toml — just add the secret
[vars]
# Set via: wrangler secret put ZEPLO_TOKEN
wrangler secret put ZEPLO_TOKEN
# Paste your token from app.zeplo.io

That’s the entire setup. No queue bindings, no consumer Workers, no D1 schema.

  1. Sign up free — 500 requests/month
  2. wrangler secret put ZEPLO_TOKEN
  3. fetch('https://zeplo.to/...') from any Worker

Zeplo adds delayed jobs, retry, and scheduling to Cloudflare Workers with zero infrastructure. Start free →

Return to Blog