Back to blog

Background jobs and cron: the engine behind your credits system

Renewals, expirations, notifications, and analytics all depend on jobs that run on schedule. How to design them so they don't silently fail or drift.

XLinkedIn

A credits system isn't just APIs and a database. It's also background work:

  • Renew plans at the start of each period
  • Expire old credits
  • Send "low balance" or "renewal" notifications
  • Sync usage to analytics or billing

When background jobs fail

That work runs on a schedule (cron jobs, queues, or serverless triggers). When it fails or drifts, customers see:

  • Wrong balances
  • Missed renewals
  • Duplicate charges

This article covers what has to run in the background, how to make it reliable, and how to avoid the "why didn't it renew?" class of bugs that erode trust in your product.

What actually has to run

Typical background work in a credits and plans system includes:

1. Renewals

At the start of each billing period, grant the plan allowance (e.g., 10,000 credits) to the account. This requires a job that:

  • Finds "accounts whose next renewal time is <= now"
  • Applies the grant
  • Sets the next renewal time

2. Expirations

Credits (or grants) that have an expiry date must be zeroed or marked expired when that date passes. A job:

  • Runs periodically
  • Finds expired grants or balances
  • Applies the expiration logic (e.g., reduce balance, mark grant as expired)

3. Scheduled plan changes

Trial-to-paid, downgrade at period end, or scheduled upgrades. A job (or a generic "scheduled actions" worker):

  • Finds due transitions
  • Applies them

4. Notifications

"Your balance is low," "Your plan renewed," "Your trial ends in 3 days." These can be triggered by a job that:

  • Scans for conditions (e.g., balance < threshold, renewal in 72 hours)
  • Sends emails or in-app messages

5. Sync to billing or analytics

Push usage summaries, grant events, or balance snapshots to a data warehouse or billing provider. Often a daily or hourly job that exports and uploads.

Why background jobs are necessary

None of this can be done synchronously in the request path without making API responses slow and fragile. You need a scheduling and execution layer that runs these jobs at the right time and handles failures.

Cron and its limits

The simplest approach is cron: a script that runs every hour (or day) and does "renewals," "expirations," etc.

The problems with cron

1. Granularity If you run renewals once per day at 00:00 UTC, a customer whose renewal was at 00:01 doesn't get renewed until 24 hours later. You either:

  • Run more frequently (e.g., every 5 minutes)
  • Use a more precise scheduler that runs jobs at per-entity times

2. Single point of failure If the cron host is down or the job crashes, nothing runs until the next invocation.

3. No built-in retry A failed run might leave some accounts renewed and others not. On the next run you need idempotency so you don't double-grant.

4. Drift If the job takes longer than the interval (e.g., 1-hour job, 1-hour cron), you get overlapping runs or skipped runs depending on how you implement it.

When cron is good enough

Cron is fine for "run every night" batch work.

When you need something better

For time-sensitive renewals and expirations you often need something more robust:

  • A queue with per-item scheduling
  • A dedicated scheduler (e.g., job table with "run_at," worker picks due jobs)

Idempotency and retries

Renewal and expiration logic must be idempotent. If the job runs twice for the same account and period (e.g., retry after a timeout), it must not grant twice or expire twice.

Implementing idempotency

For renewals: "Ensure this account has exactly one grant for this period"

  • Use INSERT IF NOT EXISTS
  • Or check a "renewal_done" flag

For expirations: "Mark these grants as expired and adjust balance once"

  • Only process grants not already marked expired

The payoff

With idempotency, you can safely:

  • Retry failed jobs
  • Run catch-up after an outage

Without it, a single retry can double-credit or double-charge.

Partial failure and consistency

A job that processes 10,000 accounts may fail after 3,000. You need a strategy:

Three key practices

1. Process in small batches Commit after each batch, so you can resume from "last processed" or "next batch."

2. Track progress E.g., "renewals processed up to renewal_time X" so the next run knows where to start.

3. Alert on failure So someone notices and can fix or re-run.

What not to do

Don't use one big transaction: A failure at the end rolls back everything and you've made no progress.

Don't skip checkpointing: A failure forces you to re-do work and risks double application if not idempotent.

Design for partial success and resumability

Observability

You need to know: did the job run? how many items did it process? did it error? Simple logging (counts, last processed id or time, errors) plus alerts (job didn't run, error rate spiked) are the minimum. For credits, add business-level checks: "renewals due today: N, renewals applied: M" and alert if M is far from N. That catches logic bugs (e.g. wrong query so no accounts found) as well as infra failures.

Summary and recommendations

Background jobs are the engine of a credits system. To keep them reliable:

Key practices

  • Identify all time-based work (renewals, expirations, scheduled transitions, notifications, sync) and assign each to a job with a clear schedule or trigger
  • Prefer a scheduler or queue with per-item or fine-grained timing for renewals and expirations, so you're not limited to "once per hour" cron
  • Make jobs idempotent so retries and catch-up runs don't double-apply
  • Design for partial failure (batches, checkpoints, resume) and alert on missed runs or anomalies

The operational burden

If you're building this in-house, budget for:

  • Monitoring
  • Runbooks
  • Fixing "why didn't it renew?" incidents

Or delegate it

If you're evaluating a credits API, check that it runs renewals and expirations for you. Then you don't own the cron layer at all.

credits.dev handles plan scheduling, renewals, and credit lifecycle so you don't have to build and maintain the background engine yourself.