A quarter was the plan for that migration. It stalled on one function nobody could explain.
I had inherited a Python 2 system and started moving it to 3. Most of the work was mechanical: strings, imports, the usual. Then I hit a payment-handling path wrapped in a bare except that swallowed everything and retried once, silently.
No comment. No ticket. The author was long gone.
So, two options. Keep it, and carry into the new codebase a bug I didn't understand. Or drop it, and learn in production what it had been protecting against for years.
I kept it, logged every time it fired, and waited. It fired. Rarely, but it fired: on one specific malformed callback from a single integration, the kind the clean version of the code would have passed straight through to the ledger.
That changed how I scope rewrites. The language version was the cheap part. The costly bit was that the real specification of the system lived in defensive code, and the only copy of it was the code itself.
Now, before I agree to any rewrite, I ask what the current system does on the bad path. The happy path everybody can describe. I want to know what happens on a duplicate, a partial, a timeout, a malformed payload. If nobody can answer, the rewrite doesn't have a target yet, it has a guess.
Second question: can I run both versions side by side on real traffic and compare outputs. If the architecture makes that impossible, the rewrite is a one-shot cutover, and one-shot cutovers on money paths are how balances go quietly wrong.
The pattern I see most often is a team that wants a rewrite because the code is unpleasant to work in. That's a real cost. But unpleasant code with ten years of absorbed edge cases is probably worth more than clean code that hasn't met production yet.
If you're looking at a rewrite right now: I'd check whether you have a written description of your system's failure behaviour, or whether it lives only in exception handlers nobody wants to touch...