How to Schedule Background Jobs in Cloudflare Workers (Without Durable Objects)

Learn how to schedule HTTP callbacks, cron jobs, and retries in Cloudflare Workers without Durable Objects — using one API call.

Erlan Mar 25, 2026 10 min read

Cloudflare Workers are fast, cheap, and globally distributed. But there’s one thing they’re terrible at: running code later.

Need to send a welcome email 30 minutes after signup? Process a webhook retry after a failed delivery? Run a cleanup job every night at midnight? Workers don’t have a built-in way to do any of this without reaching for Durable Objects — which adds complexity, cost, and a new programming model you probably don’t want to learn just to schedule a task.

In this tutorial, you’ll learn how to schedule background jobs from Cloudflare Workers using Fliq — an HTTP workflow engine that handles scheduling, retries, and execution history with a single API call. No Durable Objects. No Queues. No external Redis. Just HTTP.

The problem with scheduling in Workers

Cloudflare Workers run in a V8 isolate with strict execution limits. You can’t set a timeout for 30 minutes. You can’t keep a WebSocket connection alive between requests. The execution context dies after the response is sent (unless you use waitUntil, which still has a 30-second cap).

Your options today:

  • Cron Triggers — limited to the cron syntax Cloudflare supports. Can’t schedule one-off future events. Can’t pass dynamic payloads.
  • Durable Objects — powerful, but you’re now managing stateful actors. Overkill for “call this URL in 5 minutes.”
  • Cloudflare Queues — great for fan-out, but no built-in delayed delivery or scheduling.
  • External services — AWS EventBridge, Google Cloud Scheduler, etc. Now you’re managing credentials across cloud providers.

What you actually want is simple: “Call this URL at this time, and retry if it fails.”

That’s exactly what Fliq does.

What is Fliq?

Fliq is a serverless HTTP workflow engine. You send it one API call with a URL and a time, and Fliq executes the HTTP request on schedule — with automatic retries and full execution history.

Think of it as a cloud-native setTimeout that actually works in production.

Key features:

  • One-time and recurring jobs (cron expressions)
  • Automatic retries with exponential backoff
  • Full execution history — see every attempt, status code, response time
  • At-least-once delivery — stable X-Fliq-Delivery-Id across retries so you can dedupe
  • Pay per execution — free 100,000/day during beta, then $1 per 100k executions

Setting up your Worker

Let’s build a practical example: a Cloudflare Worker that handles user signups and schedules a welcome email to be sent 30 minutes later.

Step 1: Create a new Workers project

npm create cloudflare@latest -- my-scheduled-worker
cd my-scheduled-worker

Choose “Hello World” as the template when prompted.

Step 2: Get your Fliq API token

Sign up at fliq.sh and grab your API token from the dashboard settings. You’ll need this to authenticate API calls.

Store it as a Workers secret:

npx wrangler secret put FLIQ_API_TOKEN

Paste your fliq_sk token when prompted.

Step 3: Write the Worker

Replace src/index.ts with the following:

export interface Env {
  FLIQ_API_TOKEN: string;
}

export default {
  async fetch(request: Request, env: Env) {
    const url = new URL(request.url);

    if (url.pathname === "/api/signup" && request.method === "POST") {
      return handleSignup(request, env);
    }

    if (url.pathname === "/api/send-welcome-email" && request.method === "POST") {
      return handleSendWelcomeEmail(request);
    }

    return new Response("Not found", { status: 404 });
  },
};

async function handleSignup(request: Request, env: Env) {
  const body = await request.json() as { email: string; name: string };

  // 1. Save user to your database (D1, Turso, PlanetScale, etc.)

  // 2. Schedule welcome email for 30 minutes from now
  const scheduledAt = new Date(Date.now() + 30 * 60 * 1000).toISOString();
  const workerUrl = new URL("/api/send-welcome-email", request.url).toString();

  const fliqResponse = await fetch("https://api.fliq.sh/v1/jobs", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": "Bearer " + env.FLIQ_API_TOKEN,
    },
    body: JSON.stringify({
      url: workerUrl,
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        email: body.email,
        name: body.name,
        type: "welcome",
      }),
      scheduled_at: scheduledAt,
      max_retries: 3,
    }),
  });

  if (!fliqResponse.ok) {
    return new Response("Failed to schedule welcome email", { status: 500 });
  }

  const job = await fliqResponse.json() as { id: string };

  return Response.json({
    message: "Signup successful! Welcome email scheduled.",
    job_id: job.id,
  });
}

async function handleSendWelcomeEmail(request: Request) {
  const body = await request.json() as {
    email: string;
    name: string;
    type: string;
  };

  // Send the email using your provider (Resend, SendGrid, Mailgun, etc.)
  console.log("Sending welcome email to " + body.email);

  return Response.json({ sent: true });
}

Step 4: Deploy and test

npx wrangler deploy

Test the signup endpoint:

curl -X POST https://my-scheduled-worker.your-account.workers.dev/api/signup \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com", "name": "Alice"}'

You’ll get back a response with the scheduled job ID. In 30 minutes, Fliq will POST to your /api/send-welcome-email endpoint with the user’s data. If the request fails, Fliq retries up to 3 times with exponential backoff.

Adding recurring jobs with cron

Fliq also supports cron expressions for recurring jobs. Here’s how to schedule a daily cleanup job that runs every night at midnight UTC:

const response = await fetch("https://api.fliq.sh/v1/jobs", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer " + env.FLIQ_API_TOKEN,
  },
  body: JSON.stringify({
    url: "https://my-worker.example.com/api/daily-cleanup",
    method: "POST",
    cron: "0 0 * * *",
    max_retries: 5,
  }),
});

Handling retries and failures

Fliq automatically retries failed jobs (any non-2xx response). You can configure the retry behavior per job:

{
  "url": "https://my-worker.example.com/api/process-payment",
  "method": "POST",
  "body": "{\"invoice_id\": \"inv_123\"}",
  "scheduled_at": "2026-03-26T10:00:00Z",
  "max_retries": 5
}

Each retry is a separate execution attempt. You can see every attempt — including the status code, response time, and response body — in the Fliq dashboard.

Monitoring execution history

Every job in Fliq has a full execution history. You can check the status of any job via the API:

curl https://api.fliq.sh/v1/jobs/job_abc123 \
  -H "Authorization: Bearer YOUR_TOKEN"

Or view it in the Fliq dashboard, which shows:

  • Job status (pending, running, completed, failed)
  • Every execution attempt with timestamps
  • HTTP status codes and response times
  • Request and response bodies

Cost comparison

Let’s compare the cost of scheduling 100,000 background jobs per month:

ApproachMonthly costSetup timeMaintenance
Fliq$15 minutesNone
Durable Objects~$5-15HoursManage state, handle alarms
AWS EventBridge + Lambda~$10-20HoursIAM, CloudFormation, monitoring
Self-hosted Redis + BullMQ$20-50+DaysServer, monitoring, on-call

Complete example: multi-step onboarding flow

Here’s a more realistic example — scheduling a complete onboarding sequence when a user signs up:

async function scheduleOnboarding(
  env: Env,
  baseUrl: string,
  user: { email: string; name: string; id: string }
) {
  const now = Date.now();
  const MINUTE = 60 * 1000;
  const DAY = 24 * 60 * MINUTE;

  const jobs = [
    {
      url: baseUrl + "/api/emails/welcome",
      scheduled_at: new Date(now + 30 * MINUTE).toISOString(),
      body: { userId: user.id, type: "welcome" },
    },
    {
      url: baseUrl + "/api/emails/getting-started",
      scheduled_at: new Date(now + 1 * DAY).toISOString(),
      body: { userId: user.id, type: "getting-started" },
    },
    {
      url: baseUrl + "/api/emails/tips",
      scheduled_at: new Date(now + 3 * DAY).toISOString(),
      body: { userId: user.id, type: "tips" },
    },
    {
      url: baseUrl + "/api/check-activation",
      scheduled_at: new Date(now + 7 * DAY).toISOString(),
      body: { userId: user.id, type: "activation-check" },
    },
  ];

  const results = await Promise.all(
    jobs.map((job) =>
      fetch("https://api.fliq.sh/v1/jobs", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "Authorization": "Bearer " + env.FLIQ_API_TOKEN,
        },
        body: JSON.stringify({
          ...job,
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(job.body),
          max_retries: 3,
        }),
      })
    )
  );

  return results;
}

Four API calls, and you’ve scheduled a complete onboarding sequence spanning a week. No cron jobs, no state management, no infrastructure.

Wrapping up

Cloudflare Workers are great for handling requests. Fliq is great for scheduling them. Together, they give you a fully serverless stack for any time-based workflow — without the complexity of Durable Objects or external queue infrastructure.

What you get with Fliq + Workers:

  • Schedule any HTTP request for any future time
  • Automatic retries with exponential backoff
  • Full execution history and monitoring
  • Dynamic cron schedules created via API
  • No infrastructure to manage
Try Fliq free — 100,000 executions/day

Further reading

Erlan

Fliq team