tedious
Tedious is a low-level Node.js implementation of Microsoft's Tabular Data Stream protocol for SQL Server and Azure SQL. It opens a single database connection, authenticates with SQL credentials, NTLM, access tokens, service principals, managed identity, or Azure Identity, and exposes event-driven requests, typed parameters, stored procedures, transactions, prepared statements, bulk loads, result metadata, and row streaming. It deliberately does not provide connection pooling, an ORM, schema migrations, a query builder, or a Promise-first application API.
Tedious is a capable foundation for SQL Server integrations that need protocol-level control, and it is actively maintained. Most application teams should install `mssql` instead for pooling and Promises, while watching the npm tag split before standardizing on version 20.
Use it if
- You need direct control over SQL Server's TDS protocol, request events, type metadata, stored procedures, table-valued parameters, or bulk loading
- You are building a higher-level SQL Server adapter and want a pure-JavaScript transport instead of an ODBC native addon
- You need Azure SQL authentication through DefaultAzureCredential, managed identity, a service principal, or an access token
- Your application can provide its own pool and Promise wrapper while keeping all values in typed Request parameters
- Your runtime is Node 20 or older: Tedious 20.0.0 requires Node 22, and its release notes explicitly drop Node 18, 20, and 21
- You expect pooling or concurrent requests on one connection: the API docs permit only one active request per connection and Tedious ships no connection pool
- You want a Promise-first query method that returns rows: core uses Connection and Request callbacks plus row events, so most applications are better served by the `mssql` wrapper
- You want a lean dependency tree for SQL-password authentication: 20.0.0 installs Azure Identity, Azure Key Vault Keys, and their supporting packages whether or not that authentication path is used
- Cancellation correctness is critical on the default npm tag: releases 20.0.4 and 20.0.5 contain cancellation and connection fixes but were still published under the `next` tag while npm `latest` remained 20.0.0
Setup reality
Tedious 20 starts with a hard platform gate: Node 22 or newer. Supply a hostname, database, and authentication block, then call `connection.connect()` explicitly; constructing a Connection no longer means it is ready. SQL Server must have TCP/IP enabled, the correct port reachable, and the login enabled. Encryption defaults to true. In production, keep `trustServerCertificate` false and install a certificate chain valid for the hostname; setting it true is a local-development concession, not a generic fix. Azure SQL users must choose the matching authentication type and supply tenant/client details or an environment that DefaultAzureCredential can resolve. SQL-password credentials belong in a secret store, never source. The driver is callback and event based: rows arrive one at a time, the completion callback is the safe point for the next request, and some failures are AggregateError values whose nested errors must be inspected. One connection handles only one request at once, so a web service needs a maintained pooling layer with checkout, timeout, health check, and guaranteed release logic. Do not use the old `tedious-connection-pool`; Tedious's FAQ calls it inactive. Always use `Request.addParameter()` with an explicit TYPES value rather than string interpolation, specify precision and scale for Decimal/Numeric, and choose NVarChar for Unicode. Row collection is off unless configured because retaining every row defeats streaming. Close connections on shutdown and after fatal errors, set connection/request timeouts, and test certificate, firewall, Azure token, cancellation, and failover behavior against the same SQL Server edition used in production.
Patterns
Connect with a SQL Server loginconnect-with-sql-login
import { Connection } from 'tedious';
const connection = new Connection({
server: process.env.SQL_HOST!,
authentication: {
type: 'default',
options: {
userName: process.env.SQL_USER!,
password: process.env.SQL_PASSWORD!,
},
},
options: {
database: process.env.SQL_DATABASE!,
port: 1433,
encrypt: true,
trustServerCertificate: false,
connectTimeout: 15_000,
},
});
connection.connect((error) => {
if (error) throw error;
console.log('connected');
});Version 20 requires Node 22. Keep certificate verification on in production; a hostname-valid SQL Server certificate avoids trustServerCertificate true.
Execute a query with typed parametersexecute-parameterized-query
import { Request, TYPES } from 'tedious';
const request = new Request(
'SELECT id, name FROM dbo.Users WHERE email = @email',
(error, rowCount) => {
if (error) throw error;
console.log({ rowCount });
},
);
request.addParameter('email', TYPES.NVarChar, email, { length: 320 });
connection.execSql(request);Parameter names omit the `@`. Use NVarChar for Unicode and set realistic lengths so SQL Server can choose better plans.
Collect row events into objectscollect-query-rows
const rows: Record<string, unknown>[] = [];
const request = new Request(sql, (error) => {
if (error) return reject(error);
resolve(rows);
});
request.on('row', (columns) => {
rows.push(Object.fromEntries(
columns.map((column) => [column.metadata.colName, column.value]),
));
});
connection.execSql(request);This buffers every row in application memory. For large results, process row events incrementally and apply pause/resume backpressure.
Pause a request while processing a rowstream-rows-with-backpressure
request.on('row', (columns) => {
request.pause();
void persistRow(columns)
.then(() => request.resume())
.catch((error) => {
request.cancel();
report(error);
});
});
connection.execSql(request);Do not start another request while this one is paused. One Tedious connection can execute only one request at a time.
Set and handle a request timeoutset-request-timeout
const request = new Request(sql, (error) => {
if (error) {
console.error(error);
return;
}
});
request.setTimeout(5_000);
connection.execSql(request);Timeout triggers cancellation, which is not the same as rolling back a wider transaction. Test the pinned 20.x release because later next-tag builds contain cancellation fixes.
Commit or roll back a transactionrun-transaction
connection.beginTransaction((beginError) => {
if (beginError) throw beginError;
const request = new Request(sql, (requestError) => {
if (requestError) {
return connection.rollbackTransaction((rollbackError) => {
if (rollbackError) throw rollbackError;
});
}
connection.commitTransaction((commitError) => {
if (commitError) throw commitError;
});
});
request.addParameter('amount', TYPES.Decimal, amount, { precision: 18, scale: 2 });
connection.execSql(request);
});Wait for each callback before the next operation. If rollback also fails, preserve both errors and discard the connection from the pool.
Prepare and execute a repeated statementprepare-and-execute
const request = new Request(
'INSERT INTO dbo.Events (kind, payload) VALUES (@kind, @payload)',
(error) => { if (error) throw error; },
);
request.addParameter('kind', TYPES.NVarChar);
request.addParameter('payload', TYPES.NVarChar);
request.on('prepared', () => {
request.once('requestCompleted', () => connection.unprepare(request));
connection.execute(request, { kind: 'login', payload: '{}' });
});
connection.prepare(request);Always unprepare when finished to release the server handle. A prepared Request occupies the connection while its operations run.
Call a procedure with an output parametercall-stored-procedure
const request = new Request('dbo.CountCharacters', (error) => {
if (error) throw error;
});
request.addParameter('inputVal', TYPES.VarChar, 'hello');
request.addOutputParameter('outputCount', TYPES.Int);
request.on('returnValue', (name, value) => {
console.log(name, value);
});
connection.callProcedure(request);The Request text is the procedure name for callProcedure, not a CALL or EXEC SQL string. Output values arrive through returnValue events.
Bulk-load validated rowsbulk-insert-rows
const bulk = connection.newBulkLoad(
'dbo.ImportRows',
{ keepNulls: true },
(error, rowCount) => {
if (error) throw error;
console.log({ rowCount });
},
);
bulk.addColumn('id', TYPES.Int, { nullable: false });
bulk.addColumn('name', TYPES.NVarChar, { length: 100, nullable: true });
connection.execBulkLoad(bulk, [
{ id: 1, name: 'Ada' },
{ id: 2, name: null },
]);Column order, names, types, lengths, and nullability must match the target table. Validate and batch large inputs before sending them.
Use the Azure default credential chainconnect-with-azure-identity
const connection = new Connection({
server: 'example.database.windows.net',
authentication: {
type: 'azure-active-directory-default',
options: {},
},
options: {
database: 'app',
encrypt: true,
trustServerCertificate: false,
},
});DefaultAzureCredential can try several sources. Constrain and test the deployed identity setup so local developer credentials do not hide missing production configuration.
Log every nested driver errorinspect-aggregate-errors
function reportTediousError(error: unknown) {
if (error instanceof AggregateError) {
for (const cause of error.errors) console.error(cause);
return;
}
console.error(error);
}Some authentication and request paths return AggregateError so the original SQL Server or token error is not lost behind a generic failure.
Close a connection on shutdownclose-connection
process.once('SIGTERM', () => {
connection.once('end', () => process.exit(0));
connection.close();
});In a pooled service, stop accepting work first, wait for checked-out requests, then drain the pool rather than closing one arbitrary connection.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| mssql | npm | Choose it for a Promise-first SQL Server client with pooling that uses Tedious as its default driver |
| msnodesqlv8 | npm | Choose it when native ODBC performance or Windows trusted connections justify platform-specific binaries |
| sequelize | npm | Choose it when models, associations, migrations, and cross-database ORM behavior matter more than TDS-level control |
| knex | npm | Choose it for a query builder, migrations, and pooling while retaining more SQL visibility than an ORM |