# 0.15.8 Migration Guide

Migration guide for upgrading to Blnk v0.15.8, covering inflight expiry for queued holds, worker upgrade order, and balance reconciliation.

This guide covers the changes you need to review when upgrading to Blnk v0.15.8.

Most integrations can upgrade without code changes. Review this guide if you create inflight transactions without `skip_queue` and set `inflight_expiry_date`.

## At a glance

| Change | Who is affected? | What changed |
| :-- | :-- | :-- |
| Worker upgrade order | Self-hosted deployments that upgrade servers and workers one at a time | Workers run expiry. Stop every older worker before you start a 0.15.8 worker. |
| Queued hold expiry | Apps that create queued inflight holds with `inflight_expiry_date` | Expiry voids the `_q` hold, not the placeholder, and skips a hold that is already committed or voided. |
| Queued split holds expire | Apps that create queued split holds with `inflight_expiry_date` | A split hold now expires. On 0.15.7 and earlier, it stayed held. |
| Holds released twice | Apps that committed or voided a queued hold before it expired, on 0.15.7 or earlier | Expiry may have released the hold's amount a second time. Find these holds and correct the balances once: on Cloud through Support, on self-hosted Core in Postgres. |

## Stop older workers before you start 0.15.8

Blnk workers run expiry. The server only schedules it. While any worker on 0.15.7 or earlier is running, it can pick up a scheduled expiry and release a committed hold again.

Scheduled expiry waits in Redis while no worker runs. When the first 0.15.8 worker starts, it runs every expiry that came due, with the new behavior.

**What to do:** Stop every worker on 0.15.7 or earlier, then start the 0.15.8 workers. Do not run old and new workers side by side. Servers can be upgraded in any order. See [Upgrade Core](/migrations/core/upgrade).

## Expiry skips a settled queued hold

When you create an inflight transaction without `skip_queue`, the response returns a `QUEUED` placeholder. The worker then writes the real hold with the reference `<reference>_q`, and that is the transaction you commit or void.

From 0.15.8, expiry finds the `_q` hold behind the placeholder and voids only what is still held. If you already committed or voided the hold, expiry skips it.

**What to do:** Nothing in your code. If you read the `VOID` from an expired queued hold, find it under the `_q` hold, not under the placeholder. See [Schedule inflight expiry](/transactions/inflight/void-inflight#schedule-inflight-expiry).

## Queued split holds expire

A queued split hold has one placeholder for each leg, `<reference>_1`, `<reference>_2`, and so on, and a `_q` hold under each one. On 0.15.7 and earlier, expiry failed for a queued split, so its legs stayed held. Split holds were not released twice.

From 0.15.8, expiry voids each leg that is still held. A split that passed its expiry before the upgrade may still be held.

**What to do:** If you relied on a queued split staying held past its `inflight_expiry_date`, commit it before you upgrade. After the upgrade, void any split that is still held past its expiry with [Void inflight](/transactions/inflight/void-inflight).

## Find holds released twice on 0.15.7 or earlier

On 0.15.7 and earlier, the expiry still ran after you committed or voided the `_q` hold. It wrote a `VOID` directly under the placeholder and released the hold's amount a second time. That lowered the source balance's `inflight_debit_balance` and the destination balance's `inflight_credit_balance`, out of the amounts reserved for other holds.

An expiry `VOID` from an older version is the only `VOID` whose `parent_transaction` is the placeholder itself. A `VOID` from a manual void or from 0.15.8 expiry sits under the `_q` hold. List every `VOID` that belongs to a queued hold, and keep the ones where `parent_transaction` equals `meta_data.QUEUED_PARENT_TRANSACTION`:

```bash List expiry voids
curl -X POST "http://localhost:5001/transactions/filter" \
  -H "X-blnk-key: <api-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "filters": [
      { "field": "status", "operator": "eq", "value": "VOID" },
      { "field": "meta_data.QUEUED_PARENT_TRANSACTION", "operator": "isnotnull" }
    ],
    "limit": 100,
    "offset": 0
  }'
```

For each `VOID` you keep, list every transaction under its placeholder. The `INFLIGHT` row whose `parent_transaction` is the placeholder is the `_q` hold. Add up the `precise_amount` of the `APPLIED` and `VOID` rows whose `parent_transaction` is the `_q` hold:

```bash List a placeholder's transactions
curl -X POST "http://localhost:5001/transactions/filter" \
  -H "X-blnk-key: <api-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "filters": [
      { "field": "meta_data.QUEUED_PARENT_TRANSACTION", "operator": "eq", "value": "<placeholder_transaction_id>" }
    ],
    "limit": 100
  }'
```

The amount released twice is the smaller of the expiry `VOID`'s `precise_amount` and that total. If the total is `0`, the hold expired normally and is not affected.

**What to do:** After every worker runs 0.15.8, run this check. It only reads data. For each affected hold, record the placeholder, the expiry `VOID`, the `_q` hold, the `source`, the `destination`, and the amount released twice. Page with `offset` until you have every `VOID`. See [Filter records](/search/db/filtering).

## Correct affected balances

Each affected balance holds less than it should by the total released twice for its holds. `inflight_debit_balance` is short on the source, and `inflight_credit_balance` is short on the destination.

Blnk still shows the `_q` hold under an expiry `VOID` as `INFLIGHT`, but the expiry already released it. Committing or voiding that hold can release the amount again and widen the gap. Do not commit or void a `_q` hold whose placeholder has an expiry `VOID`.

On Blnk Cloud, send the list from the previous step to [Support](mailto:support@blnkfinance.com). Blnk corrects the balances for you.

If you host Core yourself, add the amount released twice back to each balance in Postgres. `inflight_balance` is `inflight_credit_balance` minus `inflight_debit_balance`, so it changes with them. Run one transaction for each affected hold:

```sql Correct one affected hold
BEGIN;

UPDATE blnk.balances
SET inflight_debit_balance = inflight_debit_balance + <amount_released_twice>,
    inflight_balance = inflight_balance - <amount_released_twice>,
    version = version + 1
WHERE balance_id = '<source>';

UPDATE blnk.balances
SET inflight_credit_balance = inflight_credit_balance + <amount_released_twice>,
    inflight_balance = inflight_balance + <amount_released_twice>,
    version = version + 1
WHERE balance_id = '<destination>';

COMMIT;
```

The update changes inflight amounts only. Settled balances and transactions stay as they are. The check in the previous step reads transactions, so it still lists a hold after you correct it. Correct each hold once.

**What to do:** On Cloud, send the list to Support. If you host Core yourself, back up Postgres and stop every server and worker. Run the update once for each affected hold, then start Blnk again. Confirm that each corrected balance holds what its open holds need, and keep the list as the record of the correction. See [Backup to disk](/operating/backup-disk).

<RelatedTopics
  items={[
    { title: "Upgrade Core", href: "/migrations/core/upgrade" },
    { title: "Blnk Core changelog", href: "/changelog/blnk-core" },
    { title: "Void inflight transactions", href: "/transactions/inflight/void-inflight" },
    { title: "Filter records", href: "/search/db/filtering" },
  ]}
/>