Your App Hasn't Shipped in a Year. The Problem Usually Isn't the Code.
A frozen app isn't a stable one. It's decaying against store policy, OS behaviour, and credentials that expire on their own schedule — and the thing blocking the release is almost never the code.

Open your own store listing and look at the line that says when it was last updated. If that date is more than a year old, you already know it — and you've probably made peace with it, because nothing is visibly on fire. The app is in the stores. It opens. Support isn't drowning. Whatever stopped the last release is still there, but it stopped being urgent a long time ago.
Here's the part that makes it urgent again: an app that isn't shipping isn't holding still. It's decaying, on a schedule that belongs to Apple and Google rather than to you, and the decay is designed to be invisible until it isn't.
Google Play requires apps to target a reasonably recent Android API level. Miss that window and your app doesn't get pulled down or flagged — it just stops being installable for people on newer devices. Your existing users notice nothing. You simply stop acquiring anyone, and the first symptom is a slow bend in your install curve that finance will blame on marketing. Apple runs its own version of this: apps that haven't been updated in a long stretch and aren't pulling meaningful downloads get flagged and eventually removed from the App Store. Underneath both, your iOS distribution certificate and provisioning profiles expire annually whether you ship or not, so a year-old app is very likely an app whose release credentials have already lapsed — which you find out on the day you finally need them. And every third-party SDK you depend on — payments, auth, push, analytics — is running its own deprecation clock that never once asked how your roadmap was doing.
Then there's the ground itself moving. Operating systems change behaviour under apps that haven't changed at all; so do the third-party apps you integrate with. I've watched a send flow that had been stable for months break because another app shipped an update, with nothing on my side touched.
So the cost of not shipping isn't zero, and it isn't linear. It's a bill that accrues quietly and arrives all at once, usually on the day something breaks badly enough that you have to ship — and that is exactly the day you discover you can't.
Which raises the question worth the rest of this article. When a team stops shipping, what actually stopped them? In four codebases I've taken over or rebuilt the delivery for, it was never once "the code can't be changed."
Nobody can press the button
Start with the version of this that's easiest to miss, because from the outside the team looks productive.
There's a fix in the repository. It's been written, reviewed, and merged. It has not reached a single user. Ask why and you get four different answers from four people, none of them wrong, none of them the whole thing.
I joined a Flutter app for a healthcare workforce marketplace — the kind of product where an institution posts shifts and nurses and medical professionals take them. Real money, real rosters, real people expecting to get paid. The Android build pipeline existed and was broken. The iOS pipeline did not exist at all: no automated build, no automated signing, no path to TestFlight, nothing. Distribution to testers ran through Firebase App Distribution, which in practice meant a human being made a build by hand whenever one was needed.
That is a specific and diagnosable condition, and it is not a code-quality problem. The application code was fine. The machinery that turns application code into something a user can install was the thing that had rotted.
So that's what got fixed first, before any feature work. I rebuilt the Android pipeline, built the iOS pipeline from zero, migrated tester distribution off Firebase App Distribution and onto Play Store internal testing, and shepherded the Android app through Play review. Only after that did the feature work start — and it hasn't stopped since: multi-slot shift scheduling with bulk calendar fill, a unified notifications hub, application-history tracking, gross and net pay estimation, all on a two-week ticket-driven cadence.
The principle underneath that, and it's the one I'd put first if I only got to say one thing: the release pipeline is part of the product, and it's the part that decides whether any of the rest of it counts. Code that can't ship has the same customer value as code that was never written. When a team stops delivering, look at the machinery before you look at the codebase — it's cheaper to check and it's usually the answer.
The release runs at the speed of your least available person
The second version is subtler, because everything technically works. It just works through a person.
QA needs a build. Someone opens their laptop, runs the build, and drops the APK into a Slack channel. It takes twenty minutes and everyone considers it solved. Then that person is in meetings all day, or on holiday, or has actual work to do — and testing for the week quietly slides.
That was the state of the message-automation app I lead now: a broken CI/CD pipeline, and QA receiving hand-delivered APKs over Slack. I rebuilt the pipeline and automated deployment to Play Store internal testing, which killed the hand-off entirely. The number that came out the other side is the one I'd point a sceptical founder at: 10+ production releases in three months, on a codebase that had been shipping at nothing like that rate.
The twenty minutes was never the cost. Here's the real one, and it's a loop rather than a line item. When shipping is expensive, nobody proposes small changes — a copy fix, a one-line guard, a tidier error message stop being worth the ceremony. So changes batch up. Big batches are riskier, so releases get scarier. Scarier releases happen less often, which makes the next batch bigger. Teams don't decide to stop shipping. They arrive there, one reasonable deferral at a time, and by the end the release is a scheduled ordeal that everyone has a good reason to postpone.
Automating the hand-off doesn't just save the twenty minutes. It makes a one-line fix cheap enough to be worth having, which is the thing that actually restores cadence.
The gate you can't code your way past
Sometimes the pipeline is fine, the build is done, the binary is uploaded — and the store says no.
I shipped a click-to-chat and scheduling utility to the App Store and got rejected under Guideline 4.2.3(i), Minimum Functionality: the reviewer had to install WhatsApp before they could use the app. My fix seemed obvious. I built a complete standalone path — enter a number, compose, send through Apple's own Messages app — that worked end to end on a device with nothing else installed at all.
It was rejected again, on the same guideline.
What I'd actually missed was that the first screen of a clean install still opened with a "WhatsApp isn't available" notice and a link to go get it, before any input, before my new path was ever reachable. I'd made the app work without the dependency. I hadn't checked what the app showed you before you did anything. Moving that notice behind a real user action — and making the standalone path visible in every state — is what cleared it. The full breakdown of that rejection, including what the guideline is really testing, is its own article.
The cost of that mistake wasn't the code. It was a whole review cycle spent proving the wrong hypothesis — a cycle I got no information from, because I'd changed something that wasn't the problem. So: treat store review as a stage with its own schedule and its own failure modes, and never spend a resubmission on a guess. One defensible change per cycle, and you should be able to say in a sentence why it answers the specific text of the rejection.
The platform ships whether you do or not
The last one is the one that turns a pause into a debt.
I took over Unified Remote's Android and iOS apps from the outgoing team — a product now past 10M installs. Inheriting an app with no handover is its own kind of blocked: nothing is broken enough to force a decision, and nobody left knows why anything is the way it is. What forced the decision there was the platform. The codebase was still pinned to a minimum API level far enough behind that it was constraining what could be built on top of it, and the floor underneath had kept rising the entire time.
So we did the migration: minimum API 9 to 14 in under three months, deleting dead and duplicated code as we went, because the fastest way through a migration is to have less code to migrate. With the floor raised, the work that had been impossible became ordinary — GPU and memory profiling, moving bitmap uploads to the GPU on a background thread for 3× faster live-image rendering, and frame times and memory pulled back under Android Vitals limits.
That's the shape of the whole problem in one story. A year of not shipping is a year of accumulating a migration you were always going to do, at a steadily worse exchange rate. The platform floor rises on a schedule nobody consulted you about. Standing still isn't neutral — it's a decision to fall behind at the platform's pace instead of your own, and to pay for it in one lump later, usually under time pressure, usually when something else is already on fire.
The order I'd unblock it in
If your app hasn't shipped in a year, resist the urge to start with the feature everyone's been waiting for. Start by proving the road exists.
- Can you produce a signed release build today, on a machine that is not one specific person's laptop? Do credentials first — signing keys, distribution certificates, provisioning profiles, and who actually holds the store account logins. Lapsed credentials block everything downstream, and finding out now costs an afternoon. Finding out on release day costs the release.
- Can that build reach a tester without a human in the loop? Not "can someone send it" — can the pipeline deliver it. If the answer involves a person and a chat app, you don't have a pipeline yet.
- Ship something trivial, on purpose. A copy change. A version bump. Something with no product risk whose entire job is to prove the whole road end to end — build, sign, upload, review, release — while the stakes are nothing. Never let a real feature be the thing that discovers your pipeline is broken.
- Then take the platform debt as its own release. Target API level, expired dependencies, deprecated SDKs. Boring, unglamorous, and it unblocks everything after it.
- Then features.
That order isn't arbitrary — it's the same logic as the rest of the rescue playbook: make the situation legible and the machinery trustworthy before you spend money on top of it. A team that can ship a trivial change on demand can ship anything. A team that can't ship at all doesn't have a roadmap problem, however much it looks like one from the outside.
And if what you've got is a pipeline nobody can run, credentials nobody can find, and a codebase whose last owner is gone — that's the shape of work I take on. It usually starts with getting one trivial release out the door, just to prove it can be done.
Ramiz Raja is a senior mobile developer and technical lead with 12+ years shipping and rescuing production apps on Android, iOS, and Flutter. More at codebyramiz.com.