When MCP goes stateless on July 28, the held-open stream goes with it. A tool call that takes ten minutes can no longer report back over an SSE connection that stays alive for the duration, because there's no session to keep alive. The replacement is the Tasks extension, and the most common mistake founders are about to make with it is treating it as a job queue. It isn't one. It's a way for a long-running tool to say "still working, check back" — and nothing more.
That distinction is the whole decision. If you understand exactly what Tasks does and doesn't define, you'll know in about thirty seconds whether it's enough for your feature or whether you need real infrastructure behind it.
What Tasks actually is#
Here's the entire mechanism. A server can answer a tools/call with a task handle instead of a result. The server generates the task ID — the client doesn't mint it — and the client then drives the work with three methods: tasks/get to poll, tasks/update to supply requested input, and tasks/cancel to stop it. There is no fourth method to list your tasks: tasks/list was removed because, with no session, there's no safe way to scope "your" tasks. You hold the ID the server gave you, or the task is gone.
The work moves through a five-state machine: working, then either a terminal completed / failed / cancelled, or a detour through input_required when the task needs something from you. That last state returns an inputRequests map; you answer with tasks/update and the task returns to working. Cancellation is cooperative — you ask, the server winds down. Terminal states are immutable: once a task is failed, it stays failed.
Read that list again and notice what's missing. There is no retry. No backoff. No persistence guarantee. No dead-letter path. No schedule. Tasks is a reporting surface, not an execution engine.
That's not a criticism of the spec — it's the design. The 2026-07-28 release deliberately made MCP smaller, pushing responsibility back to the host and server. Tasks defines how a client observes long work. It leaves how that work survives entirely to you.
What a real queue gives you that Tasks doesn't#
Everything a queue is famous for is exactly the list Tasks stays silent on:
- Automatic retries and backoff. Celery gives you
autoretry_for, amax_retries(default 3, orNonefor forever), andretry_backoff. BullMQ hasattemptswith fixed, exponential, or custom backoff. Temporal retries activities by default under a declarative Retry Policy. Tasks has none of this — a failed task just sits atfailed. - Durability across a restart. SQS retains messages up to 14 days; Temporal persists every state transition to Event History so a workflow survives a crash mid-execution. An MCP task lives only as long as the server process choosing to remember it.
- A dead-letter path. SQS moves poison messages to a DLQ after
maxReceiveCounttries; BullMQ keeps a failed set. Tasks has nowhere for work that keeps dying to go. - Scheduling and fan-out. Celery Beat runs cron; BullMQ has repeatable jobs and Flows for parent-child fan-in; Temporal spawns child workflows. Tasks models one call, not a graph of them.
None of that means Tasks is underbuilt. It means the two things solve different problems, and stacking them is the intended shape.
The pattern that actually works#
Don't choose between them. Let Tasks be the façade and put a queue behind it.
Your MCP server receives tools/call, enqueues the real work onto Celery / BullMQ / SQS / Temporal, and immediately returns a task handle keyed to the queue's job ID. The queue owns durability, retries, and the dead-letter path. The MCP client sees only a clean tasks/get poll and a five-state machine. When the queue's job finishes, your tasks/get handler reads its status and reports completed. The protocol stays thin; the reliability lives where reliability tools already are.
Reach for Tasks alone — no queue — only when all of these are true: the work is a single call the client is actively waiting on, it runs minutes to hours (not days), one worker handles it, and it's acceptable for the task to vanish if the server restarts. A user-triggered report or a long export fits. A billing run, a nightly sync, or anything a customer's money depends on does not.
The stateless spec didn't take your background jobs away. It just stopped pretending the protocol was the thing running them. Once you internalize that Tasks is a window and not a worker, the architecture picks itself: the window goes in the protocol, the worker goes in your queue, and the July 28 migration stops being scary and starts being a refactor you can name.



