Read the number, then read its severity. The number names the layer that refused, and the severity says what kind of layer it was. Microsoft Learn defines the levels: 11 is an object or entity that does not exist, 14 is security, 16 is user-correctable configuration, 17 is the service out of some resource, 20 and above is fatal and usually takes the connection with it, and 10 is informational and is converted to severity 0 before it reaches the calling application. That last one is why the worst-explained connection failures look like they carry no number at all.
Sourced from Microsoft Learn on 2026-09-03, and checked against sqlcmd 1.10.0 and Azure CLI 2.90.0.
Step 1: make the number visible
-b sets a non-zero exit only at severity 11 and above. Without -m-1 a severity 10 message prints
its text with no Msg number, which is the class 47072, 47073 and 42101 belong to.
-m-1 is an ODBC sqlcmd instruction, meaning the 18.x build from mssql-tools18 or the
Microsoft command line utilities. Measured 2026-09-05, go-sqlcmd 1.10.0, the 1.x build
brew install sqlcmd and winget install sqlcmd install, prints no Msg header on a severity 10
message at any -m value, so on that build those three numbers never appear at all. Query
sys.messages when the number matters.
sqlcmd -S your-server.database.windows.net,1433 -d your-database -U your-login -P "$SQL_PASSWORD" \
-N -l 30 -b -m-1 -Q "SELECT 1"
-N requests an encrypted connection and -l 30 is the connection timeout Learn recommends as a
floor. Then resolve the number on the engine rather than from memory, with the
catalog view Learn publishes for exactly this:
SELECT message_id, severity, is_event_logged, [text]
FROM sys.messages
WHERE language_id = 1033 AND message_id IN (4060, 18456, 40532, 40613, 40615)
ORDER BY message_id;
The severity column is the diagnosis
| Number | Sev | What refused, and what the severity says about it | First move |
|---|---|---|---|
40615 |
16 | No IP rule covers the address the server saw | Add the IP rule, then allow five minutes |
40914 |
not sourced | The client subnet has service endpoints and the server has no virtual network rule | Add the virtual network rule for the subnet |
47073 |
10 | Public network access is Disable, and severity 10 means the client may see no number | Use the private endpoint, or set public access to selected networks |
42101 |
10 | A firewall rule change while public network access is Disable | Set public access to selected networks first |
47072 |
10 | The client offered a TLS version below the server minimum | Raise the client to TLS 1.2 or higher |
40532 |
11 | 11 is "the given object does not exist", and the object is the server, not the login | Check the server name and login format |
40531 |
11 | The gateway could not determine the server name from the connection | Send username@servername, or fix the first DNS segment |
18456 |
14 | Security: the login was evaluated and refused | Named here, answered by entra-id-auth |
4060 |
11 | The object that does not exist is the user in that database | Named here, answered by entra-id-auth |
40613 |
17 | The service is out of something. On a paused serverless or free database it is the expected first answer | connect-to-azure-sql owns the retry doctrine |
10928 / 10929 |
16 | A limit was reached. Resource ID 1 is workers, 2 is sessions | See diagnose-resource-pressure |
40544 |
20 | Size quota, and 20 is fatal, so the connection is gone and retrying on it cannot work | Free space or scale up |
40501 / 49918 / 40197 |
16 | Busy or reconfiguring. Transient | Retry with backoff |
18456, 4060 and 40613 keep a row so a pasted number resolves, and nothing more.
47072, 47073 and 42101 are severity 10, and Learn disagrees with itself
The connectivity settings page prints them as top-level errors, for example Error 47073. The
error number tables list all three at severity 10 with text beginning Reason:, the shape of a
sub-message attached to a login failure rather than a standalone error. Both are Microsoft Learn and
they do not agree.
Take the tables as operative: they explain the symptom. What a client sees was measured on 2026-09-08, by setting public network access to Disabled on a live logical server and connecting:
mssql: login error: Reason: An instance-specific error occurred while establishing a connection to
SQL Server. Connection was denied because Deny Public Network Access is set to Yes. For more
information, see https://go.microsoft.com/fwlink/?linkid=2323206.
It is not a timeout, and it is not silent. The refusal is immediate and go-sqlcmd 1.10.0 prints
the severity 10 sub-message text in full, remedy and documentation link included. What it does not
print is the bare number, so a developer searching their own terminal output for 47073 finds
nothing while the reason is on the screen in front of them. Read the reason, not the number, and look
the number up in sys.messages on a connection that works if you need it for a ticket.
40615 and 40914 are the firewall, and no connection string fixes them
The address to allow is the address the server observed, which network address translation
usually makes different from the client's configured address. 40615 names it in the message, so
read it there rather than guessing.
az sql server firewall-rule list -g your-resource-group -s your-server -o table
az sql server firewall-rule create -g your-resource-group -s your-server \
-n AllowDevWorkstation --start-ip-address 192.168.1.0 --end-ip-address 192.168.1.255
Two things that go wrong here:
- Changes take up to five minutes. A rule that looks correct and still fails may simply be
young.
DBCC FLUSHAUTHCACHE;refreshes it sooner, but it runs in the user database, never in the logicalmasterwhere the rules physically live, needsKILL DATABASE CONNECTIONor the admin account, and needs a connection that already works. It is a tool for the second database, not the one locking you out. - A rule from
0.0.0.0to0.0.0.0is not a wildcard for the internet. It is the documentedAllowAllWindowsAzureIpsspecial case, and Microsoft warns it admits Azure resources in other customers' subscriptions. Prefer a virtual network rule or a private endpoint.
40532 is not 18456 and it is not 4060
All three read as "login failed" and are routinely answered as one thing. Telling them apart is the
diagnosis this skill exists to make, because 40532 is fixed by correcting the connection target
and the other two never are. The severity table above splits them: 40532 is the gateway refusing
before any database was reached, 18456 is the login refused at severity 14, and 4060 is a valid
login with no user in the database it asked for.
Check the login format before the credential. 40531 documents the rule: the server name must be
the first segment of the DNS name, and some libraries do not send it at all, in which case it travels
as username@servername. If both are sent they must match.
Azure SQL Database withholds the reason from the client and there is no server error log to read the state from, so do not promise a diagnosis the platform does not expose. Narrow it by what changed: a new server, a new password, a new database name, a new deployment slot.
One more check when it fails on some client networks only. A hosts file or a private DNS zone pinning the server name to a retired gateway address produces persistent 18456, 40532 and 40615 failures, because the gateway validates the name it was reached by, and logins sent straight to an IP address fail by design.
Public network access and the minimum TLS version
Both are server properties and both refuse at severity 10, so read them rather than infer them:
az sql server show -n your-server -g your-resource-group \
--query "{publicNetworkAccess:publicNetworkAccess, minimalTlsVersion:minimalTlsVersion}"
TLS 1.0 and 1.1 are retired, the lowest minimum Azure SQL Database supports is 1.2, and once a minimum is enforced it cannot be reverted to the default. Enforcing 1.3 breaks clients whose driver or operating system does not support it.
Errors with no number, and the certificate case
When the remote host terminates the TCP connection the client library raises the error, so the SQL
number is 0 and the text belongs to the TCP provider. The one to recognise names the pre-login
handshake and a TCP provider error 0. Microsoft's guidance is a fixed number of
retry attempts before declaring failure. This is also the usual shape of a firewall or network
appliance dropping the connection mid-stream.
The certificate failure is a different animal. The certificate chain was issued by an authority that is not trusted means the client does not trust the public root the service certificate chains
to, or is not reaching the service at all. The roots to have are DigiCert Global Root G2, Microsoft
ECC Root Certificate Authority 2017 and Microsoft RSA Root Certificate Authority 2017.
Test it with encryption on and no trust override. If this succeeds, the chain validates and the certificate is not your problem:
sqlcmd -S your-server.database.windows.net,1433 -U your-login -P "$SQL_PASSWORD" \
-N -l 30 -b -m-1 -Q "SELECT 1"
Do not resolve it with -C or TrustServerCertificate=true outside local development. That
disables the validation the error is reporting and leaves the connection encrypted but
unauthenticated, which is the attack the check exists to prevent. Fix the trust store instead.
Local container differences
The 40xxx family is the gateway, the IP firewall and the service's resource governance, and the
local Azure SQL Database container has none of the three, so an agent chasing a firewall rule to
explain a local failure is chasing something that is not there. What remains locally is the
numberless class above, which behaves the same in both places. A 40xxx failure in the cloud had no
local equivalent to fail on, so a clean local run is not evidence either way.
Check it worked
Run the same command that failed, -b -m-1 still on, and check three things in order.
sqlcmd -S your-server.database.windows.net,1433 -d your-database -U your-login -P "$SQL_PASSWORD" \
-N -l 30 -b -m-1 -Q "SELECT DB_NAME() AS db, SUSER_SNAME() AS login_name;"
echo "exit=$?"
- Exit 0 and one row naming the database and login you expected. Exit 0 with no row means the statement never ran.
- A different number is progress, not failure.
40615becoming18456means the firewall fix worked and the credential is the next problem, which isentra-id-auth's. - The same number after a firewall change is most likely the five minute propagation delay, not
a wrong rule. Confirm the rule with
az sql server firewall-rule listbefore changing anything else.
Before reporting a diagnosis
- The number and its severity were both read before any file was edited
-
-m-1was on, so a severity 10 refusal could not hide as a timeout - A
40xxxerror was not blamed on the connection string -
40532was told apart from18456and4060, not merged into "login failed" - A certificate error was not answered by trusting the server certificate
- Nothing past the credential was answered here
References
- references/error-codes.md: open it when the number is not in the table above, or when the exact documented wording matters, because it carries the verbatim text, the severity and the Microsoft Learn page each number came from.