Module 7 / Background jobs

Module 7 · lesson 1 of 6 · Read 8 min

Now or later?

Deciding what has to happen before the response.

Right now, when a customer taps "Place order" in Tiffin, the server does everything before it answers. It checks the cart, charges the card, saves the order, emails a receipt, sends an SMS and updates the restaurant's stats. Only then does the spinner stop.

That worked when Tiffin was small. But one of those steps is slow, and another fails now and then, and the customer pays for both. In this lesson you'll learn to ask one question about every step in a request: does this have to happen before the response?

What happens when you tap "Place order"

Here is Tiffin's checkout today, with typical timings measured on a busy evening:

Step Typical time Slowest 1% (p99)
Validate cart and prices 5 ms 20 ms
Charge the card (payment provider) 800 ms 2.5 s
Save the order 10 ms 40 ms
Email the receipt (email provider) 1.5 s 8 s
Send an SMS ("Order placed!") 600 ms 3 s
Add loyalty points 15 ms 50 ms
Update the restaurant's daily stats 20 ms 80 ms

Add up the typical column and the customer waits about 3 seconds. On a bad request, when the email and SMS providers are both slow, they wait over 13 seconds. Most of that time is spent on things the customer doesn't need yet. Nobody reads their receipt email while the checkout spinner is still turning.

Customer app
→
Servervalidate, charge, save
→
Email provider1.5 s
→
SMS provider0.6 s
Every step runs before the customer gets an answer, so the slowest provider sets the pace.

The question to ask about each step

For every step in a request, ask two things:

  1. Does the customer need the result of this step to see the answer?
  2. If this step fails, should the whole request fail?

If both answers are "no", the step can happen later. Let's go through checkout:

Step Needs it now? If it fails, fail the order? Decision
Validate cart Yes, wrong prices mean a wrong charge Yes Now
Charge the card Yes, "order placed" means "you've paid" Yes Now
Save the order Yes, it's the order Yes Now
Email receipt No No, send it later Later
SMS No, the app already shows the order No Later
Loyalty points No, nobody checks within a second No Later
Restaurant stats No No Later

Three steps stay. Four move out. Checkout drops from about 3 seconds to about 0.8 seconds, almost all of it the card charge.

Work that must finish before you respond is synchronous. Work you promise to do soon after, without making the user wait, is asynchronous.

Notice "promise". Moving work out of the request doesn't mean it becomes optional. The receipt must still go out. You've changed when, not whether.

Slow and broken dependencies spread

Speed is the obvious win. The less obvious one is that you stop borrowing other systems' problems.

When the email provider is part of checkout, its bad day is your bad day. If it takes 8 seconds, checkout takes 8 seconds. If it is down, you have two bad choices: fail the order (and lose the sale) or ignore the error (and lose the receipt, silently).

There's also simple maths here. Say checkout depends on five outside services and each is up 99.9% of the time. If all five must work for checkout to work:

0.999 × 0.999 × 0.999 × 0.999 × 0.999 ≈ 0.995

That's 99.5%, or about 3.6 hours of failed checkouts a month (0.5% of 720 hours), even though every single provider kept its promise. Each synchronous dependency you remove takes its downtime out of that sum.

Slow steps also hold resources. A server thread or connection waiting 8 seconds on an email provider isn't serving anyone else. At dinner time, a slow provider can fill every worker slot on your app servers and make the whole app slow, not just checkout.

What "later" costs you

Asynchronous work isn't free. Before you move a step out, be honest about what you're signing up for:

  • Somewhere to keep the work. "Send this receipt soon" has to be written down in a place that survives a server restart. That's a queue, covered in the next lesson.
  • Something to do the work. A separate process, a worker, picks it up. That's another thing to deploy, monitor and scale.
  • Failures nobody sees. In a synchronous request, an error shows up on the customer's screen and in your error rate. In a background job, the error happens quietly 30 seconds later. You need retries, alerts and somewhere for failed jobs to go.
  • A short gap where things disagree. For a few seconds the order exists but the loyalty points don't. Usually fine. Sometimes it confuses users ("where are my points?"), and you need to design the UI around it.
  • Jobs that run twice. Most queues will occasionally deliver the same job twice. A duplicate receipt is annoying. A duplicate payout is expensive. We'll fix this in "Jobs that can run twice".

That's real machinery. So "later" should be a decision about each step, not a habit of throwing everything into the background.

The hard case: charging the card

The card charge is the slowest step left, at 800 ms. Could it be asynchronous too?

Some apps do exactly that. You tap "Pay", the app says "Payment processing", and a minute later you get "Order confirmed" or "Payment failed". UPI payments in India often work like this, because the bank's confirmation can take a while.

For Tiffin, think about what happens next. The kitchen starts cooking when the order arrives. If the payment then fails, a restaurant has cooked a biryani nobody paid for. So the customer has to know "placed or not" before the order reaches the kitchen. Keeping the charge synchronous is the simpler, safer choice here: 800 ms of waiting buys a clear answer.

Keeping the charge and the order in step when one of them fails partway is its own hard problem. We'll take it on properly in Module 16.

Slow work that a user asks for

Not every background job hides behind a fast response. Sometimes the user asks for the slow thing directly:

  • A restaurant uploads 40 menu photos that need resizing into four sizes each.
  • Finance downloads a CSV of every order last quarter.
  • A restaurant asks for a PDF statement of last month's earnings.

Here you can't pretend it's done. The usual pattern is to accept the request, start a job, and give the user a way to check on it:

POST /reports/monthly-statement    →  202 Accepted
                                      { "job_id": "rep_8812", "status": "queued" }

GET  /jobs/rep_8812                →  200 OK
                                      { "status": "done", "url": "https://..." }

202 Accepted means "I've got your request and I'll work on it", which is exactly the truth. The client polls the job's status or gets told when it's ready (Module 6 showed how a server can push that news).

Clock-driven work

A third kind of background work isn't triggered by a request at all. Paying restaurants every Monday, cancelling orders the kitchen never accepted, clearing out abandoned carts. These are driven by the clock. They share most of the machinery with request-driven jobs, plus a few problems of their own, covered in "Scheduled jobs".

Sort the rider app's steps

When a rider taps "Delivered", the server: marks the order delivered, tells the customer's app, asks the customer for a rating (push notification), adds the delivery fee to the rider's earnings, and recalculates the rider's acceptance score for the week. Which steps must happen before the rider's app gets its response?

Show one answer

Now: mark the order delivered. The rider's app needs to know it worked before it shows the next job, and if this fails the rider should tap again.

Later: everything else. The customer update, rating push and score recalculation don't change what the rider sees. Earnings can also be later, but the rider app probably shows "earnings today", so the job should run within seconds, and the UI can show the new delivery as "pending" until it does.

A good follow-up question for the product team: is it acceptable for earnings to lag by a minute during an outage? If not, crediting earnings might belong in the same database transaction as marking it delivered, since it's a fast local write anyway.

What to remember

  1. For each step in a request, ask: does the user need its result now, and should its failure fail the request? Two "no"s mean it can happen later.
  2. Moving work out of the request cuts latency and stops slow or broken providers from becoming your outage.
  3. Each synchronous dependency multiplies into your availability. Five services at 99.9% give you about 99.5%.
  4. Asynchronous work needs a queue, workers, retries, monitoring, and jobs that are safe to run twice. Don't move work out by habit.
  5. Keep a step synchronous when the next thing depends on its answer, like charging the card before the kitchen cooks.
  6. For slow work the user asks for, return 202 Accepted with a job ID and let them check back.