Skip to content

Worker Architecture

Heavy processing in TT Time Tracker runs in a dedicated services/worker process, separate from the API. This article explains why.

What the worker does

The worker handles three types of background work:

  1. Invoice OCR (invoice-processing queue) — Downloads invoice images from RustFS, runs them through Google Cloud Vision OCR, then sends the extracted text to OpenAI for structured data extraction. This can take several seconds per invoice.

  2. Email sync (email-sync queue) — Polls IMAP inboxes to retrieve invoices sent by email. Network latency and large email payloads make this unsuitable for a synchronous API request.

  3. Automation (automation queue) — Runs per-user automations on a schedule.

  4. Webhook delivery (webhook-delivery queue) — POSTs event payloads to subscriber URLs with retry logic.

Why a separate process?

Crash isolation. If OCR processing causes an OOM error or an unhandled exception crashes the worker, the API continues serving requests. A single Node.js process cannot provide this isolation.

Independent scaling. In a high-throughput scenario, you can run multiple worker instances consuming from the same BullMQ queues without changing the API at all.

No latency impact. Invoice OCR can take 3–10 seconds. Running it synchronously in the API would block the thread (Node.js is single-threaded) during that window. Queuing the job returns a response to the user immediately while the worker processes it asynchronously.

Different concerns. The API handles HTTP — it needs low latency and high concurrency. The worker handles CPU/IO-bound work — it benefits from a different concurrency model (BullMQ's concurrency setting).

The trade-off

Two services to run, two Dockerfiles to maintain, and a dependency on Redis. For a simple app, this is over-engineering. For TT Time Tracker where OCR is a core feature, the trade-off is justified.

In local development, the worker is optional — invoices will queue up without being processed, which is fine for testing most features.

Communication between API and worker

The only communication channel is the Redis-backed BullMQ queue. The API enqueues a job with a payload; the worker processes it and writes results directly to the database. They never call each other over HTTP.

This means the worker can be deployed on a different machine, scaled independently, or temporarily stopped without affecting API availability.

TT Time Tracker — Internal Documentation