The database runs, the credentials are somewhere in the dashboard, and still the app fails at startup with Connection refused, a timeout or an SSL handshake error. These errors mean the app is aiming at the database the wrong way, not that the database is down. Work through the checks in order; the first two explain most cases.
Platform behaviour checked in November 2026.
1. The URL is in the wrong format
Java's PostgreSQL driver uses its own URL scheme:
jdbc:postgresql://<host>:<port>/<dbname>?sslmode=require
Hosting platforms usually hand out a different shape. Heroku's documentation shows it side by side: DATABASE_URL follows postgres://<username>:<password>@<host>/<dbname>, while the JDBC driver expects jdbc:postgresql://.... Passing the first to the second fails, often with a message that does not mention the format at all.
On Heroku, Java apps can use JDBC_DATABASE_URL, which is already in the JDBC format. On other platforms, either build the JDBC URL from the parts or set host, port, database, user and password as separate variables and assemble them in configuration.
2. SSL is required and not requested
Managed databases often refuse unencrypted connections. Heroku Postgres requires SSL with TLS 1.2 or higher, and its documentation says a connection without sslmode=require receives a connection error. The fix is one parameter on the JDBC URL:
spring.datasource.url=jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=require
Heroku also warns against editing the platform's config var to add it, because failovers or credential rotations can overwrite the variable. Add the parameter in your own configuration instead.
3. Refused or timed out: read the difference
The two errors point in different directions. Connection refused means the app reached a host, but nothing listened on that port: check the host name and port, and whether the database is in the same region or network you think. A timeout means nothing answered at all, which usually comes from network rules.
Many database providers only accept connections from allowed IP addresses or from their own private network. An app on another platform, with changing outbound addresses, will time out until the database allows it. Check the provider's network or "trusted sources" settings before changing anything in the app.
4. Credentials and names differ from local
Local databases often run with a default user and an open password. On the host, the user, password and database name come from variables, and a single typo or an unset variable produces an authentication error that looks like a connection problem at first glance. Print which variables are set at startup, never their values, and compare them with the provider's dashboard.
5. The pool is bigger than the plan allows
A connection pool opens several connections at startup. Small database plans have a low connection limit, shared by every instance of the app and every tool you connect with. If instances times pool size exceeds the limit, new instances fail during deploys, exactly when old and new instances run side by side.
Keep the pool small on small plans, for example:
spring.datasource.hikari.maximum-pool-size=5
Then check the arithmetic: instances times pool size, plus room for a migration tool and your own client, must stay under the plan's limit.
Test the connection before the app
When the error is unclear, take the app out of the picture. From a shell on the same platform, or from your machine if the database accepts outside connections, connect with the database's own client using the same host, port, user and SSL setting:
psql "host=$DB_HOST port=$DB_PORT dbname=$DB_NAME user=$DB_USER sslmode=require"
If this fails too, the problem is the network, the credentials or the SSL setting, and no change to the Java code will fix it. If it works, compare its settings with the JDBC URL the app builds, character by character.
Migrations with more than one instance
Tools such as Flyway or Liquibase often run at startup. That is convenient with one instance, but during a rolling deploy two instances can start at the same time and both try to migrate. Both tools use a lock in the database to keep that safe, but a long migration can still delay startup past the platform's health check. For larger changes, run migrations as a separate release step before the new version starts, so the app itself only starts and serves.
Next steps
If the database connects but the app still fails, the cause is elsewhere in the environment: go back to works locally, not online. For a full setup with a database from the start, the deployment guide walks through the steps, and the hosting comparison shows which platforms run Java next to a managed database.
Sources