Technical guide · States, error messages and the fix

Stuck Odoo Queues (queue.job)

Jobs stuck in failed or stuck in enqueued forever, and marketplace synchronizations that stop running. Here's what each queue_job (OCA) state means, why the queue gets blocked, and how to diagnose and resume it — with the real error messages.

What queue_job is, and what actually breaks

queue_job is the OCA module that runs background tasks in Odoo: instead of blocking the user while an order syncs with a marketplace, the method is decorated with @job (or called with .with_delay()) and a queue.job record is queued for a separate process — the jobrunner — to pick up and execute. Many heavy connectors use it (including the OCA connector family) to parallelize stock, price, order and tracking synchronizations.

The problem that reaches production is almost always one of two things: the queue doesn't move (jobs stay in enqueued or pending and nothing runs) or jobs keep failing (state failed, and Amazon, Shopify or Mirakl orders never come in). The two have different causes and are diagnosed in the same place.

Job states (and what getting stuck in each one means)

State What it means Getting stuck here is a symptom of…
pending Queued, waiting for a slot in its channel. Saturated channel, runner down, or channel capacity set to 0.
enqueued Reserved by the runner, about to run. The runner died right after reserving it, or there's no worker to pick it up.
started Running right now. Worker restarted mid-job (deploy, OOM, timeout) and the job was orphaned.
done Finished successfully. Nothing: healthy state.
failed Raised an exception and exhausted its retries. A real business or API error: read exc_info.

A healthy job moves through pending → enqueued → started → done in seconds. Where it gets stuck tells you where to look: if it never gets past pending or enqueued, the problem is the runner or the channels; if it reaches failed, the problem is inside the job (the traceback tells the story).

1. The queue doesn't move: jobs stuck in enqueued or pending forever

The jobrunner isn't running

This is cause number one. The runner that drains the queues only starts if Odoo is in multiprocess mode. If you launch the server with --workers=0 (typical in development, and sometimes it slips into production), the runner never starts, and jobs pile up without ever running. You can confirm it because the startup log is missing the runner's line:

INFO ... odoo.addons.queue_job.jobrunner.runner: starting

Fix: start with --workers=2 or more, and add the section to odoo.conf so the runner knows which database and channels to serve:

[options]
workers = 4
server_wide_modules = web,queue_job
[queue_job]
channels = root:2

queue_job isn't in server_wide_modules

The jobrunner is a thread loaded when the server starts, not per database. If queue_job is installed in the database but doesn't appear in server_wide_modules (or as --load=web,queue_job), the module works fine in the UI but the runner never starts. Result: jobs enter pending and stay there.

Fix: add queue_job to server_wide_modules (or to the --load flag) and restart. On Odoo.sh and other managed PaaS this usually means touching the server configuration, not just installing the module.

Saturated channel or misconfigured capacity

Each job runs in a channel with a maximum capacity of parallel jobs (root:2 = two at once). If a long job (a bulk catalog import) occupies the channel and doesn't finish, everything else queues up behind it in pending. And if you accidentally set a channel to capacity 0, that channel never runs anything.

Fix: split traffic across channels (for example root:2,root.import:1) so a heavy import doesn't block fast stock and order syncs. In Queue Jobs, group by channel to see what's eating the capacity.

Orphaned jobs in enqueued / started after a restart

A deploy, a worker restart, an out of memory event or a timeout (limit_time_real) kills the process that was running a job. The record freezes in started or enqueued, and since it was already reserved, the runner won't pick it up again on its own.

Fix: in Settings → Technical → Queue Jobs, filter by started/enqueued state with an old date, select them and use Requeue (back to pending). The module also ships the Queue Job: Requeue Dead Jobs cron and an autovacuum that cleans up/marks as failed jobs that have been frozen too long — check that both crons are active.

2. Jobs in failed: the marketplace sync doesn't come in

Here the queue does move, but the jobs blow up. The record in failed stores the full traceback in the exc_info field: always read it before retrying. These are the typical failures in marketplace synchronizations.

Expired credentials or token

The most common one: the marketplace API token expired or the credentials were revoked. The job fails on the call and, after exhausting its retries, ends up in failed. It usually looks something like this:

requests.exceptions.HTTPError: 401 Client Error: Unauthorized for url

Fix: renew the token or the keys in the connector's configuration and then Requeue. Retrying before fixing the credential just repeats the 401.

API rate limit (429)

Amazon SP-API, Shopify and Mirakl limit requests per minute. If you fire many jobs in parallel (a channel with a high capacity), you start getting them rejected:

429 Too Many Requests / QuotaExceeded: You exceeded your quota for the requested resource

Fix: lower the capacity of the channel hitting that API and let queue_job retry with a retry_pattern (backoff). The correct behavior is for the job to turn a 429 into a delayed retry, not a final failure. If your connector doesn't do that, that's a connector bug.

Database locks (SerializationFailure)

If several jobs touch the same records at once (for example the stock of a product sold across several channels), PostgreSQL aborts one of the transactions on a serialization conflict:

psycopg2.errors.SerializationFailure: could not serialize access due to concurrent update

Fix: queue_job automatically retries this error (RetryableJobError), so it usually resolves itself on the next attempt. If it persists, reduce the channel's parallelism or use an identity_key to serialize jobs that touch the same resource.

An order field the API rejects

An unmapped country, a shipping method that doesn't exist, a SKU missing in Odoo, or an amount that doesn't add up make the API return a 400/422 with a validation message. This failure won't fix itself by retrying: the data is still wrong.

Fix: read the exact message in exc_info, fix the mapping or the master data (carrier, country, product), and only then retry. Marking the job as done without fixing the data leaves that order out of sync and off the radar.

Step-by-step diagnosis (5 minutes)

Real queue_job or a plain cron? The honest take

queue_job is excellent, free (OCA, LGPL) and the right choice when you have volume and need parallelism and per-job retries. But it adds one more piece to maintain: the jobrunner, its channels, and one more place where something can get stuck. Not every connector needs it.

Criterion queue_job (OCA) Plain cron (ir.cron)
Parallelism Yes, by channels; great for volume spikes. Sequential; one batch after another.
Retries Per job, with configurable backoff. Manual or hand-rolled in the cron's code.
Traceability One record per job with its own trace. Only what you log yourself.
Operations Needs a runner, channels and server_wide_modules. Zero extra infrastructure: already built into Odoo.
Best for Large catalogs, many channels, spikes. Small/medium catalogs, simple operations.

That's why several FlexigoTech connectors — Amazon, Shopify, Mirakl and ManoMano — sync stock, orders and tracking with plain crons and don't force you to set up queue_job just to get started. Fewer moving parts, fewer queues to get stuck. If your volume calls for it, queue_job is still there; but it isn't a requirement for selling on marketplaces from Odoo.

Queue stuck and orders not coming in?

At FlexigoTech (Barcelona) we diagnose why your queue_job isn't moving, reactivate orphaned jobs, fix the root cause of your failed jobs, and, if it makes sense, simplify your sync to more robust crons. Engineers, no middlemen.

See custom Odoo development

FAQ

Why do jobs stay in enqueued forever?

enqueued means the job was reserved but the process meant to run it never did. It's almost always that the jobrunner isn't running: Odoo started with --workers=0 doesn't start the runner, queue_job is missing from server_wide_modules, or the [queue_job] section with its channels is missing from odoo.conf. A job in enqueued has no automatic retry: you need to Requeue it or restart the runner.

What's the difference between pending, enqueued, started, done and failed?

pending: queued, waiting for a slot in its channel. enqueued: reserved by the runner, about to run. started: running. done: finished successfully. failed: raised an exception and exhausted its retries. A healthy job moves through pending → enqueued → started → done in seconds. If it stays in enqueued or started, the problem is the runner or a worker that died mid-job.

How do I retry a marketplace job that failed?

In Settings → Technical → Queue Jobs you open the failed record, read exc_info (the full traceback) and click Requeue / Set to pending. Before retrying, fix the cause: expired credentials (401), a rate limit (429), or an order field the API rejects (400/422). Retrying without fixing the cause just repeats the failure.

Do I need queue_job for marketplace connectors?

Not always. Many connectors, including several of FlexigoTech's, sync stock, orders and tracking with plain crons (ir.cron) without depending on queue_job, which simplifies operations. queue_job adds parallelism, per-job retries and traceability, very useful at high volumes, but it's one more piece to maintain. For small catalogs, a well-built cron is usually simpler and less fragile.

Next step

Custom Odoo development Free test: which connector do you need?