Scheduled job that calls a model on a timer
A scheduled job that calls a model whether or not anyone is using the app.
Why it costs money
A scheduled job pays on every tick, whether or not there is new data or anyone reads the output. It never appears in request traces, so it is easy to forget once it ships.
The cost is per-run price times frequency times the number of things the job loops over. As an example, a job that runs every 15 minutes makes 2,880 runs a month. If each run summarizes updates for every user, 1,000 users means 2.88 million model calls a month, most of them for users who were not active.
Example
Flagged
import cron from "node-cron";
import OpenAI from "openai";
const openai = new OpenAI();
cron.schedule("*/15 * * * *", async () => {
const users = await db.user.findMany();
for (const user of users) {
const updates = await db.update.findMany({ where: { userId: user.id } });
const res = await openai.chat.completions.create({
model: "gpt-4o",
max_tokens: 400,
messages: [{ role: "user", content: "Summarize these updates: " + JSON.stringify(updates) }],
});
await db.digest.create({ data: { userId: user.id, text: res.choices[0].message.content ?? "" } });
}
});Fixed
import cron from "node-cron";
import OpenAI from "openai";
const openai = new OpenAI();
const HOUR = 60 * 60 * 1000;
cron.schedule("0 * * * *", async () => {
const since = new Date(Date.now() - HOUR);
const users = await db.user.findMany({ where: { lastSeenAt: { gte: new Date(Date.now() - 7 * 24 * HOUR) } } });
for (const user of users) {
const updates = await db.update.findMany({ where: { userId: user.id, createdAt: { gte: since } } });
if (updates.length === 0) continue;
const res = await openai.chat.completions.create({
model: "gpt-4o-mini",
max_tokens: 400,
messages: [{ role: "user", content: "Summarize these updates: " + JSON.stringify(updates) }],
});
await db.digest.create({ data: { userId: user.id, text: res.choices[0].message.content ?? "" } });
}
});How to fix it
Confirm the job needs to run that often, and skip the model call when nothing has changed since the last run. Limit it to active users, and cap how many scheduled jobs a single user can create if they are user-defined.
Give the job its own per-run budget, use a cheaper model where quality allows, and consider the provider's Batch API, since scheduled work rarely needs an immediate answer.
What the audit detects
- LLM calls inside scheduled code: cron-style schedulers (node-cron, APScheduler add_job, Celery periodic tasks), setInterval, and while True loops that sleep between runs.
- Findings are grouped per file. When the model is known, the report estimates cost per run and per month at an hourly schedule.
- It cannot see how often the job actually runs in production or whether it is limited elsewhere. Those numbers are for you to confirm.