What are the changes?
Wallet Traceability
This version introduces wallet traceability — a system that tracks exactly which inbound (top-up) transactions fund each outbound (consumption) transaction. This enables:- Credit source visibility: Distinguishing when free (granted) vs. purchased credits are being consumed.
- Full audit trail: Every outbound transaction records exactly which inbound transactions funded it and in what amounts.
- Invoice breakdown: Invoices now show a breakdown of granted vs. purchased credit usage, enabling more accurate accounting and reporting.
- New API endpoints: Query which inbound transactions funded a given outbound transaction, and vice versa.
What should self-hosted users do?
Cloud users do not need to follow these instructions as the migration will be performed by the Lago Team.
Migration Steps
1. Install Lago v1.45.0
Install the new version. Schema migrations (new tables and columns) will run automatically on startup.2. Validate wallets (dry-run)
Open a shell on your API server and run the migration task in dry-run mode (the default):| Variable | Default | Description |
|---|---|---|
DRY_RUN | true | Set to false to run the backfill. Any other value (or unset) runs in dry-run mode. |
ORGANIZATION_ID | (all) | Scope to a single organization by its ID. |
LIMIT | (all) | Maximum number of customers to process. The task iterates over distinct customers — all wallets belonging to a customer are always processed atomically as a group. When set, the task prints a next cursor value (see CURSOR). |
CURSOR | (first customer) | Start processing from a specific customer ID (inclusive). Must be a valid UUID. When omitted, the task defaults to the first customer ID in scope. When used without LIMIT, all remaining customers from that cursor onward are processed. When combined with LIMIT, only the next N customers are processed and the task prints a next cursor value for the following run. |
BATCH_SIZE | 1000 | Number of records fetched per database batch. Automatically capped to LIMIT when both are set. |
ERROR_DISPLAY_LIMIT | 50 | Maximum number of problematic wallets (dry-run) or errors (backfill) to display in the console output. |
THREAD_COUNT | 0 | Number of threads for parallel processing. 0 runs sequentially. Ensure your database connection pool size is at least THREAD_COUNT + 1. Due to Ruby’s Global VM Lock (GVL), multi-threading primarily helps when the bottleneck is database I/O wait time. If the migration is CPU-bound, additional threads may not significantly improve performance. |
INCLUDE_TERMINATED | false | Set to true to include terminated wallets in the migration. By default, only active wallets are processed. |
ERROR_LOG_FILE | (tmp dir) | File path for the CSV error log. A file is created at startup in the system’s temporary directory (e.g., /tmp/wallet_migration_errors_20260402120000.csv), but only populated with data when errors are found. On a clean run the file will remain empty. Set this variable to override the default path. |
- A progress bar showing validation progress.
- The number of wallets that can be migrated automatically.
- The number of problematic wallets with details about each issue.
- A migration readiness percentage.
- A CSV error log with all problematic wallets (written automatically to the tmp directory, see
ERROR_LOG_FILE).
If all wallets pass validation, you can skip directly to Step 4.
It is safe to run the backfill directly (Step 4) without running the dry-run first — the backfill also validates each wallet before migrating it and will roll back any customer with problematic wallets. The main purpose of the dry-run is to separate validation from migration, giving you the opportunity to review and fix problematic wallets upfront rather than discovering them during the backfill.
3. Fix or handle problematic wallets
Review the output from Step 2. For each problematic wallet, choose one of the following approaches based on the issue type (see the Troubleshooting section below for details):- Fix the underlying data — Correct the transaction records so the wallet passes validation, then re-run Step 2 to confirm.
- Terminate and recreate — If the data is unrecoverable, terminate the wallet and create a new one (see Terminate and recreate).
4. Run the backfill
Once all wallets pass validation (or have been handled), run the same task withDRY_RUN=false:
- Displays a progress bar showing backfill progress.
- Computes and populates
remaining_amount_centsfor all inbound transactions. - Creates
wallet_transaction_consumptionsrecords linking inbound and outbound transactions. - Sets
traceable = trueon each successfully migrated wallet.
The backfill is safe to run multiple times. It acquires customer-level advisory locks and processes each customer’s wallets atomically, so it can be safely run while the application is serving traffic.
traceable = true.
Scaling the migration with parallel processes
For most installations, a single process is fast enough — parallelization is only worth considering if you have hundreds of thousands of customers with wallets to migrate. We recommend starting with a smaller subset (e.g.,LIMIT=1000) to get a feel for how long the migration takes, then extrapolating from there.
There are two ways to parallelize the migration:
Multi-threading (THREAD_COUNT) runs multiple threads within a single process. Due to Ruby’s Global VM Lock (GVL), this primarily helps when the bottleneck is database I/O wait time and may not significantly improve performance for CPU-bound workloads.
Multiple processes with CURSOR + LIMIT is the recommended approach for large datasets. By splitting the customer
space into non-overlapping windows, you can run several independent processes in parallel — each handling a different
slice. This sidesteps the GVL entirely and scales linearly with the number of processes.
To set this up, you simple launch the first process to discover the next cursor, then launch additional processes with the provided cursor to split the workload:
Re-running the same
CURSOR + LIMIT combination is safe but not recommended, as it will not produce the same result. If you need to re-run, start from the beginning without a CURSOR and continue with the cursors printed by each run.Troubleshooting: Wallets that cannot be migrated
The validation task may flag wallets with data inconsistencies caused by historical bugs. Below are the known issue types and recommended resolutions.Reconcilable issues
These issues can be fixed by updating the data directly in the database, then re-running the validation.| Issue | Validation message | Cause | Resolution |
|---|---|---|---|
| Decimal amount_cents | Decimal amount_cents on inbound TX_ID: 12.9999 (expected integer) | Historical bug storing values non-rounded decimals instead of integers. | Update the transaction amount in the database to rounded integers. |
| Balance drift (small) | Balance drift < 1 unit: 0.5 cents (...) — likely rounding | Historical rounding bug. | Update the wallet balance in the database to match the sum of its transactions. |
| Balance drift (large) | Balance drift >= 1 unit: 500 cents (...) | Historical rounding bug. | This issue might be caused by a significant number of outbound transactions impacted by the historical rounding bug and therefore causing a bigger drift. Update the wallet balance in the database to match the sum of its transactions. |
When fixing wallet balances, you may also need to update related attributes such as
credits_balance and credits_ongoing_balance to keep the wallet consistent. If this level of manual correction is not acceptable, it is preferable to terminate the wallet and recreate it instead (see Terminate and recreate).Non-reconcilable issues
These issues cannot be automatically reconciled due to historical data corruption. The wallet must be terminated and recreated (see Terminate and recreate).| Issue | Validation message |
|---|---|
| Negative wallet balance | Negative wallet balance: -500 cents |
| Negative transaction amount | Negative amount_cents on inbound TX_ID: -100 |
| Insufficient inbound | Outbound TX_ID: insufficient inbound to consume 5000 cents (available: 1000 cents, shortfall: 4000 cents) |
| Missing transaction history | Outbound TX_ID: no inbound transactions available — missing transaction history |
Terminate and recreate escape hatch
Terminated wallets are ignored by the migration task and all traceability operations. It is safe to leave non-traceable wallets in a terminated state — active wallets will still fully benefit from traceability. If a wallet cannot be reconciled due to data issues, the recommended approach is:- Terminate the problematic wallet.
- Create a new wallet for the customer.
- Top up the new wallet with the correct balance.
- The new wallet will automatically be created as traceable — all future transactions will have full consumption tracking from the start.
Wallets created after installing v1.45.0 are automatically traceable — they do not need to be backfilled. The migration task only applies to wallets that existed before the upgrade.