What “production-ready” actually means
“It works on my machine” is a joke with a long life because it keeps being true. Code that behaves perfectly in development can fail within hours of meeting real users, real data, and real network conditions.
The gap between working and production-ready is where most of the difficulty in software engineering actually lives. It is also almost entirely invisible from the outside, which is why it gets cut first when a project is under pressure.
This is what we mean by the phrase, and why we treat it as the point at which something is finished rather than an optional extra.
Development conditions are a fantasy
Locally, the database is small and fast. The network never fails. The only user is you, and you use the feature the way it was designed to be used. Every dependency is available. Nothing runs concurrently.
Production is none of those things. The database has years of accumulated data, including rows written by a version of the code that no longer exists. The network fails intermittently. Users do things nobody anticipated, sometimes twice simultaneously because the button did not appear to respond. Third-party services go down without warning.
Code written against the fantasy usually works. It just does not keep working.
Handling failure as a designed behaviour
The most common gap in otherwise competent code is the assumption that operations succeed.
Every call across a network is a place where things can go wrong: it can fail outright, it can hang, it can succeed slowly enough that the user gives up, or it can succeed while the caller has already stopped listening. Each of these needs an intentional decision, and the default — an unhandled error surfacing as a blank screen — is a decision too, just not a good one.
Three habits cover most of it:
Set timeouts on everything. A request without a timeout can hang indefinitely, holding a connection open. Enough of those and the whole application stops responding, brought down by a slow dependency rather than a broken one.
Retry carefully, and only when it is safe. Transient failures are worth retrying with increasing delays. But retrying an operation that already partially succeeded can duplicate it — charging a customer twice, sending an email twice. Whether an operation is safe to repeat is a property you have to design, usually by giving each attempt an identifier the receiving end can recognise.
Fail in a way the user understands. When something cannot be recovered, say so plainly, preserve whatever the person had entered, and make the next step obvious. Losing ten minutes of someone’s typing to an unexplained error is a product failure, not a technical one.
Data outlives code
Applications get rewritten. The data usually does not — it gets migrated, over and over, accumulating history as it goes.
This has practical consequences. Constraints belong in the database, not only in application code, because the database is what still enforces them after someone writes a script that bypasses your carefully validated form. Nullable columns should mean something is genuinely optional rather than that adding it was inconvenient. Migrations need to be reversible, because the moment you need to undo one is never a calm moment.
Indexes deserve particular attention, because their absence is invisible until it is catastrophic. A query scanning a table of five thousand rows is instantaneous. The same query at five million is a timeout, and the change happens gradually and then all at once.
If you cannot see it, you cannot operate it
A system without observability is one you learn about from customers, which is the slowest and most expensive feedback loop available.
The baseline is modest but non-negotiable. Structured logs with enough context to trace a single request end to end. Errors reported somewhere a person actually looks, with the stack trace and the circumstances attached. A few metrics that reflect the business rather than the machine — orders completed, signups finished, payments failed. Server CPU tells you the box is busy; failed payments tell you something is wrong.
Alerts should be rare and meaningful. A channel full of warnings nobody reads is worse than no alerting, because it manufactures the feeling of monitoring without any of the substance.
Secrets, permissions, and the boring parts
Security in most applications is not defeated by sophisticated attacks. It is defeated by credentials committed to a repository, dependencies never updated, permissions granted broadly because narrowing them was fiddly, and error pages helpfully printing internal details to anyone who triggers them.
The unglamorous version of security is a checklist: secrets in environment configuration and never in source control, dependencies updated on a schedule rather than in a panic, permissions granted narrowly, user input treated as hostile in every context it is used, and errors that log richly on the inside while saying little on the outside.
Deployment should be dull
A release that requires a specific person, a sequence of manual steps, and a quiet afternoon is a release that will eventually go wrong at the worst time.
Deployment should be automated, repeatable, and reversible. Automated because humans skip steps under pressure. Repeatable because a process that works differently each time cannot be debugged. Reversible because the ability to return to the last known-good state in minutes turns a crisis into an inconvenience.
The practical test: if the person who normally deploys is unavailable, can someone else release a fix safely? If not, that is a risk sitting quietly in your operations, unrelated to code quality.
What this means if you are commissioning software
None of this is visible in a demonstration. A prototype and a production system can look identical on screen while differing enormously in what happens when something goes wrong — which is the only moment that ends up mattering.
Useful questions to ask a team building for you:
- What happens if a third-party service you depend on is unavailable?
- How would we find out that something is broken, and how quickly?
- How long does it take to deploy a fix, and who can do it?
- What is the rollback plan?
- How are secrets and credentials managed?
Clear answers indicate a team that has run software in production. Vague ones indicate a team that has built demonstrations. Both can produce something that works on the day it is handed over; only one produces something that is still working, and still maintainable, a year later.
The short version
Production-ready means the system behaves sensibly when things go wrong, its data stays trustworthy over years, its operators can see what is happening, its credentials are handled properly, and releasing a change is routine rather than an event.
That work is not glamorous and it rarely appears in a specification. It is the difference between software that survives contact with the real world and software that merely demonstrated well.