--- name: diagnose-connection-errors description: >- Diagnoses an Azure SQL Database connection refused before a credential was evaluated, reading the error number to name the layer that refused: the IP firewall (40615), a virtual network rule (40914), public network access disabled (47073, 42101), the gateway declining a server name or login format (40532, 40531), a TLS version under the server minimum (47072), plus the transport, pre-login, timeout and certificate failures that carry no number. Use when someone pastes an error naming an IP address, a firewall, the gateway, a certificate chain, the pre-login handshake or a bare timeout, before anyone edits a connection string: none of these is a connection-string problem. Not what the server decided after reading the credential: a login refused, or a valid login with no database user, is entra-id-auth's; a database asking for a retry while it resumes is connect-to-azure-sql's. --- # Diagnose an Azure SQL Database connection error **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. ```bash 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: ```sql 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: ```output 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. ```bash 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 logical `master` where the rules physically live, needs `KILL DATABASE CONNECTION` or 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.0` to `0.0.0.0` is not a wildcard for the internet.** It is the documented `AllowAllWindowsAzureIps` special 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: ```bash 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: ```bash 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. ```bash 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.** `40615` becoming `18456` means the firewall fix worked and the credential is the next problem, which is `entra-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 list` before changing anything else. ## Before reporting a diagnosis - [ ] The number and its severity were both read before any file was edited - [ ] `-m-1` was on, so a severity 10 refusal could not hide as a timeout - [ ] A `40xxx` error was not blamed on the connection string - [ ] `40532` was told apart from `18456` and `4060`, 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](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.