# Receiving and processing print tasks

<div style="text-align: justify;">

This page follows a print task through the Print Service: how it arrives, which instance takes it, how it is printed, retried and confirmed, and what protects it against being printed twice.

---

<h3 id="receive-modes" style="color: #203671; margin-top: 2.2em;">Receive modes</h3>

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Mode</th>
      <th style="text-align: left; padding: 6px 10px;">Behaviour</th>
      <th style="text-align: left; padding: 6px 10px;">Use it when</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><strong>Hybrid</strong> (default)</td>
      <td style="padding: 6px 10px;">WebSocket events, plus a safety poll every <code>safetyPollIntervalSeconds</code> (120 s). While the WebSocket is down it polls every <code>pollingIntervalSeconds</code> (30 s) until the reconnect succeeds.</td>
      <td style="padding: 6px 10px;">Always, unless there is a reason for another mode</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><strong>WebSocket</strong></td>
      <td style="padding: 6px 10px;">Only events. Polls once at start and after every successful (re)subscribe, never periodically.</td>
      <td style="padding: 6px 10px;">Minimal API load</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;"><strong>Polling</strong></td>
      <td style="padding: 6px 10px;">Polls every <code>pollingIntervalSeconds</code>, no WebSocket</td>
      <td style="padding: 6px 10px;">Reverb is not reachable from the printer machine (firewall, proxy)</td>
    </tr>
  </tbody>
</table>

<h4 style="color: #203671; margin-top: 1.4em;">Why the safety poll</h4>

Reverb delivers an event once and does not keep it. An event sent during a short network interruption is lost, and a connection can look alive without delivering events. The safety poll finds such tasks at the latest after its interval. Tasks the journal already knows are skipped, so polling never prints twice.

<h4 style="color: #203671; margin-top: 1.4em;">The poll request</h4>

```
GET /api/v1/report-print-task?status=pending&perPage=10&page=1&withMedia=true
    &withRelations=false&withAudit=true&broadcastId=Standard,Warehouse&withoutBroadcastId=false
```

1. The filter runs on the server: only tasks of the own broadcast ids are returned.
2. `withMedia=true` delivers the PDFs directly, no second request per task.
3. `withAudit=true` delivers the creation time for the lead time statistics.
4. If more pages exist, the next page is requested immediately instead of after the interval.

<h4 style="color: #203671; margin-top: 1.4em;">The WebSocket connection</h4>

1. Pusher protocol 7 against Laravel Reverb, private channel `private-report-print-tasks`, authorized with `POST /api/v1/broadcasting/auth` and the API token.
2. The event `report-print-task.created` carries `{ model, id, broadcastId }`. If the broadcast id matches, the service loads the task with `GET /api/v1/report-print-task/{id}?withMedia=true`.
3. Keep-alive: every `pusher:ping` of the server is answered with `pusher:pong`. After 25 s without traffic the service sends its own ping and drops the connection if no pong arrives within 30 s.
4. Reconnect with exponential backoff from 1 s to 60 s, ±20 % jitter. After every successful subscribe a catch-up poll fetches what was created in the meantime.
5. A first connection failure is logged as warning. An outage longer than 5 minutes writes event id 3000.

---

<h3 id="broadcast-ids" style="color: #203671; margin-top: 2.2em;">Broadcast ids: which instance prints what</h3>

Every print task and every printer in VeloxFactory can carry a broadcast id, usually one per site or printer group. An instance only takes tasks whose broadcast id is in its `receive.broadcastIds`, both from events and from polls.

Tasks without broadcast id are taken only with `includeWithoutBroadcastId`. VeloxFactory sends no WebSocket event for them, so they arrive only by polling: in Hybrid within `safetyPollIntervalSeconds`, in mode WebSocket only at start and after a reconnect. Give print tasks a broadcast id whenever they should print immediately.

<div style="border-left: 4px solid #203671; background: #f0f3fb; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
⚠️ <strong>One instance per broadcast id.</strong> The job journal protects against double printing within one instance only. Two instances listening to the same broadcast id, or two instances with <code>includeWithoutBroadcastId</code>, print the same task twice.
</div>

---

<h3 id="rate-limit" style="color: #203671; margin-top: 2.2em;">Rate limit</h3>

The VeloxFactory API limits requests per IP address, by default 10 per minute (`API_RATE_LIMIT_PER_MINUTE`). The service reads the limit from `X-RateLimit-Limit` and the rest from `X-RateLimit-Remaining` in every response and shows both on the dashboard.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Action</th>
      <th style="text-align: left; padding: 6px 10px;">API calls</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px;">Task received by WebSocket</td><td style="padding: 6px 10px;">2 (GET task, set-printed)</td></tr>
    <tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px;">Task received by poll</td><td style="padding: 6px 10px;">1 (set-printed) plus its share of the poll</td></tr>
    <tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px;">Poll</td><td style="padding: 6px 10px;">1 per page</td></tr>
    <tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px;">Error report</td><td style="padding: 6px 10px;">1 (set-status)</td></tr>
    <tr><td style="padding: 6px 10px;">WebSocket login</td><td style="padding: 6px 10px;">1 (broadcasting/auth)</td></tr>
  </tbody>
</table>

1. On HTTP 429 every API call of the process waits until `Retry-After` (60 s if the header is missing), then the same request is sent again. Print tasks wait, they do not fail. The API state shows *Rate limited*, event id 2002 is written.
2. The service does not slow down in advance. A low remaining count is only displayed.
3. **Settings → Receiving** shows the base load of the chosen mode in calls per minute (Hybrid with 120 s: 0.5) and warns when it exceeds half of the limit.
4. The limit counts per IP. All print services, scripts and MCP clients behind the same NAT share it. For print environments raise `API_RATE_LIMIT_PER_MINUTE` in the `.env` of your VeloxFactory instance.

---

<h3 id="pipeline" style="color: #203671; margin-top: 2.2em;">Processing</h3>

```
Receiver -> queue -> journal check -> worker (max. maxParallelJobs) -> printer lock -> queue check -> print -> outbox
```

1. **Journal check.** A task the journal knows in state *Printing* or later is skipped. A task that was confirmed or reported before and is pending again (reprint) is loaded fresh; only if VeloxFactory really shows it as pending is it printed again.
2. **Fetch.** Tasks from a WebSocket event are loaded now, tasks from a poll already carry their PDF. A task that no longer exists or is no longer pending is dropped.
3. **Printer.** The queue name of the task is resolved to the local printer (alias, see [Printers and printing](/books/veloxfactory/page/printers-and-printing)). The Windows default printer is never used; a task without printer name fails with `PrinterNotFound`.
4. **Printer lock.** With `serializePerPrinter` only one job runs per printer, ordered by task id. Jobs for different printers run in parallel up to `maxParallelJobs`.
5. **Queue check.** Before printing, the Windows queue is checked. *Not installed* fails immediately. *Offline*, *paper problem* or *error* goes into retry without printing.
6. **Print.** See [Printers and printing](/books/veloxfactory/page/printers-and-printing). The spooler document name is `VF #{id} {file name}`, so the job can be found in the Windows print queue.
7. **Confirm.** With `confirmation = Spooled` the task is confirmed as soon as the spooler accepted it. With `Completed` the service waits until the spooler job has finished (at most `printTimeoutSeconds`).

<h4 style="color: #203671; margin-top: 1.4em;">Retries</h4>

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px;">Retried (transient)</th>
      <th style="text-align: left; padding: 6px 10px;">Fails immediately (permanent)</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Printer reports offline, paper problem or error (<code>PrinterOffline</code>)</td>
      <td style="padding: 6px 10px;">Printer not installed for the service account (<code>PrinterNotFound</code>)</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Spooler refused to accept the job (<code>SpoolerError</code>)</td>
      <td style="padding: 6px 10px;">PDF missing, unreadable or not renderable (<code>RenderFailed</code>)</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px;"></td>
      <td style="padding: 6px 10px;">Print call or spooler job ran into the timeout, spooler job ended with an error (<code>Timeout</code>, <code>SpoolerError</code>)</td>
    </tr>
  </tbody>
</table>

With the defaults (`retryCount` 3, `retryBackoffSeconds` [5, 30, 120]) a task waits about 2.5 minutes before it is reported as failed. For label printers that are offline longer, for example during a roll change, use more retries or a longer last value, e.g. `[5, 30, 120, 300, 600]` with `retryCount` 5.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Once handed to the spooler, a job is never retried.</strong> A failure after that point (timeout, spooler error) is only reported. Retrying could print the document twice.
</div>

<h4 style="color: #203671; margin-top: 1.4em;">Error reports</h4>

Permanent failures are reported with `PATCH /api/v1/report-print-task/{id}/set-status`, status `error` and an `errorMessage` of the form `[Category] message`, shortened to 1,000 characters. Categories: `PrinterNotFound`, `PrinterOffline`, `RenderFailed`, `Timeout`, `SpoolerError`, `Unknown`. The task shows the message in VeloxFactory.

---

<h3 id="journal" style="color: #203671; margin-top: 2.2em;">Job journal and outbox</h3>

VeloxFactory has no endpoint to claim a task. The local journal in `state.db` takes that role and records every task with its state:

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">State</th>
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Shown as</th>
      <th style="text-align: left; padding: 6px 10px;">Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px;"><code>Received</code></td><td style="padding: 6px 10px;">Received</td><td style="padding: 6px 10px;">Taken, waiting or in retry</td></tr>
    <tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px;"><code>Printing</code></td><td style="padding: 6px 10px;">Printing</td><td style="padding: 6px 10px;">Print call running</td></tr>
    <tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px;"><code>Spooled</code></td><td style="padding: 6px 10px;">Spooled, not confirmed</td><td style="padding: 6px 10px;">In the spooler, confirmation pending. <strong>Never printed again.</strong></td></tr>
    <tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px;"><code>Confirmed</code></td><td style="padding: 6px 10px;">Printed</td><td style="padding: 6px 10px;">VeloxFactory shows the task as printed</td></tr>
    <tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px;"><code>Failed</code></td><td style="padding: 6px 10px;">Failed</td><td style="padding: 6px 10px;">Failed, error report pending</td></tr>
    <tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px;"><code>ErrorReported</code></td><td style="padding: 6px 10px;">Error reported</td><td style="padding: 6px 10px;">VeloxFactory shows the task as error</td></tr>
    <tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px;"><code>Unknown</code></td><td style="padding: 6px 10px;">Unknown</td><td style="padding: 6px 10px;">The service ended while printing. Not printed again automatically, reported as error with <em>Print state unknown after service restart, check printer and reset task if needed.</em></td></tr>
    <tr><td style="padding: 6px 10px;"><code>DryRun</code></td><td style="padding: 6px 10px;">Dry run</td><td style="padding: 6px 10px;">Written to the dry-run folder, not confirmed</td></tr>
  </tbody>
</table>

**Outbox.** Confirmations and error reports are written to the outbox first and then sent. If the API is unreachable, the outbox retries with backoff (5 s, doubling, at most every 5 minutes) until it succeeds. If VeloxFactory answers 404 (task deleted) or 422 (rejected), the entry is dropped. After a crash, spooled tasks without outbox entry get their confirmation again. The dashboard tile *Queue* shows open outbox entries as *to confirm*.

**Reprint.** To print a task again, use **Jobs → Print again** in the window. It resets the task to pending in VeloxFactory (`PATCH set-printed` with `false`), the next poll prints it. Resetting the task in VeloxFactory itself has the same effect.

---

<h3 id="pause" style="color: #203671; margin-top: 2.2em;">Pause processing</h3>

A pause stops printing without stopping the service. Use it for printer maintenance, a roll change or while wrong tasks are cleaned up in VeloxFactory.

1. Set it on the dashboard (**Pause** / **Resume**) or under **Settings → Processing** with reason and optional end time. The pause is stored in `config.json`, so it survives a restart and a pause set in the window also pauses the headless engine.
2. While paused: the WebSocket stays connected, events are noted but not processed, periodic polls are skipped. Tasks already in the pipeline wait before their next print attempt; the waiting time does not count as a retry. A print call already in progress is not interrupted. The outbox keeps delivering confirmations.
3. New tasks stay pending in VeloxFactory.
4. On resume the service polls immediately and prints everything that arrived meanwhile.
5. The engine state is *Paused* with reason and end time. The health endpoint still answers 200.

---

<h3 id="dry-run" style="color: #203671; margin-top: 2.2em;">Dry run</h3>

For tests and demonstrations without paper. With `processing.dryRunFolder` set (or `--dry-run <folder>` on the command line, which applies to that run only) every task is received, fetched and resolved as usual, but instead of printing the PDF is written to `<folder>\<task id>_<file name>`. Nothing is confirmed, the task stays pending in VeloxFactory.

<div style="border-left: 4px solid #203671; background: #f0f3fb; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
⚠️ <strong>Dry-run tasks are not printed later.</strong> The journal marks them as <em>Dry run</em>, and this instance skips them after the dry run is switched off, although they are still pending in VeloxFactory. Use a test broadcast id for dry runs, or create new print tasks afterwards.
</div>

</div>