Celery task timeout: time_limit and soft_time_limit (tested)

Set @app.task(soft_time_limit=60, time_limit=90): the soft limit raises SoftTimeLimitExceeded inside the task, the hard limit kills it with TimeLimitExceeded. Both are ignored with -P solo; run the default prefork pool.

Celery has two per-task limits: soft_time_limit raises SoftTimeLimitExceeded inside the task so it can clean up, and time_limit (hard) kills the process running the task and fails it with TimeLimitExceeded. They are enforced by the prefork pool (the default).

1. Hard and soft time limits with the prefork pool

$ cat tasks.py
import time
from celery import Celery
from celery.exceptions import SoftTimeLimitExceeded
app = Celery("tasks", broker="redis://127.0.0.1:8964/0", backend="redis://127.0.0.1:8964/1")

@app.task(time_limit=2)
def slow():
    time.sleep(10)

@app.task(soft_time_limit=1, time_limit=5)
def slow_soft():
    try:
        time.sleep(10)
    except SoftTimeLimitExceeded:
        return "cleaned up"
$ celery -A tasks worker -c 1 --loglevel=INFO > worker.log 2>&1 &
$ cat run.py
import time
from tasks import slow, slow_soft
for t in (slow, slow_soft):
    t0 = time.time()
    try:
        print(t.name, "->", t.delay().get(timeout=20), f"({time.time()-t0:.0f}s)")
    except Exception as e:
        print(t.name, "->", type(e).__name__, e, f"({time.time()-t0:.0f}s)")
$ timeout 60 python run.py
tasks.slow -> TimeLimitExceeded TimeLimitExceeded(2,) (2s)
tasks.slow_soft -> cleaned up (1s)
$ grep -E "ERROR|WARNING|succeeded" worker.log | cut -c1-200
[2026-10-23 11:00:02,602: ERROR/MainProcess] Task handler raised error: TimeLimitExceeded(2)
[2026-10-23 11:00:02,602: ERROR/MainProcess] Hard time limit (2s) exceeded for tasks.slow[9516fbc7-4263-400d-82aa-873f4c047dee]
[2026-10-23 11:00:02,709: ERROR/MainProcess] Process 'ForkPoolWorker-1' pid:3012968 exited with 'signal 9 (SIGKILL)'
[2026-10-23 11:00:03,783: WARNING/MainProcess] Soft time limit (1s) exceeded for tasks.slow_soft[e188cdea-0f12-4ba6-a058-dae7eb169b79]
[2026-10-23 11:00:03,792: INFO/ForkPoolWorker-2] Task tasks.slow_soft[e188cdea-0f12-4ba6-a058-dae7eb169b79] succeeded in 1.0103442892432213s: 'cleaned up'

slow failed after 2 s with TimeLimitExceeded(2,) and its process was replaced (SIGKILL). slow_soft caught SoftTimeLimitExceeded after 1 s and returned normally, so the task succeeded. If you do not catch the soft limit exception, the task fails with it.

2. With -P solo the limits are silently ignored

$ cat tasks.py
import time
from celery import Celery
from celery.exceptions import SoftTimeLimitExceeded
app = Celery("tasks", broker="redis://127.0.0.1:8964/0", backend="redis://127.0.0.1:8964/1")

@app.task(time_limit=2)
def slow():
    time.sleep(10)

@app.task(soft_time_limit=1, time_limit=5)
def slow_soft():
    try:
        time.sleep(10)
    except SoftTimeLimitExceeded:
        return "cleaned up"
$ celery -A tasks worker -P solo --loglevel=INFO > worker.log 2>&1 &
$ cat run.py
import time
from tasks import slow, slow_soft
for t in (slow, slow_soft):
    t0 = time.time()
    try:
        print(t.name, "->", t.delay().get(timeout=20), f"({time.time()-t0:.0f}s)")
    except Exception as e:
        print(t.name, "->", type(e).__name__, e, f"({time.time()-t0:.0f}s)")
$ timeout 60 python run.py
tasks.slow -> None (10s)
tasks.slow_soft -> None (10s)

The same tasks under -P solo both ran the full 10 seconds and succeeded; the worker log had no time limit message at all. If your limits "do nothing", check the pool (-P solo, and Windows setups that need it). Use the default prefork pool, or put timeouts inside the task (for example a timeout= on the network call that hangs).

Timeouts on the caller side

result.get(timeout=20) only stops waiting: it raises celery.exceptions.TimeoutError in the caller and the task keeps running on the worker. Global defaults are task_time_limit and task_soft_time_limit.

Related: Celery chord error handling, Celery MaxRetriesExceededError, Celery link_error errback.

Frequently asked questions

What is the difference between time_limit and soft_time_limit in Celery?

soft_time_limit raises SoftTimeLimitExceeded inside the task so it can clean up; time_limit kills the process and the task fails with TimeLimitExceeded.

Why is my Celery time_limit not working?

The solo pool (-P solo) does not enforce time limits; in our test the task ran past both limits and succeeded.

Does AsyncResult.get(timeout=) stop the task?

No. It raises celery.exceptions.TimeoutError in the caller; the task keeps running on the worker.

Written with AI assistance. Measured on 2026-10-23 with Celery 5.6.3 (kombu 5.6.2, redis-py 8.1.0) on Python 3.13.5, Redis 7 (Docker) as broker and backend. Each block was run verbatim and the output pasted unedited. PlainHeaders is our extension; Celery and Redis are free.