Programování

Fronty úloh na pozadí: BullMQ v Node.js a Celery v Pythonu

Proč posílat e-maily a generovat PDF mimo HTTP požadavek. Praktické nastavení BullMQ a Celery s Redisem, opakování s backoffem, idempotence a provoz workerů.

12. 10. 2026 · 4 min čtení

Uživatel klikne na „Objednat“ a čeká, než server odešle e-mail, vygeneruje fakturu a zavolá účetní systém. Pokud jedna z těch služeb zrovna zlobí, požadavek spadne a objednávka je v nejasném stavu. Fronta úloh tohle řeší: HTTP požadavek jen zapíše „co je potřeba udělat“ a samostatný worker to zpracuje na pozadí, s opakováním při chybě.

Kdy frontu použít

  • Odesílání e-mailů a notifikací.
  • Generování PDF, zpracování obrázků a videí, importy CSV.
  • Volání pomalých nebo nespolehlivých externích API.
  • Zpracování webhooků (rychle potvrdit, práci udělat později).
  • Plánované úlohy (denní reporty, úklid).

Pravidlo: co trvá déle než pár stovek milisekund nebo závisí na cizí službě, patří do fronty.

Architektura

[ Web / API ] --přidá úlohu--> [ Redis (broker) ] <--vyzvedne-- [ Worker 1..N ]

Web a workery jsou samostatné procesy, často na jiných serverech. Workerů můžete spustit víc a škálovat je nezávisle na webu.

BullMQ (Node.js)

BullMQ je knihovna nad Redisem, nástupce původního Bull.

npm install bullmq

Přidání úlohy

// queue.js
import { Queue } from "bullmq";

export const connection = { host: "127.0.0.1", port: 6379 };
export const emailQueue = new Queue("email", { connection });

// v route handleru po vytvoření objednávky:
await emailQueue.add(
  "order-confirmation",
  { orderId: order.id },
  {
    jobId: `order-confirmation-${order.id}`, // deduplikace
    attempts: 5,
    backoff: { type: "exponential", delay: 2000 },
    removeOnComplete: 1000,
    removeOnFail: 5000,
  }
);
  • jobId – úloha se stejným ID se znovu nepřidá, dokud existuje (ochrana proti dvojímu kliknutí).
  • attempts + backoff – při chybě se úloha zopakuje po 2, 4, 8… sekundách.
  • removeOnComplete / removeOnFail – kolik hotových či neúspěšných úloh si Redis drží. Bez toho paměť Redisu jen roste.

Do dat úlohy dávejte ID, ne celé objekty. Worker si aktuální data načte z databáze.

Worker

// worker.js
import { Worker } from "bullmq";
import { connection } from "./queue.js";

const worker = new Worker(
  "email",
  async (job) => {
    const order = await db.orders.find(job.data.orderId);
    if (order.confirmationSentAt) return; // už odesláno – idempotence
    await sendEmail(order.customerEmail, "Potvrzení objednávky", render(order));
    await db.orders.markConfirmationSent(order.id);
  },
  { connection, concurrency: 5 }
);

worker.on("failed", (job, err) => {
  console.error(`Úloha ${job?.id} selhala (pokus ${job?.attemptsMade}):`, err.message);
});

process.on("SIGTERM", async () => {
  await worker.close(); // dokončí rozpracované úlohy
  process.exit(0);
});

Pokud předáváte vlastní instanci ioredis místo konfiguračního objektu, musí mít pro Worker nastaveno maxRetriesPerRequest: null, jinak BullMQ vypíše chybu.

Celery (Python)

Celery je zavedený standard v Pythonu, často spolu s Djangem. Jako broker může použít Redis nebo RabbitMQ.

pip install "celery[redis]"
# tasks.py
from celery import Celery

app = Celery("tasks", broker="redis://localhost:6379/0")
app.conf.update(
    task_acks_late=True,
    worker_prefetch_multiplier=1,
    task_time_limit=300,
)

@app.task(
    bind=True,
    autoretry_for=(ConnectionError, TimeoutError),
    retry_backoff=True,      # exponenciální čekání
    retry_jitter=True,       # náhodný rozptyl
    max_retries=5,
)
def send_order_confirmation(self, order_id: int) -> None:
    order = Order.objects.get(pk=order_id)
    if order.confirmation_sent_at:
        return
    send_email(order.customer_email, render(order))
    order.mark_confirmation_sent()

Zařazení úlohy:

send_order_confirmation.delay(order.id)

V Djangu volejte delay až po úspěšném commitu transakce, jinak může worker hledat objednávku, která ještě není uložená:

from django.db import transaction
transaction.on_commit(lambda: send_order_confirmation.delay(order.id))

Spuštění workeru:

celery -A tasks worker --loglevel=INFO --concurrency=4

Pro plánované úlohy slouží celery -A tasks beat s nastavením beat_schedule.

Nastavení task_acks_late=True znamená, že úloha se potvrdí až po dokončení – pokud worker spadne uprostřed, úloha se zpracuje znovu. Proto musí být úlohy idempotentní.

Idempotence je nutnost

Fronty garantují doručení alespoň jednou. Úloha může proběhnout dvakrát: worker spadne po odeslání e-mailu, ale před potvrzením; síť vypadne; vyprší timeout. Proto:

  • před akcí kontrolujte, jestli už neproběhla (příznak v databázi),
  • u externích API posílejte idempotency key,
  • stav nastavujte, nepřičítejte.

Provoz workerů

Workery musí běžet trvale a po pádu se restartovat. Příklad unitu pro systemd (/etc/systemd/system/celery-worker.service):

[Unit]
Description=Celery worker
After=network.target redis-server.service

[Service]
User=app
WorkingDirectory=/srv/app
ExecStart=/srv/app/venv/bin/celery -A tasks worker --loglevel=INFO
Restart=always
KillSignal=SIGTERM
TimeoutStopSec=60

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now celery-worker
journalctl -u celery-worker -f

Pro Node.js workery stejně dobře poslouží PM2 nebo obdobný systemd unit.

Monitoring

  • BullMQ: dashboard Bull Board (@bull-board/express), metody getJobCounts() pro alerting.
  • Celery: Flower (pip install flower, celery -A tasks flower), příkaz celery -A tasks inspect active.

Sledujte délku fronty (roste, když workery nestíhají), počet neúspěšných úloh a dobu zpracování.

Časté chyby

  • Úloha se zařadí před commitem transakce.
  • Do úlohy se serializují celé objekty, které mezitím zastarají.
  • Chybí limit opakování – nekonečně padající úloha zahltí frontu.
  • Redis pro frontu má nastavenou eviction politiku (allkeys-lru) a tiše maže úlohy. Pro frontu používejte noeviction a ideálně jinou instanci Redisu než pro cache.
  • Worker se při nasazení zabije bez dokončení rozpracovaných úloh.