Fix sqlcmd hanging for ~10 minutes after query timeout on Linux by Copilot · Pull Request #682 · microsoft/go-sqlcmd
On Linux, sqlcmd with -t flag correctly prints "Timeout expired" but hangs for ~10 minutes before exiting. Works correctly on Windows.
Root Cause
runQuery() never closes the sql.Rows object returned by QueryContext(). On timeout, the connection remains open. Linux TCP stack waits ~10 minutes for cleanup; Windows cleans up more aggressively.
Changes
Added missing defer rows.Close() immediately after QueryContext():
rows, qe := s.db.QueryContext(ctx, query, retmsg) if rows != nil { defer rows.Close() }
This releases the connection immediately when the function returns, regardless of error or timeout conditions.
Added end-to-end test TestE2E_QueryTimeout_LiveConnection in cmd/modern/e2e_test.go to validate the timeout behavior. The test uses -t 1 with a 10-second WAITFOR DELAY query to verify that sqlcmd times out correctly and exits immediately without hanging. The test measures execution time and asserts that the command completes within 30 seconds (not 10 minutes), providing concrete validation that the fix works.
Testing
- ✅ Code compiles successfully
- ✅ Static analysis passes (
go vet) - ✅ Security scan clean (CodeQL)
- ✅ End-to-end test added with timing measurement to prevent regression
- ✅ Test verifies command exits within 30 seconds (not 10 minutes)
Original prompt
This section details on the original issue you should resolve
<issue_title>sqlcmd does not handle/honor the command timeout (-t) flag properly on Linux</issue_title>
<issue_description>sqlcmddoes not properly handle/honor the SQL Command Timeout (-t) flag properly on Linux. It works fine on Windows but the command always "hangs" for ~10 minutes before finally exiting when run on a Linux system .Testing Setup
Test Table
USE [dba] GO CREATE TABLE [dbo].[DBScriptTable]( [id] [bigint] IDENTITY(1,1) NOT NULL, [placeName] [varchar](100) NULL, PRIMARY KEY CLUSTERED ([id] ASC) ) ON [PRIMARY] GOBlocker DML
This script will create an open transaction against the above table with SERIALIZABLE transaction isolation level. Running it without completing the transaction will essentially block all operations on the table until the script is either rolled back or committed. That's why the ROLLBACK is commented out here.
-- This should be run from within SSMS so you can control the transaction SET TRANSACTION ISOLATION LEVEL SERIALIZABLE ; BEGIN TRANSACTION INSERT INTO DBscriptTable (placeName) VALUES ('Redmond, Washington') ; --ROLLBACK TRANSACTIONTest DML
Simple select to use for our
sqlcmdtests-- Save as test.sql SELECT * FROM DBScriptTable GOHow to replicate this behavior
Create the test table in a database named
dbaRun the Blocker DML with the
ROLLBACKcommented out from SSMS. This will block all operations on the table. Using something likesp_WhoIsActiveyou should be able to verify that we now have a sleeping session with an open transaction for this.Run
sqlcmdscript below in PowerShell on Windows 11. You will notice that this script will return immediately after the command timeout of 1 secondPS> get-date; sqlcmd -S "<redacted>" -U "<redacted>" -P "<redacted>" -i ./test.sql -d dba -t 1; get-date Tuesday, August 12, 2025 10:20:26 AM Timeout expired Tuesday, August 12, 2025 10:20:27 AM PS>
- Run the
sqlcmdscript below on a Linux system inbash. You will notice that the SQL "Timeout expired" message will appear after 1 second, butsqlcmddoes not return us to a bash prompt for 10 minutes. I'm not exactly sure what it's waiting on here and this is the unexpected behavior.$ date; sqlcmd -S "<redacted>" -U "<redacted>" -P "<redacted>" -i ./test.sql -d dba -t 1; date Tue Aug 12 10:19:56 EDT 2025 Timeout expired Tue Aug 12 10:29:57 EDT 2025 $
- Make sure you uncomment and execute the
ROLLBACKin the SQL script to complete that transaction.</issue_description>Comments on the Issue (you are @copilot in this section)
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.