DEV Community

Cover image for Your Backend Works Locally. That Doesn’t Mean It’s Production-Ready
The Saint
The Saint

Posted on

Your Backend Works Locally. That Doesn’t Mean It’s Production-Ready

You finish the feature.

The API responds correctly.

The database connection works.

Authentication passes.

You run the app locally for the tenth time and everything looks fine.

Then someone says:

“Cool. Let’s deploy it.”

And suddenly the problem changes completely.

A backend that works on your laptop is not necessarily a backend that is ready for production.

Local development proves that your application can run in your environment.

Production asks a much harder question:

Can this thing keep working when the environment is no longer under your control?

That difference is where a lot of small teams get caught.

Local development hides a lot

Your development machine is unusually forgiving.

You probably have:

the correct environment variables
direct access to the database
the right runtime installed
every dependency available
full filesystem permissions
a predictable network
logs directly in your terminal
one developer making requests
a database you can reset without consequences

Production gives you none of those guarantees.

The application now needs to survive restarts, failed migrations, missing secrets, expired credentials, network interruptions, disk limits, bad deployments and actual users.

That means “it runs locally” is only the beginning.

  1. Configuration becomes infrastructure

Locally, this might be enough:

DATABASE_URL=postgresql://localhost:5432/app
JWT_SECRET=dev-secret
PORT=3000

In production, configuration has consequences.

What happens if JWT_SECRET is missing?

Does the application refuse to start?

Or does it silently fall back to something unsafe?

What happens when:

process.env.DATABASE_URL

is undefined?

A production backend should fail predictably.

For example:

const required = [
"DATABASE_URL",
"JWT_SECRET"
];

for (const variable of required) {
if (!process.env[variable]) {
throw new Error(
Missing required environment variable: ${variable}
);
}
}

It is simple, but it is much better than discovering the problem after deployment.

Configuration should be validated before the application begins accepting traffic.

  1. Database migrations stop being harmless

During development, database changes are easy.

Something breaks?

Reset the database.

Change the schema.

Run the migration again.

Move on.

Production is different because the database contains data you cannot casually destroy.

Imagine changing:

ALTER TABLE users
DROP COLUMN username;

locally.

No big deal.

Now imagine doing that with 50,000 production users and discovering another part of the application still depends on username.

The migration technically succeeded.

The deployment still failed.

Production migrations need to consider:

backwards compatibility
existing data
rollback strategy
application version compatibility
long-running operations
database locks

For risky changes, the safer pattern is often incremental.

Instead of:

remove old field
deploy new code
hope

you might:

add new field
deploy compatible code
migrate data
verify
remove old field later

The boring version is usually the safer version.

  1. “The server is running” is not a health check

Your process can be alive while your application is effectively dead.

Maybe:

PostgreSQL is unreachable
Redis is unavailable
migrations failed
storage credentials are invalid
an external dependency is timing out

Your process manager may still happily report:

RUNNING

That is why production systems usually need an actual health endpoint.

A simple example:

app.get("/health", async (req, res) => {
try {
await db.query("SELECT 1");

res.status(200).json({
  status: "healthy"
});
Enter fullscreen mode Exit fullscreen mode

} catch {
res.status(503).json({
status: "unhealthy"
});
}
});

Now your infrastructure has something meaningful to test.

Not:

Is Node.js still running?

But:

Can this application actually serve requests?

That distinction matters.

  1. Logging changes when nobody is watching the terminal

During development:

console.log(error);

often feels perfectly adequate.

You are already looking at the terminal.

Production errors usually happen while nobody is looking.

And this:

Something went wrong

is almost useless three hours later.

Useful production logs should tell you things like:

what happened
when it happened
which request triggered it
which service failed
how severe the failure was

Structured logs are much easier to search and process.

Instead of:

console.log("Database error");

something closer to:

logger.error({
event: "database_connection_failed",
requestId,
error: error.message
});

gives you something you can actually investigate.

Production observability is not about printing more text.

It is about leaving enough evidence to understand failures after they happen.

  1. Backups do not matter until restoration works

A lot of systems technically have backups.

Far fewer know whether those backups can actually be restored.

There is an uncomfortable difference between:

“We create a backup every night.”

and:

“We successfully restored yesterday’s backup into a clean database.”

The second statement proves something.

The first only proves that a file exists somewhere.

A production backup strategy should answer:

How often do backups run?
Where are they stored?
How long are they retained?
Are they encrypted?
How quickly can they be restored?
When was restoration last tested?

A backup you have never restored is still partly a hypothesis.

  1. Deployment without rollback is a one-way door

Imagine deploying version 1.8.

Ten minutes later:

error rates spike
users cannot authenticate
database queries slow down
one endpoint starts returning 500

What happens next?

If the answer is:

“Give me twenty minutes while I fix it.”

you do not really have a recovery strategy.

Sometimes the fastest fix is simply returning to the last known-good version.

That requires your deployment system to know:

v1.6
v1.7
v1.8 ← current

and to make returning to:

v1.7

boring.

Production reliability is not about never breaking anything.

Eventually something will break.

Reliability is largely about reducing how expensive failure becomes.

  1. Production traffic exposes assumptions

During local development you might be the only user.

Production introduces:

User 1
User 2
User 3
...
User 10,000

And all of them can do things at inconvenient times.

You start encountering problems like:

race conditions
exhausted database connections
duplicate requests
slow queries
rate abuse
concurrent writes
memory growth
unexpectedly large payloads

For example, this query may seem harmless:

SELECT *
FROM orders;

until orders contains several million records.

Production has a habit of turning assumptions into benchmarks.

A better definition of “done”

For a backend feature, “done” should probably not mean:

✓ Works locally

A stronger checklist is closer to:

✓ Works locally
✓ Configuration validated
✓ Database changes are safe
✓ Authentication and permissions verified
✓ Health checks available
✓ Errors observable
✓ Deployment reproducible
✓ Backups exist
✓ Restoration tested
✓ Rollback possible
✓ Production behavior monitored

Not every side project needs enterprise infrastructure.

But every application should know what level of failure it is prepared to tolerate.

That is the important part.

The backend lifecycle is bigger than building

Developers spend a lot of time discussing how quickly we can create APIs, databases and authentication.

Those things matter.

But building the backend is only one stage.

Eventually the lifecycle becomes:

Build → Verify → Deploy → Operate → Own

And the transitions between those stages are where a surprising amount of engineering time disappears.

That is also the problem we have been thinking about while building CrescoDB: how to give smaller engineering teams a cleaner path through the backend lifecycle without forcing them to stitch together an increasingly complicated production stack.

But regardless of which tools you use, the principle stays the same:

A backend isn't production-ready because it runs. It's production-ready when you understand how it fails.

What is the one production check you wish you'd added before one of your deployments went wrong?

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.