Skip to main content
Technology & EngineeringBackground Jobs Services196 lines

Quirrel

"Quirrel: job queue for serverless/edge, cron jobs, delayed jobs, repeat scheduling, Next.js/Remix/SvelteKit integration, type-safe queues"

Quick Summary20 lines
Quirrel is a lightweight, developer-friendly job queue purpose-built for serverless and edge environments. Unlike traditional job queues that require persistent worker processes, Quirrel works by calling back into your application's HTTP endpoints when jobs are due. This makes it a natural fit for serverless frameworks like Next.js, Remix, Nuxt, and SvelteKit where long-running processes are not available.

## Key Points

- Always set meaningful job IDs for operations that should be idempotent (e.g., `password-reset-${userId}`), so re-enqueuing the same job replaces rather than duplicates.
- Use the `retry` option with escalating delays for transient failures, and make your job handlers idempotent since retries mean a job can run more than once.
- Run `npx quirrel ci` in your CI/CD pipeline to automatically register CronJobs on deployment, ensuring your cron schedule stays in sync with your code.
- Forgetting to set `QUIRREL_BASE_URL` in production means Quirrel will try to call `localhost`, which silently drops all jobs. Always set this to your public app URL.

## Quick Example

```bash
# docker-compose.yml
# Quirrel requires Redis for persistence
docker run -p 9181:9181 \
  -e REDIS_URL=redis://redis:6379 \
  ghcr.io/quirrel-dev/quirrel
```
skilldb get background-jobs-services-skills/QuirrelFull skill: 196 lines
Paste into your CLAUDE.md or agent config

Quirrel

Core Philosophy

Quirrel is a lightweight, developer-friendly job queue purpose-built for serverless and edge environments. Unlike traditional job queues that require persistent worker processes, Quirrel works by calling back into your application's HTTP endpoints when jobs are due. This makes it a natural fit for serverless frameworks like Next.js, Remix, Nuxt, and SvelteKit where long-running processes are not available.

Quirrel can run as a managed service (quirrel.dev) or self-hosted. It stores jobs durably and invokes your API routes at the scheduled time, handling retries and deduplication. The TypeScript SDK provides full type safety for job payloads, and framework-specific adapters make integration seamless.

Setup

Installation and Basic Configuration

// npm install quirrel

// For Next.js (App Router or Pages Router):
// npm install quirrel

// quirrel.config.js (or set env vars)
// QUIRREL_TOKEN=your-token          (for managed service)
// QUIRREL_BASE_URL=http://localhost:3000  (your app URL)
// QUIRREL_API_URL=http://localhost:9181   (self-hosted Quirrel server)

Self-Hosting with Docker

# docker-compose.yml
# Quirrel requires Redis for persistence
docker run -p 9181:9181 \
  -e REDIS_URL=redis://redis:6379 \
  ghcr.io/quirrel-dev/quirrel

Next.js Pages Router Integration

// pages/api/queues/email.ts
import { Queue } from "quirrel/next-pages";

export default Queue(
  "api/queues/email", // Must match the API route path
  async (payload: { to: string; subject: string; body: string }) => {
    await sendEmail(payload.to, payload.subject, payload.body);
  },
  {
    retry: ["1min", "5min", "30min"], // Retry delays
  }
);

Next.js App Router Integration

// app/api/queues/email/route.ts
import { Queue } from "quirrel/next-app";

const emailQueue = Queue(
  "api/queues/email",
  async (payload: { to: string; subject: string; body: string }) => {
    await sendEmail(payload.to, payload.subject, payload.body);
  }
);

export const POST = emailQueue;
export default emailQueue;

Core Patterns

Enqueuing Jobs

// From anywhere in your application:
import emailQueue from "@/pages/api/queues/email";

// Immediate job
await emailQueue.enqueue({
  to: "user@example.com",
  subject: "Welcome",
  body: "Hello!",
});

// Delayed job — runs after 10 minutes
await emailQueue.enqueue(
  { to: "user@example.com", subject: "Follow-up", body: "Hey!" },
  { delay: "10min" }
);

// Scheduled job — runs at a specific time
await emailQueue.enqueue(
  { to: "user@example.com", subject: "Reminder", body: "Don't forget!" },
  { runAt: new Date("2025-12-25T09:00:00Z") }
);

// Idempotent job — deduplicate by ID
await emailQueue.enqueue(
  { to: "user@example.com", subject: "Reset", body: "Link..." },
  { id: `password-reset-${userId}` }
);

CronJob — Recurring Schedules

// pages/api/queues/daily-digest.ts
import { CronJob } from "quirrel/next-pages";

export default CronJob(
  "api/queues/daily-digest",
  "0 8 * * *", // Every day at 8:00 AM UTC
  async () => {
    const users = await db.user.findMany({ where: { digestEnabled: true } });
    for (const user of users) {
      await generateAndSendDigest(user);
    }
  }
);

Remix Integration

// app/queues/invoice.server.ts
import { Queue } from "quirrel/remix";

export const invoiceQueue = Queue(
  "queues/invoice", // matches the route path
  async (payload: { orderId: string }) => {
    const order = await db.order.findUnique({ where: { id: payload.orderId } });
    await generateInvoicePdf(order);
    await emailInvoice(order);
  }
);

// app/routes/queues.invoice.tsx
import { invoiceQueue } from "~/queues/invoice.server";
export const action = invoiceQueue;

Managing Jobs

import emailQueue from "@/pages/api/queues/email";

// Delete a specific job by its ID
await emailQueue.delete("password-reset-123");

// Invoke a job immediately (useful for testing)
await emailQueue.invoke("password-reset-123");

// Get a job by ID
const job = await emailQueue.getById("password-reset-123");
console.log(job?.runAt, job?.body);

Local Development with the Quirrel CLI

# Start the Quirrel dev server (no external dependencies needed)
npx quirrel

# In another terminal, start your app
npm run dev

# The Quirrel UI is available at http://localhost:9181
# It shows pending jobs and lets you trigger them manually

Best Practices

  • Always set meaningful job IDs for operations that should be idempotent (e.g., password-reset-${userId}), so re-enqueuing the same job replaces rather than duplicates.
  • Use the retry option with escalating delays for transient failures, and make your job handlers idempotent since retries mean a job can run more than once.
  • Run npx quirrel ci in your CI/CD pipeline to automatically register CronJobs on deployment, ensuring your cron schedule stays in sync with your code.

Common Pitfalls

  • The API route path string passed to Queue() must exactly match the file-system route (e.g., "api/queues/email" for pages/api/queues/email.ts). A mismatch means Quirrel cannot call back into your app.
  • Forgetting to set QUIRREL_BASE_URL in production means Quirrel will try to call localhost, which silently drops all jobs. Always set this to your public app URL.

Anti-Patterns

Using the service without understanding its pricing model. Cloud services bill differently — per request, per GB, per seat. Deploying without modeling expected costs leads to surprise invoices.

Hardcoding configuration instead of using environment variables. API keys, endpoints, and feature flags change between environments. Hardcoded values break deployments and leak secrets.

Ignoring the service's rate limits and quotas. Every external API has throughput limits. Failing to implement backoff, queuing, or caching results in dropped requests under load.

Treating the service as always available. External services go down. Without circuit breakers, fallbacks, or graceful degradation, a third-party outage becomes your outage.

Coupling your architecture to a single provider's API. Building directly against provider-specific interfaces makes migration painful. Wrap external services in thin adapter layers.

Install this skill directly: skilldb add background-jobs-services-skills

Get CLI access →