Sui.

Post

Share your knowledge.

Bounty+15

Xavier.eth.
Jun 27, 2025
Expert Q&A

Sui Transaction Failing: Objects Reserved for Another Transaction

I'm encountering a persistent JsonRpcError when trying to execute transactions on Sui. The error indicates that objects are reserved for another transaction, even though I've implemented sequential transaction processing with delays.

JsonRpcError: Failed to sign transaction by a quorum of validators because one or more of its objects is reserved for another transaction. Other transactions locking these objects:
- AV7coSQHWg5vN3S47xada6UiZGW54xxUNhRv1QUPqWK (stake 33.83)
    - 0x1c20f15cbe780ee7586a2df90c1ab70861ca77a15970bea8702a8cf97bd3eed9
    - 0x1c20f15cbe780ee7586a2df90c1ab70861ca77a15970bea8702a8cf97bd3eed9
    - 0x1c20f15cbe780ee7586a2df90c1ab70861ca77a15970bea8702a8cf97bd3eed9

I've tried:

  • Sequential transaction execution (waiting for previous transaction to complete)
  • Added 3-second delays between transactions

And still getting the same error consistently.

Using Sui RPC for transaction submission. The same object ID appears multiple times in the lock list. Error occurs even with careful transaction sequencing.

  1. What causes objects to be "reserved" for other transactions?
  2. How can I properly check if an object is available before using it in a transaction?
  3. Are there best practices for handling object locks in Sui?
  4. Could this be related to transaction finality timing?

Has anyone encountered this issue before? Any insights on proper object management in Sui transactions would be greatly appreciated!

  • Sui
  • Transaction Processing
  • Move
4
10
Share
Comments
.

Answers

10
harry phan.
Jun 30 2025, 15:00

This error happens if you try and run two transactions simultaneously (e.g. start one before the previous one finishes). If you retry running the publish transaction, without running another transaction before or at the same time, it should succeed. You may also need to acquire more gas from the faucet (or wait a day – for the epoch to roll over – for the objects to get unlocked)

When you run a transaction that involves objects that your address owns (like the gas objects) validators reserve the latest version of the object to be used by the transaction it is signing. If you try and run two transactions simultaneously and they refer to the same object, then they will compete with each other for signatures from validators. In the happy case, one of the transactions wins, and runs, and the other transaction fails to get enough signatures. In the unhappy case, both transactions can fail to get enough signatures (if both got more than a third of the validator’s signatures, then neither can get over two thirds, which is the threshold), this is called equivocation, and from that point the objects that were inputs to both transactions can’t be used for any other transactions.

At the end of the epoch (they are roughly a day long – you can check progress to the next epoch change on https://suiexplorer.com), all locks are released, so you can use the objects again, but if you haven’t had an epoch change since you last tried, you will need to acquire more gas.

12
Comments
.
0xduckmove.
Jun 30 2025, 07:04

Hey, you are trying to do a transaction too fast and the objects got locked.

Try to send one transaction with the same objects at a time, if you send two transactions some validators might accept the first some might accept the second and you objects will be locked because each transactions needs 66.7% of validators and you might be getting only 50%.

=> just wait for the epoch reset, it's soon 🫡

Check more: https://forums.sui.io/t/beginner-tutorial-error-when-deploying-simple-sui-package/44842

6
Comments
.
Owen.
Owen4662
Jun 30 2025, 11:03

Sui uses optimistic concurrency control, meaning objects are locked when used in a transaction until that transaction is finalized or expires.

Even if you wait 3 seconds between transactions, if the previous one hasn't been finalized, the object remains locked. This means that transaction is still pending and holding exclusive access to the object.


How to Check If an Object Is Available

Use the Sui RPC method:

sui_getObject

Check response for "status": "Locked" or "owner": "locked".

Example request:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "sui_getObject",
  "params": ["0x...object_id..."]
}

If locked, just wait more and retry later.

5
Comments
.
MiniBob.
Jun 30 2025, 11:50

The error means the objects your transaction uses are still locked by previous transactions not yet finalized. Even with delays, objects remain reserved until those transactions complete on-chain.

To fix:

  1. Always confirm prior transactions involving the objects have fully finalized before using them again.
  2. Check object status via Sui RPC to ensure they’re unlocked.
  3. Avoid sending parallel or rapid transactions on the same objects.
  4. Implement retries with backoff and finality checks instead of fixed delays.

This locking prevents conflicting updates and is normal in Sui’s object model. Proper sequencing and finality confirmation are key to avoiding JsonRpcError.

4
Comments
.
BigDev.
Aug 15 2025, 16:24

This error means the objects you’re using are still locked by earlier transactions that haven’t finalized yet. Even with delays, Sui keeps them reserved until the chain confirms completion.

To fix it, make sure any previous transactions using those objects are fully finalized before reusing them. You can check their status through Sui RPC to see if they’re unlocked. Avoid sending multiple or rapid transactions involving the same objects. Instead of relying on fixed delays, use retries with backoff and confirm finality before retrying.

This locking is part of how Sui ensures safe updates, so proper sequencing and checking finality is the way to avoid JsonRpcError

2
Comments
.
0xF1RTYB00B5.
Sep 18 2025, 16:25

That’s a great debugging question — I’ve run into this exact issue before when building staking flows on Sui. Let me answer in detail, step-by-step, from my perspective using Sui daily.


🔍 Why objects get “reserved” in Sui

In Sui, every transaction consumes specific object versions. Once you submit a transaction that references an object, the network puts a lock on that object version until the transaction is finalized (committed or fails).

So when you see:

JsonRpcError: Failed to sign transaction ... one or more of its objects is reserved

it means:

  • The object version you’re trying to use is still “in flight” (reserved by another PTB),
  • Or you’re re-using an outdated version without refreshing from the fullnode,
  • Or the validator quorum hasn’t yet reached finality for the first transaction.

That’s why the same objectId shows multiple times in the lock list — you’re trying to consume the same locked version repeatedly.


✅ How I check object availability before using

I always follow these steps:

  1. After each transaction, re-fetch fresh object refs before building the next transaction block.

    const fresh = await provider.getObject({
      id: "0x1c20f15cbe780ee...",
      options: { showOwner: true, showPreviousTransaction: true, showContent: true },
    });
    const version = fresh.data.version; // must use this version in the next PTB
    
  2. Wait for local execution / finality when submitting. Always request WaitForLocalExecution in your signAndExecuteTransactionBlock. This ensures the object is unlocked before you try to use it again.

    const res = await signer.signAndExecuteTransactionBlock({
      transactionBlock: tx,
      requestType: "WaitForLocalExecution",
    });
    
  3. Implement a retry with exponential backoff when conflicts occur. Even with sequencing, there are cases where the fullnode lags. I use a submitWithRetry wrapper (like the one I showed earlier) that catches Object version mismatch and retries after re-fetching the object.


⚡ Best practices for handling object locks

  • Never reuse old object references. Always fetch the latest state from the RPC before constructing each PTB.
  • Design with parallelism in mind. If multiple independent flows need the same object, shard state across multiple owned objects (avoid a single global shared object).
  • Batch operations when possible. Use a single TransactionBlock with multiple Move calls instead of sending sequential transactions that lock the same object repeatedly.
  • Add backoff + retries. I usually back off 100ms, 200ms, 400ms, 800ms … up to ~3s before giving up.
  • Use checkpoints/finality awareness. Remember: an object isn’t truly free until the checkpoint is committed.

⏱️ Relation to finality timing

Yes, this is directly related. Even after a transaction appears “executed”, the object may remain reserved until:

  • The local fullnode confirms execution,
  • The consensus checkpoint includes it,
  • The version updates propagate across validators.

That’s why adding a blind 3-second delay isn’t reliable — network conditions and validator lag can extend this. The right way is fetch-after-execution instead of sleeping.


🛠️ Example: Safe sequential staking

Here’s how I do it in code (simplified):

async function safeStake(signer, validatorAddr, stakeAmountMist, stakeObjectId) {
  let attempt = 0;
  while (attempt < 5) {
    try {
      // 1. Fetch latest object version
      const obj = await provider.getObject({ id: stakeObjectId, options: { showOwner: true } });
      const version = obj.data.version;

      // 2. Build PTB with fresh version
      const tx = new TransactionBlock();
      tx.moveCall({
        target: "0x2::staking::request_add_stake",
        arguments: [tx.pure(validatorAddr), tx.pure(stakeAmountMist.toString())],
        typeArguments: [],
      });

      // 3. Execute with finality wait
      const res = await signer.signAndExecuteTransactionBlock({
        transactionBlock: tx,
        requestType: "WaitForLocalExecution",
      });

      console.log("✅ success:", res.digest);
      return res;
    } catch (e) {
      if (e.message.includes("reserved") || e.message.includes("Object version mismatch")) {
        attempt++;
        const backoff = 200 * 2 ** attempt;
        console.warn(`conflict, retrying in ${backoff}ms`);
        await new Promise(r => setTimeout(r, backoff));
        continue;
      }
      throw e;
    }
  }
  throw new Error("failed after retries");
}

This ensures:

  • I always use the latest object version,
  • I wait for finality,
  • I retry on transient lock errors.

🧩 TL;DR

  • Cause: Object locks happen because an object version is still reserved by an in-flight transaction.
  • Fix: Always re-fetch latest object versions, wait for local execution, and retry on conflicts.
  • Best practice: Shard state, batch operations, and use retries/backoff rather than fixed sleeps.
  • Yes, finality timing matters. Blind delays won’t solve it — confirmation checks + re-fetching will.
0
Comments
.

Do you know the answer?

Please log in and share it.