---
title: "Callback vs Promise vs Webhook: Where Does Your Async Result Actually Come Back?"
description: "Callback, promise, and webhook get treated like three flavors of the same thing — they aren't. The real difference is WHERE the result lives. Callback and promise hand a result back to the same program in memory in milliseconds; a webhook is another machine calling you back over the network, seconds to minutes later. Here's the mental model, the trade-offs, code for each, and a clear rule for which to reach for — including what it means for async LLM jobs."
keywords: "callback vs promise vs webhook, async JavaScript, promises, async await, webhooks, callback hell, inversion of control, HTTP callback, event driven, backend system design, Node.js async, API design, webhook vs polling, async LLM jobs, batch API"
created_at: "2026-07-04T15:00:00"
post_type: "anonym_post"
content_type: "technical_article"
---

![Banner](./banner.webp)

<div style="display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; padding: 1rem 1.25rem; margin: 1.5rem 0 2rem; background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); border: 1px solid #e2e8f0; border-radius: 12px;">
  <span style="font-size: 0.95rem; font-weight: 600; color: #475569; margin-right: 0.25rem;">Prefer to watch?</span>
  <a href="https://youtube.com/shorts/5FGfXyGrVjQ" target="_blank" rel="noopener noreferrer" style="display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.55rem 1rem; border-radius: 8px; background: #ff0000; color: #ffffff; font-size: 0.875rem; font-weight: 600; text-decoration: none;">▶ Callback vs Promise vs Webhook in 90 seconds</a>
  <a href="https://t.me/SoftwareEngineerBlog" target="_blank" rel="noopener noreferrer" style="display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.55rem 1rem; border-radius: 8px; background: #229ed9; color: #ffffff; font-size: 0.875rem; font-weight: 600; text-decoration: none;">✈ Telegram</a>
</div>

You fired off an async request. The work takes a while — a file read, a slow API, a payment that clears minutes later. So here's the real question: **how does the answer come back to you?**

There are three classic answers — **callback**, **promise**, and **webhook** — and they get argued about like they're three syntaxes for the same idea. They aren't. The difference that actually matters isn't `.then()` vs a function argument. It's **where the result lives**, and how far it has to travel to reach you.

The one-line mental model:

- **Callback + promise** = the **same program** handing a result back to *itself*, in memory, in milliseconds.
- **Webhook** = **another machine** calling *you* back over the network, seconds to minutes later.

It's **in-process** vs **across the network**. Everything else follows from that.

---

## Callback: hand over a function to be called later

The oldest answer. You call some async operation and pass it a function — "here, **call me back** when you're done."

```javascript
readFile("data.json", (err, data) => {
  // this runs later, when the read finishes
  if (err) return handle(err);
  process(data);
});
```

Simple for a single step. The trouble starts when you chain them — each result feeds the next call, and you drift rightward into the **pyramid of doom**:

```javascript
getUser(id, (err, user) => {
  getOrders(user, (err, orders) => {
    getInvoice(orders[0], (err, invoice) => {
      // ...callback hell
    });
  });
});
```

That's **callback hell** — deep nesting that's hard to read and hard to error-handle. And there's a subtler cost: you handed *your* function to *someone else's* code, so **they** decide when (and whether, and how many times) it runs. That loss of control has a name: **inversion of control**.

---

## Promise: an object standing in for a value that isn't ready yet

A promise flips the ergonomics. Instead of giving the async operation your function, **it gives you an object back** — a placeholder for a value that will exist later. You attach what happens next to *that*:

```javascript
fetchUser(id)
  .then(user => fetchOrders(user))
  .then(orders => fetchInvoice(orders[0]))
  .catch(handleError);       // one place for failures
```

Or, with `async/await`, it reads like plain sequential code:

```javascript
const user    = await fetchUser(id);
const orders  = await fetchOrders(user);
const invoice = await fetchInvoice(orders[0]);
```

Flat, readable, no nesting pyramid, and one clean path for rejections. This is the **modern in-process default** — reach for it first. Just remember what it *isn't*: a promise still runs inside your one program. It doesn't survive a restart, it doesn't cross machines, and **you're still the one who handles the reject**.

---

## Webhook: the result comes back over the network

Now the model changes completely. With a webhook, the result **doesn't come back in your memory at all**. You don't hold a function or an object waiting — you give the other service a **public URL**, and when its work is done, **it makes an HTTP POST to *your* endpoint**.

```http
POST /webhooks/payments HTTP/1.1
Host: your-app.com
X-Signature: t=1720099200,v1=8f3a...   ← verify this!

{ "event": "payment.succeeded", "id": "pay_42", "amount": 1999 }
```

```python
@app.post("/webhooks/payments")
def payment_hook(req):
    if not verify_signature(req):        # anyone can POST you — check it
        return 401
    mark_paid(req.json["id"])
    return 200                           # 2xx or they retry
```

The classic case: a **payment that clears minutes later**, an email that bounces, a video that finishes transcoding. Your original request already returned; the *real* answer arrives on its own schedule, from a different machine.

That power has a price the in-process options never pay:

- **A public URL** the other service can reach.
- **Retries + idempotency** — networks fail, so senders retry; your handler must tolerate the same event twice.
- **Signature verification** — your endpoint is public, so *anyone* could POST it. You must prove the call is genuine.

---

## The reframe: this is exactly how you talk to slow AI jobs

Here's where the same three shapes show up in a very modern place — **calling an LLM**. Generation is async and often *slow*, so you face the identical "how does the result come back?" decision:

- **In-process await** — for a normal chat completion you just `await` the call. That's the **promise** model: same program, holds the connection, result comes back in memory. Fine for a few seconds of latency.
- **Streaming (token by token)** — instead of one final value, the server pushes tokens as they're generated. This is the **callback** shape wearing new clothes: you register an `on_token` handler and the library **calls you back** for each chunk. Great for perceived latency (first token fast), but you've handed over control of when your code runs.
- **Batch / async jobs → webhook** — submit a large **batch API** job or a long fine-tune and the result may take **minutes to hours**. You do *not* hold a connection open for an hour. You either **poll** for status, or — better — register a **webhook** so the provider POSTs your endpoint the moment the job finishes. Same public-URL-plus-retries-plus-verification contract as the payment example.

So the "in-process vs across the network" split isn't academic trivia — it's the exact reason a chat call is an `await` but a batch job is a webhook. **Latency dictates the shape.** Sub-second → hold it in memory (promise). Token stream → callback. Long-running or cross-service → webhook.

---

## The differences side by side

<table>
  <thead><tr><th></th><th>Callback</th><th>Promise</th><th>Webhook</th></tr></thead>
  <tbody>
    <tr><td>Where the result lives</td><td>In memory</td><td>In memory</td><td>Over the network</td></tr>
    <tr><td>Scope</td><td>Same program</td><td>Same program</td><td>Another machine</td></tr>
    <tr><td>Latency</td><td>Milliseconds</td><td>Milliseconds</td><td>Seconds–minutes</td></tr>
    <tr><td>Ergonomics</td><td>Nesting → callback hell</td><td>Flat `.then()` / `await`</td><td>HTTP endpoint you host</td></tr>
    <tr><td>Control</td><td>Inverted (they call you)</td><td>You drive it</td><td>Inverted + public</td></tr>
    <tr><td>The cost</td><td>Deep nesting, IoC</td><td>You handle rejects</td><td>Public URL, retries, signature</td></tr>
    <tr><td>Survives a restart?</td><td>No</td><td>No</td><td>Yes (it re-POSTs / retries)</td></tr>
  </tbody>
</table>

---

## Which should you reach for?

- **Same program, async work?** → **promise / async-await**. It's the modern default. Use plain **callbacks** only for old, event-style APIs (event emitters, streaming chunks) where a callback is genuinely the right shape.
- **Another service, or a long-running job that outlives your request?** → **webhook**. Anything that finishes seconds-to-minutes later on someone else's machine — payments, transcodes, batch AI jobs — should call *you* back over the network, not make you hold a connection open.

The part most "vs" arguments miss: these aren't competitors. A real system uses **all three** — `await` for the fast in-process calls, a callback for the streaming ones, and a webhook for the slow cross-service ones. The skill isn't picking a favorite; it's recognizing **where the result lives** and reaching for the shape that matches.

> Want the 90-second visual version? [Watch the reel.](https://youtube.com/shorts/5FGfXyGrVjQ)
