A load-testing tool for Shiny applications. shinyloadtest records a user session and replays it with concurrent workers to measure application performance under load.
Requires Node.js 20+.
Install globally via npm:
npm install -g shinyloadtestOr run directly with npx:
npx shinyloadtest --help# 1. Record a session against a running Shiny app
shinyloadtest record https://example.com/app
# 2. Interact with the app in the browser at the printed proxy URL,
# then close the browser tab to stop recording.
# 3. Replay the recording with multiple concurrent users
shinyloadtest replay recording.log https://example.com/app --workers 5
# 4. Generate a performance report
shinyloadtest reportshinyloadtest record <app-url> [options]Starts a local reverse proxy that sits between your browser and the Shiny application. All HTTP and WebSocket traffic is captured to a recording file. Navigate to the proxy URL printed on startup, interact with the app as a typical user would, then close the browser tab (or press Ctrl+C) to stop.
| Option | Description |
|---|---|
--port <n> |
Local proxy port (default: 8600) |
--host <host> |
Local proxy host (default: 127.0.0.1) |
--output <file> |
Output recording file (default: recording.log) |
--open |
Open browser automatically |
shinyloadtest replay <recording> [app-url] [options]Replays a recorded session with one or more concurrent workers. If app-url
is omitted, the target URL from the recording file is used.
| Option | Description |
|---|---|
--workers <n> |
Number of concurrent workers (default: 1) |
--loaded-duration-minutes <n> |
Minutes to maintain load after warmup (default: 5) |
--start-interval <ms> |
Milliseconds between starting workers (default: recording duration / workers) |
-H, --header <header> |
Custom HTTP header, repeatable |
--output-dir <dir> |
Directory for session logs |
--overwrite-output |
Overwrite existing output directory |
--max-errors <n> |
Stop the test after this many cumulative session failures (0 to disable, default: workers * 5) |
--debug-log |
Write verbose debug log |
--diagnostics |
Write a credential-free diagnostics event log (diagnostics.jsonl) to the output directory (default: off) |
--log-level <level> |
Console log level: debug, info, warn, error (default: warn) |
Header values that look like credentials — a case-insensitive name containing
authorization, cookie, token, secret, password, passwd, api-key,
apikey, or credential — are written as [REDACTED] in the session CSV
metadata. Replay still sends the real values. Redaction covers the files only:
-H values stay in the process argument list while the run lasts, so on a
shared machine pass secrets through SHINYLOADTEST_CONNECT_API_KEY,
SHINYLOADTEST_USER, or SHINYLOADTEST_PASS instead.
When a session fails, its worker waits before retrying: about 1s after the
first failure, doubling per consecutive failure up to 30s, with ±25% jitter
so workers that fail together don't retry in lockstep. A successful session
resets the delay. Workers waiting to retry are not active app sessions, so
the requested --workers count is not proof of achieved concurrency while
sessions are failing.
Replay is driven by what the app sends back, not by a clock, so a slow app
makes a replay slow rather than failed. A few recorded messages must arrive
before a session can continue at all — the SockJS open frame, the first
message after connecting, and upload responses, whose contents feed later
messages and URLs. Replay waits for those instead of failing, logging the
recording line it is waiting on every 30 seconds. During warmup it gives up
after 15 minutes with a Warmup timed out error naming the workers and the
frame each was waiting for. Other recorded messages that never arrive are
skipped with a warning and the session carries on.
Waiting is the normal symptom of a cold or overloaded app, and it resolves on its own. Waiting on the same line across every worker usually means the recording is stale — the app changed and no longer sends that message — in which case re-record.
Add --diagnostics when a replay hangs, fails without explaining why, or you
are about to report a bug:
shinyloadtest replay recording.log https://example.com/app --workers 5 --diagnosticsThis writes diagnostics.jsonl to the output directory: a timeline of what
each worker actually did — HTTP request timings split into startup and
playback, WebSocket open and close, receives that were skipped or still
waiting, retry delays. It holds no URLs, headers, cookies, payloads, or
credentials, so it can be attached to an issue as-is. Each replay process
writes its own file, so a test split across several processes produces one
file per output directory.
It describes the client side only. It cannot tell you whether the server
released a session, and a done outcome means playback completed, not that
the app rendered the right thing. Check the receive counts and skip reasons
before trusting a session that looks clean.
Session CSVs are not safe to share as-is. Each one embeds the command line used for the run as a comment header — the app URL, output paths, and any non-credential
-Hvalues. Credential-looking header values are[REDACTED], but the rest of the invocation is not.--debug-logrecords full WebSocket payloads; treat that file as application data, not as something to post publicly.
shinyloadtest report [dirs...] [options]Generates a performance report from the session logs produced by replay.
If no directories are specified, any test-logs-* directories in the current
working directory that contain a sessions/ subdirectory are used
automatically.
The default HTML report is a self-contained interactive dashboard with charts covering session timelines, latency, event duration, concurrency impact, and more.
| Option | Description |
|---|---|
--format <format> |
Output format: html (default), text (Markdown tables), or json |
--output <file> |
Save report to a file instead of serving/printing |
--no-open |
Do not open the HTML report in the browser |
When using the default html format without --output, the report is served
on a local HTTP server and opened in your browser. With --output, the report
is written to the specified file. The text and json formats print to stdout
unless --output is provided.
shinyloadtest supports authentication via environment variables:
| Variable | Purpose |
|---|---|
SHINYLOADTEST_USER |
Username for Shiny Server Pro or Posit Connect |
SHINYLOADTEST_PASS |
Password for Shiny Server Pro or Posit Connect |
SHINYLOADTEST_CONNECT_API_KEY |
API key for Posit Connect |
These variables are used during both recording and replay. If the app requires
login and environment variables are not set, record will prompt interactively
(TTY required).
Note: If the recording was made with a Connect API key, playback must also use a Connect API key. Likewise, if the recording was made without an API key, playback must not use one.
Report generation is built in via shinyloadtest report. The
shinyloadtest R package can still be
used for custom analysis of load test results in R.
This is a TypeScript rewrite of shinycannon, the original Kotlin/JVM load-testing tool. The rewrite uses the same recording format and output format — existing recordings and analysis workflows are fully compatible.
The shinycannon command is supported as an alias for backwards compatibility.
The legacy SHINYCANNON_USER, SHINYCANNON_PASS, and
SHINYCANNON_CONNECT_API_KEY environment variables are also still accepted.
MIT