Understanding the PostgreSQL 'Connection refused' Error
When you attempt to connect to a PostgreSQL server and see the message Connection refused (often accompanied by psql: could not connect to server: Connection refused or Is the server running on host "..." and accepting TCP/IP connections on port 5432?), the client's TCP connection request is being actively denied. This is a low-level network error distinct from authentication failures or timeouts. It means that the operating system on the target host received the TCP SYN packet but returned a RST (reset) packet, or that no socket is bound to the specified address and port.
Root causes fall into a handful of categories:
- PostgreSQL is not running โ the service crashed, was stopped, or never started.
- Listening on the wrong interface โ PostgreSQL is configured to accept only local connections (e.g.,
listen_addresses = 'localhost') but you are connecting from a remote host. - Wrong port โ PostgreSQL is listening on a non-default port (not 5432) and the client uses the default.
- Firewall or network filter โ a host firewall (iptables, nftables, firewalld, AWS Security Group) or network ACL drops the packet before it reaches PostgreSQL.
- SELinux or AppArmor โ mandatory access control prevents PostgreSQL from binding to the port or accepting remote connections.
- TCP wrapper denial โ rare, but
/etc/hosts.denyorpg_hba.confrules can be misread; howeverConnection refusedusually precedes authentication. - Resource exhaustion โ the server has reached
max_connectionsand the kernel refuses new TCP connections to the port (though often this results in atoo many clientserror, some kernel-level rejections may manifest as refusal). - Kernel parameter
net.ipv4.tcp_tw_recycleor NAT issues โ can cause spurious refusals behind load balancers.
Why This Error Matters in Production
A Connection refused error is a hard stop for any application relying on the database. In production, it can cause immediate outages: web requests fail, background jobs stall, and monitoring alerts fire. Unlike transient network blips, refusal indicates a fundamental disconnect between client expectations and server reality. Without a systematic root cause analysis, teams waste time restarting random services or blaming the network when the fix might be a one-line configuration change.
Moreover, repeated refusals can cascade: connection pools exhaust retries, circuit breakers trip, and user-facing errors multiply. Understanding the exact failure mode is critical to restoring service quickly and preventing recurrence.
Systematic Root Cause Analysis
๐ Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Start by narrowing down where the refusal originates. Use a checklist approach, moving from the server process outward.
1. Verify PostgreSQL is Running and Listening
On the database host, check if the postgres process exists and is bound to the expected port.
# Check service status (systemd)
systemctl status postgresql
# Or for specific version (e.g., PostgreSQL 15)
systemctl status postgresql-15
# List all PostgreSQL processes
ps aux | grep postgres
# Check listening sockets (replace 5432 if custom port)
ss -tlnp | grep 5432
netstat -tlnp | grep 5432
If no process is running, start it:
sudo systemctl start postgresql
If it fails to start, examine logs immediately:
journalctl -u postgresql --since "5 minutes ago"
# Or check PostgreSQL's own log (path varies)
tail -100 /var/log/postgresql/postgresql-15-main.log
Common start failures: bad configuration in postgresql.conf, missing data directory, disk full, or port already in use.
2. Check Listen Addresses Configuration
PostgreSQL only listens on addresses specified by listen_addresses in postgresql.conf. The default often is 'localhost' or '127.0.0.1', which rejects any remote client. Find the active configuration file:
# Locate the config file (inside psql or via pg_ctl)
psql -U postgres -c "SHOW config_file;"
# or
pg_ctl status -D /var/lib/postgresql/15/main
Edit the file (path varies by distribution) and set:
listen_addresses = '*' # listen on all interfaces (IPv4 and IPv6)
# or specify a comma-separated list of IPs:
listen_addresses = '192.168.1.10,10.0.0.5'
After changing, restart or reload:
sudo systemctl reload postgresql
# or restart if reload not supported
sudo systemctl restart postgresql
3. Verify the Correct Port
Confirm the port PostgreSQL actually uses:
psql -U postgres -c "SHOW port;"
# or grep the config file
grep "^port" /etc/postgresql/15/main/postgresql.conf
If the client connects to a different port, adjust connection strings or change the server port. Remember to update firewall rules accordingly.
4. Firewall and Network Filter Analysis
Even if PostgreSQL is listening, a firewall can reject the TCP SYN. Check local firewall rules:
# iptables (legacy)
sudo iptables -L -n | grep 5432
# firewalld (common on RHEL/CentOS)
sudo firewall-cmd --list-all
sudo firewall-cmd --list-services
# ufw (Ubuntu)
sudo ufw status
If port 5432 (or your custom port) is not allowed, add a rule:
# firewalld example
sudo firewall-cmd --permanent --add-port=5432/tcp
sudo firewall-cmd --reload
# ufw
sudo ufw allow 5432/tcp
In cloud environments (AWS, GCP, Azure), check Security Groups or firewall tags attached to the instance. Ensure inbound TCP on port 5432 is permitted from the client's IP range.
5. SELinux or AppArmor Restrictions
SELinux on Red Hat derivatives can prevent PostgreSQL from binding to non-default ports or accepting remote connections. Check audit logs:
sudo ausearch -m avc -ts recent | grep postgres
Common issues: wrong port context or postgresql_t type enforcement. Temporarily test by putting SELinux in permissive mode:
sudo setenforce 0
# Test connection, then re-enable
sudo setenforce 1
If the connection works only when permissive, fix the policy:
sudo semanage port -a -t postgresql_port_t -p tcp 5432
# Or set boolean to allow remote connections
sudo setsebool -P postgresql_can_network_connect on
For AppArmor (Ubuntu), check status:
sudo aa-status | grep postgres
And review /etc/apparmor.d/ profiles.
6. Connection Limit Exhaustion
When max_connections is reached, PostgreSQL rejects new connections with a specific error message (FATAL: sorry, too many clients already). However, if a connection pooler or firewall acts on behalf, you might see Connection refused because the pooler's health check fails. Check current usage:
psql -U postgres -c "SELECT count(*) FROM pg_stat_activity;"
psql -U postgres -c "SHOW max_connections;"
If near the limit, either increase max_connections (requires restart and careful resource consideration) or offload connection management with a pooler like PgBouncer.
7. TCP Wrappers and pg_hba.conf (Less Common)
TCP wrappers (/etc/hosts.allow, /etc/hosts.deny) are rarely used now but can cause refusals. Check:
grep postgres /etc/hosts.deny /etc/hosts.allow
pg_hba.conf controls authentication, not initial TCP acceptance. A misconfigured pg_hba.conf leads to FATAL: no pg_hba.conf entry for host after the TCP handshake, so it's not a cause of Connection refused. But always verify it permits the client's IP with the correct authentication method.
Practical Fixes and Preventive Best Practices
Once the root cause is identified, apply the minimal fix. Below are common scenarios and their targeted solutions.
Fix 1: PostgreSQL Service Down
sudo systemctl start postgresql
# Ensure it starts on boot
sudo systemctl enable postgresql
Fix 2: Listen on All Interfaces
# Edit postgresql.conf
listen_addresses = '*'
# Reload
sudo systemctl reload postgresql
Fix 3: Firewall Opening
# Example UFW
sudo ufw allow 5432/tcp comment 'PostgreSQL production access'
Fix 4: SELinux Policy Correction
sudo setsebool -P postgresql_can_network_connect on
sudo semanage port -a -t postgresql_port_t -p tcp 5432
Fix 5: Increase max_connections (with care)
# In postgresql.conf
max_connections = 300 # from default 100, adjust based on RAM
# Restart required
sudo systemctl restart postgresql
Always pair with connection pooling to avoid over-committing memory.
Best Practices to Avoid 'Connection refused' in Production
- Monitoring and alerting โ Set up checks with
pg_isreadyor a simple TCP probe on port 5432. Alert immediately when the service is down or unreachable. - Configuration management โ Keep
postgresql.confandpg_hba.confversioned and deployed automatically. Avoid manual edits that leavelisten_addressesas'localhost'on production instances. - Connection pooling โ Use PgBouncer or an application-side pooler to buffer against connection spikes that hit
max_connections. - Infrastructure as Code โ Define security groups, firewall rules, and SELinux policies in code (Terraform, Ansible) to prevent drift.
- Regular disaster recovery drills โ Simulate PostgreSQL crashes and practice restart procedures so the team knows exactly what to check first.
- Log correlation โ Aggregate PostgreSQL logs, syslog, and firewall logs in a central system (ELK, Loki). When a connection is refused, you can trace the exact rejection point.
- Graceful shutdown handling โ Ensure your application handles database refusals with retries and exponential backoff, and that circuit breakers avoid flooding a recovering server.
Conclusion
A Connection refused error in production is almost always a sign of a missing listener: either PostgreSQL is not running, it's listening only locally, a firewall is blocking the port, or security modules like SELinux are interfering. By walking through a disciplined root cause analysisโchecking process state, listen configuration, port, firewall, and mandatory access controlsโyou can pinpoint the culprit in minutes instead of hours. Apply the minimal targeted fix, then invest in monitoring, configuration management, and connection pooling to prevent the error from reoccurring. A production database should never be a mystery: when it refuses connections, your diagnostic playbook should be ready.