The fastest way to lose control of a custodial wallet, I think, is to model a withdrawal as a transfer.
A transfer is a single moment. It either happened on-chain or it didn't. If that is your model, then the instant a user hits withdraw you are already committed. There is no safe point to cancel. No window to batch several outputs into one transaction either, and no chance to re-price the fee when the network moves under you.
A withdrawal request is not a moment. It is a record with a lifecycle: requested, held for review, approved, queued, signed, broadcast, confirmed. Every one of those is a place where a human can still step in, or a risk rule, or a fee policy.
I have built custodial BTC and ETH wallets and on-chain payment rails, and the pattern holds every time. Once the request is its own row with an explicit state machine, broadcasting becomes one transition inside the operation rather than the entire operation. Cancellation becomes a state change instead of an apology. Batching becomes a scheduling decision. Compliance holds stop being special-cased hacks bolted onto the send path.
Infer the state from the chain instead, and you get a system that can only tell you what already happened...
Wrote up the full argument here: https://polycratia.com/c/4298b9da
If you run withdrawals in production, I'd like to know what forced you to add the request record: a cancellation you couldn't honour, or probably a fee spike you couldn't re-price.