# Sync Engine

Open the POS menu, then [**Store health**](/de/troubleshooting/store-health.md), just above **Settings** and **Support** at the foot of the menu, to see everything this page describes on your own till. To check something on your till, start here:

* **Check changes waiting to send.** Open **Store health → Database** and read **changes waiting to send**: what this till still has to send to your store. See [Database](/de/troubleshooting/store-health.md#database). If it stays above 0, see ["changes waiting to send" won't go down](/de/troubleshooting/store-health.md#waiting-to-send). How sending works: [How changes get back to your store](#write-path).
* **Check what's downloaded.** Open **Store health → Database** and read the table: how much of your store is on this device. See [What's on this device](/de/troubleshooting/store-health.md#coverage). How downloads are counted: [Is everything downloaded?](#coverage).
* **When does a change made in WooCommerce reach the till?** See [How the POS learns about changes](#change-signal).
* **A sale or edit didn't reach your store.** See [Changes that never reached your server](/de/troubleshooting/store-health.md#refused-changes).
* **"The POS is open in another tab."** See [One tab runs the POS](#multi-tab).
* **"Local database unavailable."** Press **Reload the app**. See [Local storage](#local-storage).
* **Updating to 2.0.** See [Before you click Update](/de/getting-started/upgrade-2-0.md#before-you-click-update).

Before you click Update

On every till, open **Store health → Database** and wait until **changes waiting to send** reads 0, then update. Changes still waiting on a till are not sent after the update. If the count won't go down, don't update yet: see ["changes waiting to send" won't go down](/de/troubleshooting/store-health.md#waiting-to-send). More in [Upgrading to 2.0](#upgrading).

WCPOS is local-first. Every screen reads and writes a database on the device, and a **sync engine** keeps that database and your WooCommerce store in step in the background. The rest of this page explains what the engine fetches, when, and how sales get back to your store, for developers, integrators and store owners who want to know what the POS asks of their hosting.

## One database per store and cashier[​](#one-engine-per-scope "Direkter Link zu One database per store and cashier")

Each store and cashier on a device has its own local database. Two cashiers on one device never share local data.

* **Switching is all or nothing.** The new store or cashier loads its database first and only takes over once it is ready. An error is shown, never a half-switched till.
* **Every request names its store.** On WCPOS Pro with several stores, a price changed at the till updates that store's price, not the web store's.
* **A new version starts a new database.** Nothing is migrated in place. See [Upgrading to 2.0](#upgrading).

How engines and scopes are arranged

Each device runs **one sync engine per site**. Inside it, every store and cashier pair is a **scope** with its own local database. Switching store or cashier switches the scope, and its whole database, on the same engine. Connecting to a different site replaces the engine.

## What runs in the background[​](#sync-lanes "Direkter Link zu What runs in the background")

The till's **change check** asks your store whether anything changed, every 60 seconds on **Balanced**. The [sync presets](/de/troubleshooting/store-health.md#sync-presets) set it from every 10 seconds (**Realtime**) to every 5 minutes (**Eco**).

* **Background work waits for the cashier.** Top-ups of products, customers and variations only run after 60 seconds with no one using the till and no screen waiting for data, and they hold off while your store is struggling to answer. So do the background checks for deleted records and for your store's totals.

Every lane, its cadence and its request ceiling

Everything the engine does on a timer is a **lane**: a named, bounded piece of background work. The change check is the one you can tune. Changes made on the device go out about 0.3 seconds after they are made.

Seeds, trickles, totals and audits each declare how many requests one run may make. The three paths a cashier waits on (the change check, the write drain and the demand drain) don't have a ceiling yet. The budgets are in [Sync Performance](/de/developers/sync-performance.md).

| Lane                                         | Cadence                                                                     | Requests per run                         |
| -------------------------------------------- | --------------------------------------------------------------------------- | ---------------------------------------- |
| Change check                                 | Your preset: 10 s (Realtime), 60 s (Balanced), 5 min (Eco); custom from 5 s | No ceiling yet                           |
| Write drain                                  | About 0.3 s after each change, 10 s backstop                                | No ceiling yet                           |
| Demand drain (fetches what screens declared) | 30 s                                                                        | No ceiling yet                           |
| Recent orders, product and reference seeds   | 5 min                                                                       | 0 (they queue work for the demand drain) |
| Customer, product and variation trickles     | 5 min, after 60 s idle                                                      | 1                                        |
| Server totals retry                          | 30 s                                                                        | 11                                       |
| Coverage compaction                          | 60 s                                                                        | 0                                        |
| Existence prime                              | 15 min                                                                      | 5                                        |
| Deletion audit                               | 17 min                                                                      | 11                                       |

Orders are not on the change check, so each run of the recent orders seed also re-checks every open cart on the device that your store has a copy of and that has nothing waiting to send. A cart your store has deleted or trashed is then removed from the device. Any targeted order fetch does the same: an order your store no longer has is dropped instead of failing the fetch, unless this device still has a change waiting to send on it.

The check interval and the records per request are set per device on **Store health → Performance**:

![Store health on the Performance tab: the status line, the engine uptime strip, the Eco, Balanced and Realtime presets, the two sliders and the Right now line](/img/2.x/reference/sync-engine/en/tablet-light-regular--se-4-performance-card.png)12345678

1. **Status** — **✓ Normal**, or **⚠ Needs attention** when the till is offline, sync is blocked, a first download failed, or a sync job's last run failed. Then the last 24 hours of requests on this till.
2. **Engine uptime** — one cell per hour for the last 24 hours. Green: requests and no errors. Amber: at least one request failed. Empty: the app was closed.
3. **Eco** — the change check every 5 minutes, 25 records per request.
4. **Balanced** — the default: every 60 seconds, 50 records.
5. **Realtime** — every 10 seconds, 75 records.
6. **Check for changes** — the change check interval, 5 seconds to 5 minutes.
7. **Records per request** — how many records one pull fetches, 10 to 100.
8. **Right now** — when the next check is due, and about how many requests an hour these settings make.

## How the POS learns about changes[​](#change-signal "Direkter Link zu How the POS learns about changes")

The POS doesn't download your catalog again to spot changes. At each change check it asks your store one light question: has anything moved since last time? On **Balanced**, a connected till checks for price and stock changes about once a minute. Checks can take longer when your store is struggling to answer. Orders follow their own, separate schedule.

How the change check works

Your store keeps a **change log**, and the POS asks whether anything moved since its last position in it.

* **One check covers eight kinds of record:** products, variations, tax rates, customers, coupons, categories, brands and tags. Orders are not on it; open and recent orders have their own lane, so product and order updates arrive on different rhythms.
* **A quiet store costs almost nothing.** The request is conditional. When nothing changed, your store answers `304 Not Modified` with no body.
* **Checks are spread out by ±20%,** so several tills on one site drift apart instead of asking at the same moment.
* **An unused till slows down, a little.** After 10 minutes with no touch, key press or accepted scan, checks stretch to at least 30 seconds, then at least 60. A hidden window goes straight to 60. On **Balanced** and **Eco** that changes nothing. The next touch brings the full rate back with an immediate check, unless your store has asked the POS to back off. The idle rate is never faster than your setting.
* **A till that was closed for days starts from now.** If more than 5,000 changes piled up, or your store pruned its log past the till's position, the engine jumps to the head of the log and re-checks what the device already holds. The cost follows the size of the local copy, not the time away.

## How screens get their data[​](#declared-demand "Direkter Link zu How screens get their data")

A screen always shows what is already on the device, straight away. When it needs records the device doesn't have yet, such as a search past the first products, the engine fetches them from your store while the till is online. Not everything is on the device from the start; what is depends on the kind of record:

1. **Tax rates** are fetched at start-up, first: the cart can't do its sums without them.
2. **Products:** the first 100 in the default order are fetched at start-up. The rest arrive when a screen asks for them, and through an idle top-up in the cashier's sort order.
3. **Customers** are not fetched at start-up. A screen asks for them, an idle top-up adds 10 at a time, and the change check brings new and edited ones.
4. **Categories, tags, brands and coupons** are not fetched at start-up either. They load when first opened (a reopen within 15 seconds doesn't ask again), are filled in the background once your store reports it has some, are refreshed every 30 minutes and follow the change check.

When the till is online, opening the variation picker refreshes its prices and stock. Variations already on the device show at once while it does.

How a screen asks for data

Screens don't sync. **A screen declares what it shows (search, filters, sort, page) and the engine decides whether that needs a request.** Each declaration ends one of three ways:

* **Served locally** — every record it needs is already on the device.
* **Fetched** — the engine went to your store for it.
* **Released** — the screen let it go, usually because you scrolled or changed a filter and a newer declaration replaced it. This is routine, not an error.

A filter your store can't express is applied on the device; the rest of the query may still go to your store. A declaration that resolves doesn't mean a kind of record is fully downloaded. That is tracked separately, [below](#coverage).

## Is everything downloaded?[​](#coverage "Direkter Link zu Is everything downloaded?")

**Store health → Database** lists each kind of record with the count on the device, your store's total and a coverage bar. See [What's on this device](/de/troubleshooting/store-health.md#coverage). A number there is never made up from what the device holds:

* **A total that is out of date or missing reads *checking…*.** Out of date, the last figure from your store stays beside it. A count from the device would always read 100% and hide the very gap the figure is there to show.
* **Orders read partial by design.** The bar measures against every order in your store, while the till keeps only open and recent ones (**open + recent stay on device · older download when viewed**).
* **Ready to sell comes early.** It turns on as soon as the first products are on the device, and being offline doesn't hold it back: selling offline is the point.

How download totals are checked

A figure like *1,240 of 5,000 products* needs your store's total for the second number. The engine keeps one per kind of record. Each is a cheap request of its own, one record per page, reading WooCommerce's `X-WP-Total` header, at most once every 15 minutes or so per kind.

## How changes get back to your store[​](#write-path "Direkter Link zu How changes get back to your store")

Every change made on the device (a sale, a product edit, a new customer) is saved on the device first and sent to your store within a second or so. An open cart is the exception: it is sent when you press **Checkout**. **A sale rung up offline waits on the device and goes when the connection comes back.** Most changes need nothing from you. Two kinds wait for a choice on **Store health → Database**, and a third is settled for you:

* **An order that clashes is retried once.** If your store has a newer copy of the order, the engine reconciles and sends once more. If that fails too, or the record is not an order, the change is held for you to choose: **Send again** or **Use server version**. See [Changes held because your store had a newer version](/de/troubleshooting/store-health.md#held-changes).

* **A change your store refuses is never retried on its own**, except an order your store has deleted (next). It is kept with your store's reason and shown on **Store health → Database** as **changes never reached your server**. **Send again** rebuilds it from the record as it is on the device now; **Discard** drops it. See [Changes that never reached your server](/de/troubleshooting/store-health.md#refused-changes).

* **Order deleted on the store.** When your store has deleted an order, or moved it to the trash, while this till was still updating it, the POS settles it without asking: an open cart is kept and sent again as a new order with a new number the next time you press **Checkout**; any other order is removed from this device. A message titled **Order deleted on the store** says which, once:

  * Open cart: **Order #1234 was deleted on the store. The cart has been kept as a new order.**
  * Any other order: **Order #1234 was deleted on the store and has been removed from this device.**

  The log records [SYNC203](/de/error-codes/SYNC203.md).

How changes are sent

* **A durable queue comes first.** Each change goes into a queue on the device before anything is sent. About 0.3 seconds later the write drain sends it; a 10-second poll catches anything a nudge missed.
* **Cart changes go one at a time per order.** A fast run of scans applies in order. Adding a product that already has exactly one line in the cart raises that line's quantity instead of adding a second line.
* **What your store sends back is kept.** When your store accepts a change, the device takes your store's copy, so later edits match it. For orders that keeps the line IDs WooCommerce assigned, so an update doesn't add duplicate lines. A local edit your store hasn't seen yet is never overwritten.
* **A deleted order is settled by the POS.** Your store answers an update to an order it no longer has with `404`. The POS discards the queued change; for an open cart it then queues the cart again as a new order, with any edit made in the meantime on it.

## One tab runs the POS[​](#multi-tab "Direkter Link zu One tab runs the POS")

In a web browser, **only one tab per site runs the POS**. A second tab of the same site, for any store or cashier, stops at this screen. It never opens the local database, so it can't ring a sale or send anything, and nothing in the first tab is disturbed.

![A second browser tab of the POS: The POS is open in another tab, Only one tab can run the register, and the Take over here button](/img/2.x/reference/sync-engine/en/tablet-light-regular--se-5-second-tab-card.png)12

1. **The POS is open in another tab** — this tab is parked. **Only one tab can run the register.**
2. **Take over here** — asks the other tab to hand over. It finishes a payment in progress and waits up to 15 seconds for a save, then parks itself, and this tab loads the POS.

* **A parked tab doesn't wake up by itself.** Closing the live tab leaves the other one parked until you press **Take over here** or reload it.
* **"The other tab isn't answering"** — the live tab didn't hand over. Close it, or reload it, to carry on in this one.
* **A payment the old tab tries to start after it has let go of the register** is refused before it is sent, and logged as [CLIENT161](/de/error-codes/CLIENT161.md). A payment that started first, even one that starts while the handover waits for a save, finishes before the tab hands over.
* **Desktop and mobile apps** have no tabs and no gate. A browser that can't coordinate its tabs (no Web Locks support) runs every tab without the check.

## Local storage[​](#local-storage "Direkter Link zu Local storage")

In 2.0 every till keeps its data in a database on the device itself.

If a banner reads **Local database unavailable — scanning, checkout, saving and voiding are blocked until the app is reloaded**, press **Reload the app**. Until you do, refunds are blocked too, and a scan shows **Scanning unavailable** instead of going quiet. The POS stops anything that could lose a sale when the device's database stops answering or a local save stalls. The code is [SYNC161](/de/error-codes/SYNC161.md).

Where the data lives on each platform

The database is SQLite, run through RxDB Premium:

| Platform        | Where the data lives                                                             |
| --------------- | -------------------------------------------------------------------------------- |
| Web             | SQLite-wasm in one dedicated worker, in the browser's private file system (OPFS) |
| Desktop         | SQLite in the app's main process                                                 |
| iOS and Android | SQLite through expo-sqlite                                                       |

Queries run inside the database: filter, sort and page. Only the rows on screen reach the app.

## Upgrading to 2.0[​](#upgrading "Direkter Link zu Upgrading to 2.0")

2.0 doesn't migrate local data. Every device **downloads everything again**:

1. **Every device starts a fresh database** and downloads again from your store, once. Your store holds the full copy of everything synced.
2. **Changes queued before the update are not sent.** On every till, wait until **changes waiting to send** reads 0 on **Store health → Database** before you update. See [Before you click Update](/de/getting-started/upgrade-2-0.md#before-you-click-update). The old 1.x databases are deleted once the store is ready to sell.
3. **The app and the plugin go together.** The 2.0 app needs the WCPOS 2.0 plugin. With only one side updated, the app says which: [AUTH331](/de/error-codes/AUTH331.md) when the plugin is older, [SYNC341](/de/error-codes/SYNC341.md) when the app is older.

## What changed since 1.9[​](#what-changed-in-v1100 "Direkter Link zu What changed since 1.9")

1.9, 1.10–1.11 and 2.0 side by side

|                     | 1.9                                     | 1.10–1.11                                          | 2.0                                            |
| ------------------- | --------------------------------------- | -------------------------------------------------- | ---------------------------------------------- |
| Unit of sync        | One stream per screen query             | One engine, a database per site, store and cashier | Same                                           |
| What drives a fetch | A screen opening                        | A screen declaring what it needs                   | Same                                           |
| Change detection    | Periodic polls and an hourly full audit | Change log with conditional `304` answers          | Same                                           |
| Your store's totals | Not tracked                             | Per kind of record, with *checking…*               | Same                                           |
| Refused changes     | Retried out of sight                    | Durable queue, recovery panel, no silent retries   | Same                                           |
| Browser tabs        | Not coordinated                         | Every tab live, one elected sender                 | One live tab per site, others parked           |
| Storage             | IndexedDB (web), SQLite (apps)          | File-system storage (OPFS on web)                  | SQLite on every platform                       |
| Sync tuning         | Fixed                                   | Presets and sliders in Store health                | Same                                           |
| Local search        | Word-prefix matching                    | Substring matching                                 | Every word, any length, anywhere in the record |

For the request budgets, the back-off when your store struggles and the measured numbers, see [Sync Performance](/de/developers/sync-performance.md).

## Related Documentation[​](#related-documentation "Direkter Link zu Related Documentation")

[Store HealthWhere the store owner sees all of this on the till.](/de/troubleshooting/store-health.md)

[Sync PerformanceRequest budgets and back-off when your store is busy.](/de/developers/sync-performance.md)

[Upgrading to 2.0What to do on every till before you update.](/de/getting-started/upgrade-2-0.md)
