DaffyReplay 0.2.0
dotnet tool install --global DaffyReplay --version 0.2.0
dotnet new tool-manifest
dotnet tool install --local DaffyReplay --version 0.2.0
#tool dotnet:?package=DaffyReplay&version=0.2.0
nuke :add-package DaffyReplay --version 0.2.0
DaffyReplay
DaffyReplay takes a recording of real SQL Server traffic made by DaffyTee and plays it against a test or pre-production SQL Server - as fast as possible, one request at a time, or with the recorded timings - then reports what failed that did not fail before, which row counts changed, and how the timings compare.
It replays at the wire. A DaffyTee recording made with --raw holds every request's exact TDS
bytes, so the replay sends exactly what the application sent: the same parameter types, precision
and collations, table-valued parameters, prepared statements, server cursors, bulk loads and
pooled-connection resets - with nothing installed or switched on in either database, no trace, no
Extended Events session.
What it is for
- Upgrades - record on the version you run today and replay on the one you are moving to, to
see what breaks and what slows down before you move. Most servers this matters for are older than
2022: record on SQL Server 2016, 2017 or 2019 and replay on 2022 - or go straight to 2025. Replay
first at the database's old compatibility level, then at the new one, and
comparethe two. - Compatibility levels, indexes, schema and configuration changes - replay the same recording
before and after, on the same server, and
comparethe two runs statement by statement. - Migrations - a new server, new hardware, a new platform, measured against the real workload rather than a synthetic one.
- Regression testing - does every statement the application sends still work, and return the same number of rows?
- Load and stress testing - the real workload at its recorded rate, at twice the rate, or as fast as the server will take it.
How it fits
production app ──► DaffyTee ──► production SQL Server
│ --sink file --raw --bulk full
▼
events-YYYYMMDD.ndjson
│
▼
daffyreplay run ──► test SQL Server, restored to the moment recording began
│
▼
replay.log report.json report.html (results.ndjson)
DaffyReplay records nothing itself. To record a replay - for a byte-for-byte check, say - point DaffyReplay at a DaffyTee and point that DaffyTee at the test server.
dotnet tool install -g DaffyReplay # install
dotnet tool update -g DaffyReplay # upgrade to the latest version
daffyreplay --version
Quick start
1. Record, with DaffyTee in front of the application (the recommended settings for a replay):
daffytee --sink file --sink-path "D:\capture\events-{date}.ndjson" --raw --bulk full --max-event-kb 8192
Back up the database the moment recording starts (or plan a point-in-time restore to it), and start DaffyTee before the application connects - or record just a window of a running system, as in Recording a peak window.
2. Look at what was recorded:
daffyreplay inspect "D:\capture\events-202610*.ndjson"
It says how much can be replayed, and anything that would make a replay mean less: sessions that began before the recording, events DaffyTee had to drop, requests without their raw bytes.
3. Name the server to replay into. It prompts for the password, logs in to prove it works, and stores the password encrypted:
daffyreplay target add DEV01 --target "Server=tcp:sqltest01,1433;User Id=replay_user;Encrypt=Mandatory;TrustServerCertificate=True"
4. Restore the backup on DEV01, and replay:
daffyreplay run "D:\capture\events-202610*.ndjson" --target DEV01
It shows what it is about to do and asks before starting. While it runs, the console shows elapsed
time against recorded time, sessions, requests per second, new errors and lag; Ctrl+C stops cleanly
and still writes the report for what ran. Open report.html in the run's folder.
Recording with DaffyTee
The file sink alone is enough - no RabbitMQ or ClickHouse is needed - and a busy six hours can be
several GB: DaffyReplay streams the files and never loads them whole. With {date} in the name they
roll at midnight, so give DaffyReplay a list or a wildcard. DaffyTee's own guide covers mapping
users and pointing applications at it; these are the switches that decide what a replay can do.
Recommended: everything a replay can use
daffytee --sink file --sink-path "D:\capture\events-{date}.ndjson" --raw --bulk full --max-event-kb 8192
| Switch | What DaffyTee records | What DaffyReplay does with it |
|---|---|---|
--sink file --sink-path ... |
every event as a line of JSON, one file a day | reads the files, any number, by list or wildcard |
--raw |
each request's exact bytes, with its decoded SQL and parameters | sends those bytes: the most faithful replay |
--bulk full |
bulk loads (SqlBulkCopy, bcp) with their rows |
replays them, rows and all |
--max-event-kb 8192 |
requests up to 8 MB whole (the default is 1 MB) | larger requests are cut short and cannot be replayed, so raise it if bulk loads or big parameters need it |
inspect then says the recording is whole:
daffytee 0.3.0 at daffytee://DEV01:1433: on, sampling 100% of requests, bulk full, raw payloads, passwords masked
requests 1,282: rpc 836, txn 274, batch 170, bulk 2
replayable 1,282 (100.0 %): exactly 1,282
No warnings: the recording is complete and every request can be replayed exactly.
Other choices, and what they cost a replay
Each changes what DaffyTee writes and what inspect and the report then say.
Leave bulk loads out - an ETL load in the middle of the workload is large, and often not what you are testing:
daffytee --sink file --sink-path "D:\capture\events-{date}.ndjson" --raw --bulk meta
--bulk meta, DaffyTee's default, records each bulk load's table, rows and bytes, not the rows;
--bulk off records no event for them at all. Either way the INSERT BULK statement that starts a
load is still recorded. DaffyReplay leaves each load out together with its INSERT BULK - the server
would refuse whatever came next if one were sent alone - and says so:
skipped 2: bulk loads recorded without their rows (DaffyTee --bulk meta)
skipped 2: INSERT BULK statements whose rows are not replayed (they go together)
The tables those loads filled stay as the restore left them, so statements that read them later
can report different row counts. To decide at replay time instead, record with --bulk full and
add --no-bulk to daffyreplay run.
Add a SQL log for people - the same requests as runnable T-SQL, beside the events:
daffytee --sink file --sink-path "D:\capture\events-{date}.ndjson" --raw --bulk full --max-event-kb 8192 --sql-log "D:\capture\sql-{date}.log"
DaffyReplay does not read it; it costs disk.
Decode larger responses - --response-kb 256 (64 by default). DaffyTee decodes each response up
to that size for its per-statement row counts, result-set sizes and errors, which is what the replay
compares statement by statement; larger responses are compared on their final count. It costs
DaffyTee memory per request in flight. Give daffyreplay run the same --response-kb.
Without --raw - smaller files. Batches and SqlTransaction requests still replay exactly,
rebuilt from their text and fields; procedure calls - stored procedures, and every parameterised
query (sp_executesql) - are skipped, or with daffyreplay run --approximate replayed as T-SQL with
literal values, marked approximate and compared on results and errors only. inspect warns:
- DaffyTee was not recording raw bytes (--raw): procedure calls cannot be replayed exactly
Record without --raw only with DaffyTee 0.3.1 or later. Earlier versions decoded a datetime
value wrong when its time of day was after 00:02:23, so inspect warns about procedure calls that
hold one, and --approximate skips them rather than send the wrong time.
Sampled - --sample 10% captures a random tenth of requests: sessions full of holes, which a
replay cannot make sense of. If you have to sample, sample whole connections -
--sample 25% --sample-by session - for a functional check of a quarter of the load. inspect warns
either way.
A queue for heavy traffic - events wait in memory before they are written, up to
--queue-max-mb (256). Events dropped past that (proxy.gap) are holes in the replay, and
inspect counts them; give a busy server with large bulk loads more.
Before you record
- Back up the database, or take a snapshot, the moment recording starts - and restore it before every replay. Without it, inserts hit duplicate keys, updates and deletes touch different rows, and the report is mostly noise. The report labels the errors typical of this. Under the full recovery model, a point-in-time restore to the moment recording began does the same - see Recording a peak window.
- Restoring on another server? Its logins need the same SIDs as the original's, or the restored
database's users are orphaned and every session fails to open it. Create the logins with
CREATE LOGIN ... WITH SID = ..., or remap the users after the restore (ALTER USER ... WITH LOGIN). - Start DaffyTee before the application connects where you can. A session already open when
recording began is missing its earlier state - SET options, temp tables, prepared statements - and
inspectlists them. Applications on a connection pool reset each connection when they reuse it, which drops its temp tables and resets its SET options, so for them this costs little. - Requests whose passwords DaffyTee masked (
CREATE LOGIN ... PASSWORD) carry no raw bytes and are skipped.
Recording a peak window
DaffyTee can run as a plain gateway for days and record only when asked. Its tee - the copy of each request - switches on and off at its prompt while every session carries on untouched. Say the busiest half hour is Monday at 10:00:
1. Run DaffyTee as the gateway, the tee off - every recording setting in place for when it comes on:
daffytee --sink file --sink-path "D:\capture\events-{date}.ndjson" --raw --bulk full --max-event-kb 8192 --tee off
Applications connect through it as usual and nothing is captured; the file gets only DaffyTee's own
start and heartbeat events. Keep it running in a terminal: the daffytee> prompt is where the tee
is switched, and only there - by design, an admin's T-SQL cannot switch the record of their own
actions off.
2. Have a way back to the moment the tee comes on. Under the full recovery model - most
production databases - nothing is needed now: your regular full and log backups will restore to
any moment. Under the simple model, take a COPY_ONLY full backup in a quiet moment just before,
and switch the tee on as soon as it finishes.
3. Switch it on, at DaffyTee's prompt:
daffytee> :tee on
daffytee> :status
...
tee : on, sampling 100% of requests, bulk full, raw payloads, passwords masked
4. Half an hour later, switch it off:
daffytee> :tee off
Events already captured are still written. Use :tee off, not :quit: :quit (and Ctrl+C) stops
DaffyTee itself and disconnects every session, which in production is an outage. DaffyTee writes a
proxy.control event each time, saying who switched the tee and when.
5. Restore the test server to the moment the tee came on. inspect shows it, in UTC:
daffyreplay inspect "D:\capture\events-20261012.ndjson"
daffytee 0.3.0 at daffytee://DEV01:1433: on, sampling 100% of requests, bulk full, raw payloads, passwords masked
changed 2026-10-12 15:00:02 tee on (by console)
changed 2026-10-12 15:30:05 tee off (by console)
sessions 214: 37 began during the recording, 177 before it
Take a log backup, and on the test server restore the last full backup, then the log backups up to
that moment. The proxy.control event in the file has the time to the millisecond; STOPAT takes
the SQL Server's local time (here UTC-5):
RESTORE DATABASE Sales FROM DISK = N'E:\backup\Sales_full.bak' WITH NORECOVERY, REPLACE;
RESTORE LOG Sales FROM DISK = N'E:\backup\Sales_log_0945.trn' WITH NORECOVERY;
RESTORE LOG Sales FROM DISK = N'E:\backup\Sales_log_1015.trn' WITH STOPAT = '2026-10-12T10:00:02.250', RECOVERY;
Then put the identity columns back where production had them. SQL Server hands out identity values from a cached block, and a database recovered to a point in time carries on after that block: the replay's inserts would get other ids than production gave, and every later statement that names one of them would read a different row. In each restored database:
DECLARE @table nvarchar(400), @column nvarchar(300), @sql nvarchar(max);
DECLARE identities CURSOR LOCAL FAST_FORWARD FOR
SELECT QUOTENAME(SCHEMA_NAME(t.schema_id)) + N'.' + QUOTENAME(t.name), QUOTENAME(c.name)
FROM sys.identity_columns AS c JOIN sys.tables AS t ON t.object_id = c.object_id
WHERE t.is_ms_shipped = 0 AND CONVERT(bigint, c.increment_value) > 0;
OPEN identities;
FETCH identities INTO @table, @column;
WHILE @@FETCH_STATUS = 0
BEGIN
SET @sql = N'DECLARE @max bigint = (SELECT MAX(' + @column + N') FROM ' + @table + N');
IF @max IS NOT NULL DBCC CHECKIDENT (''' + REPLACE(@table, N'''', N'''''') + N''', RESEED, @max) WITH NO_INFOMSGS;';
EXEC sys.sp_executesql @sql;
FETCH identities INTO @table, @column;
END;
CLOSE identities;
DEALLOCATE identities;
6. Replay the window:
daffyreplay run "D:\capture\events-20261012.ndjson" --target DEV01
inspect and the run warn that sessions began before the recording - the application's pooled
connections, open since long before - which is expected here. Right at the start, expect a new
error or two: a transaction that was open when the tee came on has its first statements neither in
the recording nor, uncommitted at that moment, in the restored database, so on replay its later
statements run on their own and its commit fails ("The COMMIT TRANSACTION request has no
corresponding BEGIN TRANSACTION", 3902).
Keeping the tee on all the time works too, disk allowing: replay any window of it with --from and
--to, and restore to --from the same way.
Targets and logins
Passwords are never recorded, so the logins a replay uses come from the target. By default every session logs in as the target's own login. Sessions recorded as a particular SQL Server login - or from a particular DaffyTee login - can be mapped to logins of their own, so permissions behave as they did:
daffyreplay target logins DEV01 add svc_reports --login svc_reports # recorded SQL Server login
daffyreplay target logins DEV01 add daffytee:alice --login alice_test # sessions of DaffyTee login alice
| Command | What it does |
|---|---|
target add NAME [--target "..."] |
a target: server, encryption, default login. Logs in first; saves nothing if that fails |
target list, target show NAME |
the targets, never a password |
target test NAME |
log in again now, as the default login and every mapped one |
target logins NAME [add RECORDED \| remove RECORDED] |
the mapped logins |
target remove NAME |
delete a target and its credentials |
Targets are stored encrypted (AES-256-GCM, its key protected by DPAPI on Windows, owner-only on
Linux) in %LOCALAPPDATA%\DaffyReplay, or --config-dir / DAFFYREPLAY_HOME. Encryption is
Mandatory (TDS 7.4) unless the connection string says Encrypt=Strict (TDS 8.0, which needs a
certificate configured on the server) or Encrypt=Optional. An Entra ID application
(Authentication=ActiveDirectoryServicePrincipal, for Azure SQL) is supported with DaffyTee's own
code for it, but has not yet been tested with DaffyReplay.
Running a replay
--pace |
What it does | Good for |
|---|---|---|
recorded (default) |
Each request starts at its recorded offset from the start. --speed 2 halves every gap. A session stays sequential, as the application was: if its previous request has not finished, the next waits, and the wait is reported as lag. |
Real-world load |
fast |
No waiting: every session sends its requests back to back, sessions concurrently. --max-sessions caps how many. |
Throughput and stress |
serial |
One request at a time across all sessions, in recorded order. A request still running after --serial-wait seconds (2) is almost always blocked by a lock another session releases later in the sequence; the replay carries on, as the recording did, and counts it. |
Functional regression, and per-statement timings without concurrency |
Filters work for both inspect and run: --from / --to (recorded time, UTC), --app,
--login, --database (all accept *), --session N, --exclude (a regular expression matched
against the SQL), and --no-bulk (bulk loads and their INSERT BULK statements).
Other options: --request-timeout S cancels a request after S seconds, as a client's command timeout
would (30 by default in serial pace); --replay-cancels cancels a request that was cancelled when
recorded after as long as it ran then; --keep-app-name and --keep-host-name log in with each
session's recorded application and host name, for when APP_NAME() matters (Resource Governor
classifiers, application logic); --results also writes every request's result; --out DIR names
the run's folder; --yes skips the question, for scripts.
Safety. DaffyReplay refuses a target that is the server the recording was made on - compared by
host and port, and by the addresses they resolve to - unless you add --same-target (a restored
pre-production server can be what you mean). It never replays DaffyTee's own daffytee.* commands,
refused logins, or distributed-transaction requests. Everything else the application did - inserts,
updates, deletes, schema changes - it does again.
Examples
Restore the target before each run; every example assumes it.
Does everything still work? One request at a time, in recorded order - the cleanest functional check, and each statement's timing without concurrency in the way:
daffyreplay run "D:\capture\events-20261012.ndjson" --target DEV01 --pace serial
Production's load, as it happened. The default pace; the console and the report show whether the replay kept up (lag) and how each statement's time compares:
daffyreplay run "D:\capture\events-20261012.ndjson" --target DEV01
Twice production's load, or everything as fast as the server takes it, 50 sessions at a time:
daffyreplay run "D:\capture\events-20261012.ndjson" --target DEV01 --speed 2
daffyreplay run "D:\capture\events-20261012.ndjson" --target DEV01 --pace fast --max-sessions 50
An upgrade from 2019 to 2022. Restore the 2019 backup on the 2022 server; the database keeps compatibility level 150. Replay, restore again, raise the level, replay again, and compare the runs statement by statement:
daffyreplay run "D:\capture\events-20261012.ndjson" --target SQL2022 --out D:\replays\level-150
-- restore again, then:
ALTER DATABASE Sales SET COMPATIBILITY_LEVEL = 160;
daffyreplay run "D:\capture\events-20261012.ndjson" --target SQL2022 --out D:\replays\level-160
daffyreplay compare D:\replays\level-150 D:\replays\level-160
The same goes for an index, a configuration change or new hardware: one run before, one after.
One application, one hour:
daffyreplay run "D:\capture\events-202610*.ndjson" --target DEV01 --app "OrdersApi*" --from 2026-10-12T15:00:00Z --to 2026-10-12T16:00:00Z
Leave things out - a nightly purge that would empty the tables the rest reads, and the bulk loads:
daffyreplay run "D:\capture\events-20261012.ndjson" --target DEV01 --exclude "^exec dbo\.PurgeHistory" --no-bulk
When the application name matters - Resource Governor classifies sessions by it, or the
application's own logic reads APP_NAME() - log in as each session did; and cancel requests as the
application's command timeouts did:
daffyreplay run "D:\capture\events-20261012.ndjson" --target DEV01 --keep-app-name --keep-host-name --replay-cancels
A recording made without --raw, its procedure calls rendered as T-SQL:
daffyreplay run "D:\capture\events-20261012.ndjson" --target DEV01 --approximate
In a script or a pipeline. --yes skips the question and report.json holds the results; the
run's exit code says only whether it ran (0), was interrupted (130) or could not start (1):
daffyreplay run "D:\capture\events-20261012.ndjson" --target DEV01 --pace serial --yes --out D:\replays\nightly
$report = Get-Content D:\replays\nightly\report.json | ConvertFrom-Json
if ($report.outcomes.counts.new_error -gt 0) { throw "$($report.outcomes.counts.new_error) new errors" }
What a replay does with each request
- Raw bytes are sent as recorded, framed at the connection's packet size, with two things changed because they must be: the transaction descriptor (every request inside a transaction names it, and the replay server gives out its own; DaffyReplay follows the server's transaction announcements and writes the current one into each request, exactly as SqlClient does), and the outstanding-request count, which is 1.
- Prepared statements and server cursors (
sp_prepexec,sp_execute,sp_cursorprepexec,sp_cursorfetch, ...) name handles the server gave out. Each recorded handle is mapped to the one the replay was given, and a cursor's fetches are read by the column metadata that came with its open. - Pooled-connection resets are sent as the reset flag on the request that carried it.
- Bulk loads replay with their rows, together with the
INSERT BULKstatement that announces them - or, without their rows, not at all. A client that negotiated column encryption at login - as SqlClient always does - lays out its bulk metadata differently, so each session logs in the way its recorded bulk loads show its client did. - MARS sessions replay in recorded order on one connection.
- Without raw bytes, a batch is rebuilt exactly from its text and a transaction request from its
fields; a procedure call is skipped, or with
--approximaterendered as T-SQL with its values as literals - marked approximate in the report and compared on its results and errors only.
The report
Each run writes a folder (--out, by default replay-YYYYMMDD-HHMMSS):
| File | What is in it |
|---|---|
report.html |
The report: one self-contained page, readable offline and in light or dark |
report.json |
The same, for scripts and for compare |
run.json |
What was replayed, where, how - never a password |
replay.log |
DaffyReplay's own log, JSON lines in DaffyTee's format |
results.ndjson |
With --results: every request, recorded and replay side by side, loadable next to the recording |
Outcomes. Each request against its recording: the same (ok both times, or the same errors), new error (ok when recorded, failed on replay), changed error, fixed (failed when recorded, ok now), timed out, failed (the connection broke), not replayed (its session could not log in). New errors are grouped by error number and statement; duplicate-key and foreign-key errors are labelled as typical of data that differs from the recording's.
Row counts. Every statement's row count and every result set's size are compared, where
DaffyTee decoded the recorded response (it does for small ones); for larger ones, the final count.
After a restore, differences usually come from server-generated values - identities, NEWID(),
GETDATE() - and from concurrency ordering differently than it did in production; serial pace
reproduces them most closely.
Timing. p50, p95, p99 and maximum, recorded against replayed, overall and for each statement, with the statements that got slower or faster by total time and by p95. A statement is its procedure for a procedure call, otherwise its SQL with literals replaced and whitespace and case normalised. Recorded timings come from production's hardware under production's concurrency; the most telling comparison is one replay against another on the same server:
daffyreplay compare replay-before replay-after
Pace. In recorded pace, how late requests started behind the recorded schedule, split into
waiting on the server (the session's previous request was still running) and the replay host
falling behind (which would make the results understate the server), and requests per interval,
recorded against replayed.
daffyreplay report RUN_DIR writes report.html again from report.json.
From RabbitMQ and ClickHouse
The file sink is the simple path to a replay. If you already publish DaffyTee's events to RabbitMQ
and a subscriber keeps them in ClickHouse, a slice of them becomes an ordinary event file - each row
must be the whole event, exactly as DaffyTee published it (with --raw):
clickhouse client --query "SELECT event FROM daffytee.events
WHERE time BETWEEN '2026-10-12 15:00:00' AND '2026-10-12 15:30:00'
ORDER BY time FORMAT TSVRaw" > slice.ndjson
daffyreplay inspect slice.ndjson
The table and column names are your subscriber's. A slice has no proxy.start, which inspect
mentions and is fine; restore to the slice's start as for a peak window.
To and from sqlsim
sqlsim (Microsoft's open-source sqlsimtools, in public
preview) replays a workload from its own replay XML, which its convert-xel.ps1 makes from an
Extended Events capture. DaffyReplay writes that XML from a DaffyTee recording, and reads it into a
recording it can run.
Export: a DaffyTee recording for sqlsim
daffyreplay export sqlsim "D:\capture\events-*.ndjson" --out D:\capture\replay.xml
sqlsim -S testsql01 -d Shop -E -replay fast -replayfile D:\capture\replay.xml
Every request goes in as T-SQL. Batches go as recorded. Procedure calls go as --approximate writes
them: declared types, table-valued and OUTPUT parameters as variables, prepared statements as
sp_executesql. Transactions the application ran through its driver go as BEGIN, COMMIT,
ROLLBACK and SAVE TRANSACTION, which an Extended Events capture never has. The filters
(--from, --to, --app and the rest) work as for run. What cannot be written as text - bulk
loads, server cursor calls, statements prepared before the recording began - is counted instead.
A file holds at most 100,000 events and 100 MB, sqlsim's limits; past that the export goes on in
replay-2.xml and on, each replayed on its own.
-replay fast gives each recorded session a connection of its own; -replay stress -n 1 -r 1 runs
every request on one connection in recorded order. sqlsim logs every session in as the one login it
is given, does not reset a pooled connection between uses, and has no recorded pace; the export
lists what that means for your recording.
Import: sqlsim's XML as a recording
daffyreplay import sqlsim D:\capture\replay.xml --out D:\capture\imported.ndjson
daffyreplay inspect D:\capture\imported.ndjson
daffyreplay run D:\capture\imported.ndjson --target TEST01 --pace serial
Each batch and procedure call becomes a request DaffyReplay sends as T-SQL: Extended Events writes a
procedure call as the T-SQL that makes it. Connections are kept as they were used - a pooled one
reused carries its reset - and each keeps its requests in order. DaffyReplay reads only this XML,
never an .xel file: for one, run sqlsim's convert-xel.ps1 first. --default-database names the
database for sessions whose events do not.
An Extended Events capture knows less than a DaffyTee recording, and the report says so first, in its own box:
- Transactions the application ran through its driver are not in it - SqlTransaction, EF Core,
JDBC with autocommit off. Their statements replay one at a time, and work the application rolled
back is committed.
BEGIN TRANwritten in the SQL is there. - Errors raised while a statement ran were recorded as OK - a divide by zero, a duplicate key, a RAISERROR - so a new error may have happened when recorded too.
- Rows are known per batch, not per statement. A batch's total is compared with the replay's; a procedure call's rows are not compared, since the text Extended Events wrote for it adds statements of its own.
- Every request replays as text, with literal values rather than typed parameters.
- Logins. sqlsim's capture does not record them, so sessions replay as the target's login - unless
the capture had the
usernameaction, which the import carries through.
Good to know
- Only what went through DaffyTee is replayed. SQL Agent jobs, other applications, and the work procedures and triggers do inside the server are not separate requests; the server re-runs procedure and trigger bodies itself.
- Login options. DaffyTee 0.3 does not record the options each application logged in with; replayed sessions log in as SqlClient does (packet size 8000, the login's default language), and the report says so.
- Non-determinism -
GETDATE(),NEWID(), plans the optimizer chooses differently - makes some differences expected. - SQL Server versions. Tested: recordings from SQL Server 2022, replayed on 2022 and on 2025. 2016, 2017 and 2019 speak the same protocol (TDS 7.4) and are expected to work as either, but have not been tested yet.
- Named instances by name are not supported: give the instance's port (
sqltest01,50123). - The results and the report hold SQL and parameter values. Treat them as you treat the recording.
What has been tested
Recordings made through DaffyTee 0.3.0 and 0.3.1 from four client stacks - Microsoft.Data.SqlClient
(ADO.NET), Entity Framework Core 10, the Microsoft ODBC Driver 18 and the Microsoft JDBC Driver
13.6 - replay into a restored copy on SQL Server 2022 with no new errors and no row-count
differences, at serial and recorded pace: ad-hoc and parameterised SQL, stored procedures with OUTPUT
parameters and return values, table-valued parameters, nearly every data type including MAX values,
SqlTransaction with savepoints, ODBC and JDBC manual-commit transactions, prepared statements and
JDBC server cursors, pooled-connection resets, bulk copy, MARS, temp tables, USE, multiple result
sets, large results, errors, cancels, and a login mapped separately from the default. Replayed
through a second DaffyTee, every request matched its recording byte for byte apart from the
transaction descriptor. A SQL Server 2022 recording also replays on SQL Server 2025 over TDS 8.0
(Encrypt=Strict), and has been replayed into Azure SQL Database, logging in with SQL authentication
and following its redirect. At recorded pace the scheduler holds the recorded timings to within 0.2
ms at p99, and requests start within 1 ms of them, on Windows and Linux. Procedure calls recorded
without raw bytes replay approximately with no new errors, a datetime at 13:14:15.123 among their
parameters. A peak window - the tee switched on and off at DaffyTee's prompt in front of a running
pooled workload, bulk loads recorded without their rows - replays after a point-in-time restore with
no failed sessions, and in two runs at most one new error and three row-count differences out of
about 15,000 requests.
With sqlsim 1.0.0, one workload of all four stacks, captured at once by DaffyTee and by sqlsim's Extended Events capture: exported, 99 % of the requests replayed in sqlsim and every table but the bulk-loaded one ended as the source did, with no failure that had not happened when recorded other than temp tables a reused connection kept. sqlsim 1.0.0 replaying its own capture of the same workload failed on most procedure calls: it binds parameters as ODBC sends them, and SqlClient's and JDBC's named values and table-valued parameters fail there. Imported, the capture replayed whole in DaffyReplay; its new errors were only those Extended Events recorded as OK, and an EF Core savepoint without the transaction its driver began; 310 of 318 batches' row totals agreed.
Not yet tested: Entra ID targets, and SQL Server versions before 2022.
Protocol and trademarks
DaffyReplay implements the Tabular Data Stream protocol from Microsoft's published [MS-TDS] specification; it contains no Microsoft code and is not affiliated with, endorsed by, or sponsored by Microsoft Corporation. Microsoft, SQL Server, Azure and Entra are trademarks of Microsoft Corporation. Java is a trademark of Oracle and/or its affiliates. RabbitMQ is a trademark of Broadcom Inc. ClickHouse is a trademark of ClickHouse, Inc.
License
DaffyReplay is licensed under the Business Source License 1.1, with the same grant as DaffyTee: source-available, not open source.
- Free for everyone in development, testing, staging, UAT and local debugging.
- Free in production for personal, academic and non-profit use, and for small organizations (under USD 1,000,000 in revenue and fewer than 10 people, counting affiliates).
- Any other organization may run it in production free for 60 days, on one application or workload, to evaluate it.
- Offering DaffyReplay to others as a hosted or managed service needs a commercial license.
- Each version becomes MIT-licensed four years after it is published.
The full terms, which are what count, are in LICENSE.txt in the package. Copyright (c) 2026
TroBeeOne LLC. Its one bundled dependency keeps its own license, listed in THIRD-PARTY-NOTICES.txt:
System.Security.Cryptography.ProtectedData (MIT).
Contact
TroBeeOne LLC: trobee.one · LinkedIn
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
0.2.0: To and from sqlsim (Microsoft sqlsimtools). export sqlsim writes a DaffyTee recording as sqlsim's replay XML, for sqlsim and sqlsim Studio: every request as T-SQL, procedure calls with their declared types, transactions the application ran through its driver as BEGIN/COMMIT/ROLLBACK TRANSACTION, in files within sqlsim's limits. import sqlsim reads sqlsim's replay XML (an Extended Events capture) into a recording DaffyReplay runs: pooled connections and resets kept, and the report says first what an Extended Events capture cannot tell (transactions opened through the driver, errors raised while a statement ran, rows per statement); an imported batch is compared on its row total. Replay and comparison of DaffyTee recordings are unchanged. Binaries signed by TroBeeONE LLC.
0.1.1: The binaries are now Authenticode-signed by TroBeeONE LLC. Decodes datetime values as DaffyTee 0.3.1 does, rounded to the millisecond as SQL Server shows them (.000, .003, .007), so --approximate writes them as literals SQL Server accepts. DaffyTee 0.3.0 and earlier decoded a datetime after 00:02:23 wrong unless the recording has raw bytes: inspect warns about procedure calls without raw bytes that hold such values, and run --approximate skips them rather than send the wrong time.
0.1.0: First release. Replays DaffyTee (0.3.0 or later) recordings against a test SQL Server at recorded pace (with --speed), fast or serial; byte-exact from --raw recordings, with transaction descriptors patched and prepared-statement and cursor handles mapped; approximate T-SQL for procedure calls recorded without raw bytes. inspect, target, run, report and compare commands; report.json and a self-contained report.html. Tested with ADO.NET, EF Core, ODBC and JDBC workloads on SQL Server 2022 and 2025.