Timers
Schedule durable one-shot and recurring timers that fire your worker at a specified time.
Timers
Ploy Timers let you schedule durable timers that call your worker's timer handler at a specified time. Timers survive restarts and are guaranteed to fire at least once. They can be one-shot or recurring with a fixed interval. They're ideal for delayed actions, reminders, subscription renewals, periodic cleanup, and any task that needs to run at a specific time or on a schedule.
Configuration
Add a timer binding in your ploy.yaml:
kind: worker
build: pnpm build
out: dist
timer:
TIMER: defaultThe key (TIMER) is the binding name available in your worker's env. The value (default) is the timer store identifier.
Run ploy types to generate TypeScript types:
import type { TimerBinding } from "@meetploy/types";
export interface Env {
TIMER: TimerBinding;
}Basic Example
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === "/schedule") {
// Schedule a timer to fire in 60 seconds
await env.TIMER.set("reminder-123", Date.now() + 60_000, {
userId: "user_abc",
message: "Your trial is expiring!",
});
return Response.json({ scheduled: true });
}
return new Response("Timer Worker");
},
// Called when a timer fires
async timer(event, env) {
console.log("Timer fired:", event.id);
console.log("Payload:", event.payload);
},
} satisfies Ploy;Timer Binding API
set(id, scheduledTime, options?) - Schedule a Timer
Schedules a new timer on this binding. If a timer with the same ID already exists on the same timer binding, it is replaced.
The third argument can be either a plain payload value (for simple one-shot timers) or an options object with payload and intervalMs fields.
// Simple one-shot timer with a payload
await env.TIMER.set("order-followup", new Date("2025-12-01T09:00:00Z"), {
orderId: "order_123",
});
// One-shot timer without payload
await env.TIMER.set("reminder", Date.now() + 3600_000);
// Recurring timer using options object
await env.TIMER.set("cleanup", Date.now() + 60_000, {
payload: { type: "cache" },
intervalMs: 5 * 60_000, // Every 5 minutes
});id- A unique identifier for the timer. Use this to update or cancel it later.scheduledTime- ADateobject or Unix timestamp in milliseconds.options- Either a plain JSON-serializable payload, or an options object:payload- Optional JSON-serializable data delivered with the timer when it fires.intervalMs- Optional interval in milliseconds. When set, the timer automatically reschedules after each fire.
get(id) - Get Timer Details
Returns the timer details, or null if the timer doesn't exist or has already fired.
const timer = await env.TIMER.get("order-followup");
if (timer) {
console.log(timer.id); // "order-followup"
console.log(timer.scheduledTime); // Unix timestamp (ms)
console.log(timer.payload); // { orderId: "order_123" }
console.log(timer.intervalMs); // undefined for one-shot, number for recurring
}delete(id) - Cancel a Timer
Cancels a scheduled timer so it won't fire. For recurring timers, this stops all future executions.
await env.TIMER.delete("order-followup");Timer Handler
The timer export on your worker is called when a timer fires:
async timer(event, env, ctx) {
console.log({
id: event.id, // The timer identifier
scheduledTime: event.scheduledTime, // When it was scheduled to fire (ms)
payload: event.payload, // The payload you provided
intervalMs: event.intervalMs, // Interval in ms (if recurring)
});
}Timers that throw an error are automatically retried after a short backoff. Make your timer handler idempotent to handle duplicate deliveries.
Recurring Timers
Set intervalMs to create a recurring timer that automatically reschedules after each fire. The next fire time is calculated as previousFireTime + intervalMs, ensuring consistent intervals regardless of handler execution time.
Every 5 Minutes
await env.TIMER.set("metrics-report", Date.now(), {
intervalMs: 5 * 60_000,
});Every Hour with Payload
await env.TIMER.set("hourly-sync", Date.now(), {
payload: { source: "external-api" },
intervalMs: 60 * 60_000,
});Daily Cleanup
// Start tomorrow at midnight, repeat daily
const tomorrow = new Date();
tomorrow.setHours(24, 0, 0, 0);
await env.TIMER.set("daily-cleanup", tomorrow, {
payload: { retentionDays: 30 },
intervalMs: 24 * 60 * 60_000,
});Stop a Recurring Timer
Use delete to cancel a recurring timer and prevent future executions:
await env.TIMER.delete("metrics-report");Common Patterns
Delayed Notifications
async fetch(request, env) {
const { userId, message, delayMinutes } = await request.json();
await env.TIMER.set(
`notify-${userId}-${Date.now()}`,
Date.now() + delayMinutes * 60_000,
{ userId, message },
);
return Response.json({ scheduled: true });
}Subscription Renewal Reminders
// When a user subscribes, schedule a reminder before renewal
await env.TIMER.set(
`renewal-${userId}`,
renewalDate.getTime() - 3 * 24 * 3600_000, // 3 days before
{ userId, plan: "pro" },
);
// If the user cancels, remove the reminder
await env.TIMER.delete(`renewal-${userId}`);Rescheduling Timers
Since set replaces existing timers with the same ID, you can reschedule by calling set again:
// Reschedule to a later time
await env.TIMER.set("task-deadline", newDeadline.getTime(), { taskId: "123" });Multiple Timer Bindings
You can configure multiple timer stores for different use cases:
timer:
TIMER: default
NOTIFICATIONS: notifications// General-purpose timers
await env.TIMER.set("cleanup", Date.now() + 3600_000);
// Notification-specific timers
await env.NOTIFICATIONS.set("reminder", Date.now() + 60_000, {
channel: "email",
});Next Steps
How is this guide?
Last updated on