# Handle Stuck Jobs

**URL:** <https://community.n8n.io/t/handle-stuck-jobs/312815>\
**Category:** Questions\
**Created:** [September 11, 2026, 6:45am UTC](https://community.n8n.io/t/handle-stuck-jobs/312815 "2026-09-11T06:45:28Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![Mary\_Berry](https://dub1.discourse-cdn.com/n8n/user_avatar/community.n8n.io/mary_berry/32/185465_2.png) [@Mary\_Berry](https://community.n8n.io/u/Mary_Berry)\
**Post date:** [September 11, 2026, 6:45am UTC](https://community.n8n.io/t/handle-stuck-jobs/312815/1 "2026-09-11T06:45:28Z")

</div>

HI I’m running several n8n workers that process jobs from PostgreSQL. The workers claim jobs by changing their status from pending to processing.  
The problem is that occasionally a worker crashes or gets restarted after claiming a job. The job stays stuck in processing forever and never gets picked up again.  
I’m thinking about using a locked\_at timestamp so another worker can identify stale jobs:  
SELECT id, payload  
FROM jobs  
WHERE status = ‘processing’  
AND locked\_at \< NOW() - INTERVAL ‘15 minutes’;  
Then I could reset those jobs back to pending.  
I’m mainly interested in a pattern that handles worker crashes safely without creating duplicate processing.

## Describe the problem/error/question

## What is the error message (if any)?

## Please share your workflow

```auto
(Select the nodes on your canvas and use the keyboard shortcuts CMD+C/CTRL+C and CMD+V/CTRL+V to copy and paste the workflow.)

```

## Share the output returned by the last node

## Information on your n8n setup

- **n8n version:**
- **Database (default: SQLite):**
- **n8n EXECUTIONS\_PROCESS setting (default: own, main):**
- **Running n8n via (Docker, npm, n8n cloud, desktop app):**
- **Operating system:**

---

<div class="post-metadata">

**Author:** ![n8n](https://dub1.discourse-cdn.com/n8n/user_avatar/community.n8n.io/n8n/32/196425_2.png) [@n8n](https://community.n8n.io/u/n8n)\
**Post date:** [September 11, 2026, 6:46am UTC](https://community.n8n.io/t/handle-stuck-jobs/312815/2 "2026-09-11T06:46:03Z")

</div>

Hey @Mary_Berry, while you wait for a response, here are some things that might help:

> **Suggested resources**
>
> _Automatically matched to your question._
> 
> **Docs:**
> 
> - [Scheduler](https://docs.n8n.io/deploy/host-n8n/configure-n8n/basic-configuration/use-environment-variables/scheduler#recovery-vars)
> 
> **Forum:**
> 
> - [Design Error Handling and Recovery in Large n8n Production Workflows](https://community.n8n.io/t/304713)
> - [Fault Tolerant n8n Automation for High Volume API Workflows](https://community.n8n.io/t/309504)
> - [PostgreSQL Lock Contention Under High-Concurrency n8n Workloads](https://community.n8n.io/t/304866)

@Niffzy, @Emmas - you’ve helped with similar issues before, can you take a look?

_Automatically suggested by n8n’s community bot. It’s a pilot - [please share feedback here](https://community.n8n.io/t/pilot-forum-helper-bot/304669)._

---

<div class="post-metadata">

**Author:** ![Keira\_Becky](https://dub1.discourse-cdn.com/n8n/user_avatar/community.n8n.io/keira_becky/32/141482_2.png) [@Keira\_Becky](https://community.n8n.io/u/Keira_Becky)\
**Post date:** [September 11, 2026, 6:51am UTC](https://community.n8n.io/t/handle-stuck-jobs/312815/3 "2026-09-11T06:51:29Z")

</div>

@Mary_Berry

use a lease/heartbeat model rather than simply assuming every processing job older than 15 minutes is dead.

I would give each claimed job a locked\_at timestamp and preferably a worker\_id. Workers can periodically update the heartbeat while processing.

For recovery, I would use an atomic update so two workers can’t recover the same job:

UPDATE jobs  
SET status = ‘pending’,  
locked\_at = NULL,  
worker\_id = NULL  
WHERE id = $1  
AND status = ‘processing’  
AND locked\_at \< NOW() - INTERVAL ‘15 minutes’  
RETURNING id;

If the query returns a row, that worker successfully reclaimed it. If it returns nothing, another worker already handled it or the job is no longer eligible.

For long-running jobs, I’d definitely prefer a heartbeat over a fixed timeout:

Claim Job  
↓  
status = processing  
↓  
Update Heartbeat  
↓  
Process Job  
↓  
Mark Completed

Then I’d have a small recovery workflow periodically look for jobs whose heartbeat has expired.

---

<div class="post-metadata">

**Author:** ![Niffzy](https://dub1.discourse-cdn.com/n8n/user_avatar/community.n8n.io/niffzy/32/158156_2.png) [@Niffzy](https://community.n8n.io/u/Niffzy)\
**Post date:** [September 11, 2026, 7:16am UTC](https://community.n8n.io/t/handle-stuck-jobs/312815/4 "2026-09-11T07:16:31Z")

</div>

Hi @Mary_Berry

Make the job ownership explicit instead of relying only on how old the processing status is.

When a worker picks up a job, give it a worker\_id and a lease expiration time:

```sql
UPDATE jobs
SET status = 'processing',
    worker_id = $1,
    lease_until = NOW() + INTERVAL '15 minutes'
WHERE id = $2
  AND status = 'pending'
RETURNING id;

```

While the job is running, the worker can extend its lease:

```sql
UPDATE jobs
SET lease_until = NOW() + INTERVAL '15 minutes'
WHERE id = $1
  AND worker_id = $2
  AND status = 'processing';

```

A recovery process can then look for expired leases and put those jobs back into the queue.

```plaintext
Claim Job
   ↓
Lease Active
   ↓
Process + Renew Lease
   ↓
Complete

```

If the worker crashes, the lease eventually expires and another worker can take over.

I’d also make the actual job processing idempotent and keep a retry counter, so a repeatedly failing job can eventually move to a dead-letter queue instead of retrying forever.

---

<div class="post-metadata">

**Author:** ![Anshul\_Namdev](https://dub1.discourse-cdn.com/n8n/user_avatar/community.n8n.io/anshul_namdev/32/178229_2.png) [@Anshul\_Namdev](https://community.n8n.io/u/Anshul_Namdev)\
**Post date:** [September 11, 2026, 7:37am UTC](https://community.n8n.io/t/handle-stuck-jobs/312815/5 "2026-09-11T07:37:35Z")

</div>

Hi @Mary_Berry  
A lease in your table cannot renew itself the way n8n’s does. n8n holds a 60 second lease on the execution and bumps it every 10 seconds on a timer, but an execution is a straight run through nodes with no background thread, so nothing touches locked\_at while an HTTP Request node is blocked. Renewal only happens where a node runs, which means a Loop Over Items with a renew node inside it. Anything else has to be sized rather than heartbeated.  
Sizing it means reconciling two clocks. The one that decides your worker is dead is n8n’s, not yours, and a worker too busy to renew inside 60 seconds has its execution declared stalled while the row is still marked yours. On 2.x nothing re-delivers that execution, so it is gone and the row waits out your full 15 minutes. Put n8n’s lease above your slowest job, then your job lease above that:

```auto
QUEUE_WORKER_LOCK_DURATION=900000

```

With the order the other way round the reaper hands the job to a second worker while the first is still running it.

> **[Queue mode | Deploy | n8n Docs](https://docs.n8n.io/deploy/host-n8n/configure-n8n/basic-configuration/use-environment-variables/queue-mode)**
>
> Environment variables to configure queue mode on your self-hosted n8n instance.

---

<div class="post-metadata">

**Author:** ![system](https://dub1.discourse-cdn.com/n8n/user_avatar/community.n8n.io/system/32/195824_2.png) [@system](https://community.n8n.io/u/system)\
**Post date:** [September 18, 2026, 7:37am UTC](https://community.n8n.io/t/handle-stuck-jobs/312815/6 "2026-09-18T07:37:49Z")

</div>

Closed on September 18, 2026, 7:37am UTC

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.
