Background Jobs for Agencies: Add Scheduling and Retries to Client Projects Without the Overhead
You’re building a client project. The scope says “send a follow-up email after 48 hours” or “retry the payment if it fails” or “run a daily inventory sync.”
You know what that means: Redis, a worker process, hosting for the worker, monitoring, and a conversation with the client about ongoing infrastructure costs. For a feature that takes one paragraph to describe and two sprints to build properly.
Or you explain to the client that their \(15K project now needs a \)50/month VPS just to run a cron job, and watch their face.
There’s a faster way.
The Agency Problem
Every agency project has the same tension: clients want sophisticated features, but the budget and timeline assume straightforward web development.
“Delayed tasks” and “background jobs” sound simple in a requirements doc. In practice, they mean:
- A message broker (Redis, RabbitMQ)
- A worker process that runs 24⁄7
- Hosting for that worker (separate from the web app)
- Retry logic, error handling, dead letter queues
- Monitoring so you know when it breaks at 2am on a Saturday
- Documentation so the next developer (or the client’s team) can maintain it
That’s infrastructure. And infrastructure has ongoing costs, maintenance burden, and a learning curve for whoever inherits the project.
What If Background Jobs Were Just an API Call?
// Send a review request 48 hours after purchase
await fetch(
`https://zeplo.to/https://client-site.com/api/review-request?_delay=172800&_retry=3&_token=${ZEPLO_TOKEN}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ orderId, customerEmail }),
}
);
No Redis. No worker. No extra server. Zeplo holds the request for 48 hours, delivers it to the endpoint, and retries if it fails.
The endpoint is a standard API route — the same thing you already build in Next.js, Laravel, Rails, Django, or WordPress. Nothing exotic. Nothing the next developer can’t understand immediately.
Patterns That Come Up in Every Client Project
E-commerce: Post-Purchase Flows
// After checkout completes
async function postPurchaseFlow(order) {
const TOKEN = process.env.ZEPLO_TOKEN;
const BASE = process.env.APP_URL;
// Shipping confirmation check (6 hours)
await fetch(`https://zeplo.to/${BASE}/api/check-shipping?_delay=21600&_retry=3&_token=${TOKEN}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ orderId: order.id }),
});
// Review request (5 days)
await fetch(`https://zeplo.to/${BASE}/api/review-request?_delay=432000&_retry=3&_token=${TOKEN}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ orderId: order.id, email: order.email }),
});
// Repurchase nudge (30 days)
await fetch(`https://zeplo.to/${BASE}/api/repurchase-email?_delay=2592000&_retry=3&_token=${TOKEN}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ customerId: order.customerId }),
});
}
One function. Three delayed tasks. Zero infrastructure.
SaaS: Trial Management
// When user starts a 14-day trial
async function setupTrialReminders(user) {
// Day 3: feature highlight
await scheduleJob('/api/trial/feature-highlight', { userId: user.id }, 3 * 86400);
// Day 10: upgrade nudge
await scheduleJob('/api/trial/upgrade-nudge', { userId: user.id }, 10 * 86400);
// Day 13: last chance
await scheduleJob('/api/trial/last-chance', { userId: user.id }, 13 * 86400);
// Day 14: expire trial
await scheduleJob('/api/trial/expire', { userId: user.id }, 14 * 86400);
}
// Reusable helper
async function scheduleJob(path, body, delaySeconds) {
await fetch(
`https://zeplo.to/${APP_URL}${path}?_delay=${delaySeconds}&_retry=3&_token=${ZEPLO_TOKEN}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}
);
}
Content Sites: Scheduled Publishing
// Schedule a post to go live at a specific date/time
const publishDate = new Date('2026-06-15T09:00:00Z');
const delaySeconds = Math.floor((publishDate - Date.now()) / 1000);
await fetch(
`https://zeplo.to/${APP_URL}/api/publish-post?_delay=${delaySeconds}&_retry=2&_token=${TOKEN}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ postId: draft.id }),
}
);
Any Client: Daily/Weekly Reports
// Weekly analytics email — set up once, runs every Monday at 9am
await fetch(
`https://zeplo.to/${APP_URL}/api/weekly-report?_cron=0|9|*|*|1&_retry=3&_token=${TOKEN}`,
{ method: 'POST' }
);
The Handoff Advantage
Here’s what makes this approach ideal for agency work: handoff is trivial.
When you deliver the project, the client’s team (or their next agency) sees:
- Standard API routes they already know how to maintain
- A
ZEPLO_TOKENenvironment variable - A dashboard at app.zeplo.io showing all scheduled tasks, logs, and retry history
Compare that to handing off a Redis instance, a worker process, a process manager config, monitoring alerts, and a README explaining how BullMQ works.
Documentation for the client:
“Background tasks (delayed emails, retries, scheduled reports) run through Zeplo. Log in at app.zeplo.io to see all active schedules and execution logs. The token is in the environment variables. To add a new scheduled task, create an API route and call it through zeplo.to with a delay or cron parameter. See existing routes in
/api/jobs/for examples.”
That’s the entire ops doc.
Pricing for Client Projects
| Free | Pro | |
|---|---|---|
| Cost | $0 | $39/month |
| Requests | 500/month | 1M/month |
| Good for | Development, staging, low-traffic sites | Production client projects |
For most client projects, $39/month covers everything. Bill it as part of hosting costs alongside Vercel, database, and email service. It’s cheaper than the 2 hours you’d spend setting up and maintaining a worker process.
For development and staging environments, the free tier is plenty.
Why Not Just Use [Platform X] Built-in Cron?
Vercel has cron. Netlify has scheduled functions. But none of them can:
- Delay a specific request by an arbitrary amount of time
- Retry with exponential backoff when endpoints fail
- Schedule dynamically from user actions (not just fixed intervals)
- Show a dashboard with full request/response logs for debugging
Client projects need all of these. “Send this email 48 hours after this specific user does this specific thing” isn’t a cron job — it’s a delayed task triggered by a user action.
Getting Started on Your Next Project
# Add to .env
ZEPLO_TOKEN=your_token_here
// lib/schedule.js — copy this into every project
export async function scheduleTask(path, body, options = {}) {
const params = new URLSearchParams({ _token: process.env.ZEPLO_TOKEN });
if (options.delay) params.set('_delay', String(options.delay));
if (options.retry) params.set('_retry', String(options.retry || 3));
if (options.cron) params.set('_cron', options.cron);
const url = `https://zeplo.to/${process.env.APP_URL}${path}?${params}`;
return fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
}
// Usage
await scheduleTask('/api/send-reminder', { userId: '123' }, { delay: 86400, retry: 3 });
await scheduleTask('/api/daily-digest', {}, { cron: '0|9|*|*|1-5' });
Copy the helper into your next project. You’ll never scope a Redis setup for a client again.
Zeplo adds background jobs to any web project. Delay, retry, and schedule HTTP requests with zero infrastructure. Start free →