
Load Testing
- 8 installs
- 179 repo stars
- Updated July 28, 2026
- databricks/app-templates
load-testing skill documents Load test a Databricks App to find its maximum QPS.
About
load-testing skill documents Load test a Databricks App to find its maximum QPS. Use when: (1) User says 'load test', 'benchmark', 'QPS', 'throughput', or 'performance test', (2) User wants to find how many queries per second their app can handle, (3) User wants to set up load testing scripts for their agent, (4) User wants to . name: load-testing description: "Load test a Databricks App to find its maximum QPS. Use when: (1) User says 'load test', 'benchmark', 'QPS', 'throughput', or 'performance test', (2) User wants to find how many queries per second their app can handle, (3) User wants to set up load testing scripts for their agent, (4) User wants to view load test results/dashboard."
- Load test a Databricks App to find its maximum QPS.
- Goal: Find the maximum QPS (queries per second) your Databricks App can support.
- Users at Peak - Concurrent users when peak QPS was achieved. More users beyond this doesn't help.
- Platform-specific setup patterns for load-testing.
- Evidence-backed steps from upstream SKILL.md.
Load Testing by the numbers
- 8 all-time installs (skills.sh)
- Ranked #12,321 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
load-testing capabilities & compatibility
- Capabilities
- load testing quick start · load testing when to use guidance · load testing integration patterns
- Works with
- databricks
- Use cases
- orchestration
What load-testing says it does
**Goal:** Find the maximum QPS (queries per second) your Databricks App can support.
Before beginning, use the `AskUserQuestion` tool to collect the following from the user:
npx skills add https://github.com/databricks/app-templates --skill load-testingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 8 |
|---|---|
| repo stars | ★ 179 |
| Last updated | July 28, 2026 |
| Repository | databricks/app-templates ↗ |
How do I use load-testing correctly?
Load test a Databricks App to find its maximum QPS. Use when: (1) User says 'load test', 'benchmark', 'QPS', 'throughput', or 'performance test', (2) User wants to find how many queries per second the
Who is it for?
Teams implementing load-testing workflows from the catalog.
Skip if: Skip when requirements clearly match a different specialized stack.
When should I use this skill?
User asks about load-testing, load test a databricks app to find its maximum qps. use when: (1) user says 'load test', '.
What you get
Working load-testing setup with validated configuration and next steps.
Files
Load Testing Your Databricks App
Goal: Find the maximum QPS (queries per second) your Databricks App can support.
Before You Start — Gather Parameters
Before beginning, use the AskUserQuestion tool to collect the following from the user:
1. Do they already have deployed apps to test, or do they need to set up new apps? 2. Do they want to mock LLM calls? Mocking isolates infrastructure throughput from LLM latency — useful for capacity planning. Testing without mocks measures end-to-end performance. 3. What compute sizes do they want to test? (Medium, Large, or both) 4. How many worker configurations do they want to test? (e.g., 2, 4, 6, 8 workers) 5. Do they have M2M OAuth credentials (service principal client_id/client_secret)? — Recommended for tests longer than ~30 minutes. If not, guide them to create one. 6. What is their `DATABRICKS_HOST`? (workspace URL)
---
Step 1: Set Up Load Testing Scripts
Create a load-test-scripts/ directory in the project with the following files. These scripts are framework-agnostic and work with any Databricks App.
Directory Structure
<project-root>/
agent_server/ # Existing agent code
load-test-scripts/ # Load testing scripts (create this)
run_load_test.py # Main CLI — orchestrates Locust tests
locustfile.py # Locust test definition (SSE streaming, TTFT tracking)
dashboard_template.py # Generates interactive HTML dashboard from results
.env.example # Template for env vars
load-test-runs/ # Test results (auto-created per run)
<run-name>/
dashboard.html # Interactive dashboard
test_config.json # Test parameters
<label>/ # Per-config Locust CSV resultsRequired Files
`locustfile.py` — Locust load test that:
- Sends
POST /invocationswith{"input": [...], "stream": true}to the app - Parses SSE stream (
data: {json}lines) and counts chunks untildata: [DONE] - Tracks TTFT (time to first
data:line) as a custom Locust metric - Uses M2M OAuth token exchange (
client_credentialsgrant to{host}/oidc/v1/token) with auto-refresh - Implements
StepRampShape— ramps users fromstep_sizetomax_users, holding each level forstep_durationseconds
`run_load_test.py` — CLI orchestrator that:
- Accepts
--app-url(repeatable),--client-id,--client-secret,--max-users,--step-size,--step-duration,--run-name,--dashboard,--compute-size,--labelflags - Tests each app URL sequentially (isolated metrics per config)
- Refreshes OAuth token before each app
- Runs healthcheck + warmup before each test
- Saves results to
load-test-runs/<run-name>/<label>/ - Generates dashboard at the end if
--dashboardis passed
`dashboard_template.py` — Generates a self-contained HTML dashboard with Chart.js:
- KPI cards (best config, peak QPS, lowest latency, total requests)
- Bar charts: QPS by config (median + peak), latency (p50 + p95), TTFT, total requests
- QPS Ramp Progression: line charts with QPS/Latency/Failures tabs and a max-users slider
- Grouped by compute size (medium/large side-by-side)
- Full results table with peak QPS, users at peak, latency percentiles, failure rate
- Can be run standalone:
uv run dashboard_template.py ../load-test-runs/<run-name>/
Install Dependencies
The load testing scripts use their own pyproject.toml inside load-test-scripts/ to avoid polluting the agent's production dependencies.
# load-test-scripts/pyproject.toml
[project]
name = "load-test-scripts"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
"locust>=2.32,<2.40",
"urllib3<2.3",
"requests",
]Then install from within the load-test-scripts/ directory:
cd load-test-scripts/
uv syncNote:locust>=2.43has a knownRecursionError. Pin to<2.40to avoid it.
---
Step 2 (Optional): Mock Your Agent for Load Testing
Mocking is optional — you can skip this step to test your real agent end-to-end (including LLM latency). However, mocking is useful for:
- Capacity planning — isolating Apps infrastructure throughput from LLM latency (which adds 1-30s per request)
- Cost savings — avoiding FMAPI/token usage during load tests
- Reproducibility — getting consistent measurements independent of LLM response variability
How to Mock
The mock timing is controlled by two environment variables (set in app.yaml or databricks.yml):
MOCK_CHUNK_DELAY_MS— delay between text chunks in milliseconds (default:10)MOCK_CHUNK_COUNT— number of text chunks per response (default:80)
For OpenAI Agents SDK templates: Create a MockAsyncOpenAI client that replaces AsyncDatabricksOpenAI. It simulates tool call streaming (instant) and text response streaming (delayed chunks). A reference implementation is available at `examples/mock_openai_client.py`:
from agent_server.mock_openai_client import MockAsyncOpenAI
set_default_openai_client(MockAsyncOpenAI())
set_default_openai_api("chat_completions")For LangGraph templates: Replace the ChatDatabricks model with a mock that returns pre-built AIMessage objects with tool calls and text content using configurable delays.
For custom agents: Wrap whatever external API calls you make (LLM, vector search, etc.) with mock implementations that return realistic response shapes.
---
Step 3: Deploy Load Testing Apps
Deploy multiple Databricks Apps with varying compute sizes and worker counts.
Recommended Test Matrix
| Compute Size | Workers | App Name |
|---|---|---|
| Medium | 2 | <your-app>-medium-w2 |
| Medium | 4 | <your-app>-medium-w4 |
| Medium | 6 | <your-app>-medium-w6 |
| Medium | 8 | <your-app>-medium-w8 |
| Large | 6 | <your-app>-large-w6 |
| Large | 8 | <your-app>-large-w8 |
| Large | 10 | <your-app>-large-w10 |
| Large | 12 | <your-app>-large-w12 |
Configuring Compute Size
Databricks CLI:
databricks apps create <app-name> --compute-size MEDIUM
databricks apps update <app-name> --compute-size LARGEDatabricks UI: Go to Compute > Apps > your app > Edit > Configure > Compute.
Configuring Worker Count
start-server (via AgentServer.run()) accepts a --workers flag directly. Pass the worker count in the command array using a DAB variable — no wrapper script needed:
variables:
app_name:
default: "my-agent-medium-w2"
workers:
default: "2"
resources:
apps:
load_test_app:
name: ${var.app_name}
source_code_path: .
config:
command: ["uv", "run", "start-server", "--workers", "${var.workers}"]
env:
- name: MOCK_CHUNK_DELAY_MS
value: "10"
- name: MOCK_CHUNK_COUNT
value: "80"
targets:
medium-w2:
default: true
variables:
app_name: "my-agent-medium-w2"
workers: "2"
large-w8:
variables:
app_name: "my-agent-large-w8"
workers: "8"Deploying
databricks bundle deploy --target medium-w2
databricks bundle run load_test_app --target medium-w2Verify apps are ACTIVE before proceeding:
databricks apps get <app-name> --output json | jq '{app_status, compute_status, url}'---
Step 4: Run Load Tests
Authentication — M2M OAuth (Required for Long Tests)
Load tests can run for hours. U2M OAuth tokens expire and break your test mid-run. Use M2M (machine-to-machine) OAuth with a service principal instead.
export DATABRICKS_HOST=https://your-workspace.cloud.databricks.com
export DATABRICKS_CLIENT_ID=<your-client-id>
export DATABRICKS_CLIENT_SECRET=<your-client-secret>Parameters Reference
| Parameter | Required | Default | Description |
|---|---|---|---|
--app-url | Yes | — | App URL(s) to test (repeatable) |
--client-id | Recommended | DATABRICKS_CLIENT_ID env | Service principal client ID |
--client-secret | Recommended | DATABRICKS_CLIENT_SECRET env | Service principal client secret |
--label | No | Auto-derived from URL | Human-readable label per app (repeatable) |
--compute-size | No | Auto-detected or medium | Compute size tag per app: medium, large (repeatable) |
--max-users | No | 300 | Maximum concurrent simulated users |
--step-size | No | 20 | Users added per ramp step |
--step-duration | No | 30 | Seconds per ramp step |
--spawn-rate | No | 20 | User spawn rate (users/sec) |
--run-name | No | <timestamp> | Name for this run — results saved to load-test-runs/<run-name>/ |
--dashboard | No | Off | Generate interactive HTML dashboard after tests |
Example Commands
cd load-test-scripts/
# Quick single-app test:
uv run run_load_test.py \
--app-url https://my-app.aws.databricksapps.com \
--client-id <ID> --client-secret <SECRET> \
--dashboard --run-name quick-test
# Full matrix — 8 apps, overnight:
uv run run_load_test.py \
--app-url https://my-app-medium-w2.aws.databricksapps.com \
--app-url https://my-app-medium-w4.aws.databricksapps.com \
--app-url https://my-app-large-w8.aws.databricksapps.com \
--app-url https://my-app-large-w10.aws.databricksapps.com \
--compute-size medium --compute-size medium \
--compute-size large --compute-size large \
--max-users 1000 --step-size 20 --step-duration 10 \
--dashboard --run-name overnight-sweep
# Multiple runs for statistical consistency:
for RUN in r1 r2 r3 r4 r5; do
uv run run_load_test.py \
--app-url ... \
--client-id <ID> --client-secret <SECRET> \
--max-users 1000 --step-size 20 --step-duration 10 \
--run-name my_test_${RUN} --dashboard || break
doneWhat Happens During a Run
1. Healthcheck — verifies the app streams correctly (receives [DONE]) 2. Warmup — sends sequential requests to warm up the app 3. Ramp-to-saturation — steps up concurrent users every step_duration seconds 4. When QPS plateaus despite adding users, you've found the saturation point
Estimated Duration
(max_users / step_size) * step_durationseconds per app- With defaults:
(300 / 20) * 30 = 15 steps * 30s = ~7.5 minper app - For 4 apps: ~30 min per run
---
Step 5: View Results Dashboard
Opening the Dashboard
open load-test-runs/<run-name>/dashboard.htmlRegenerating the Dashboard
cd load-test-scripts/
uv run dashboard_template.py ../load-test-runs/<run-name>/What the Dashboard Shows
- KPI Cards — Best config (peak QPS), overall peak QPS, lowest latency, total requests
- QPS by Config — Grouped bars showing median QPS and peak QPS side-by-side
- Latency by Config — Grouped bars showing p50 and p95 latency
- TTFT by Config — Time to first token (p50 and p95)
- Total Requests Served — How many requests each config handled
- QPS Ramp Progression — Line charts with tabs for QPS, QPS (excl. failures), Latency, and Failures. Includes a max-users slider to zoom into lower concurrency ranges. Charts are grouped by compute size (medium/large).
- Full Results Table — All configs with peak QPS, users at peak, latency percentiles, and failure rate
- Load Test Parameters — Summary of test configuration for reproducibility
Interpreting Results
- Peak QPS — Maximum QPS at any ramp step. This is the throughput ceiling.
- Users at Peak — Concurrent users when peak QPS was achieved. More users beyond this doesn't help.
- Failure Rate — Should be 0% or very low. High rates mean the app is overloaded.
- QPS Ramp Chart — Look for where the line flattens. That's the saturation point.
---
Troubleshooting
| Issue | Solution |
|---|---|
| Auth token expired mid-test | Use M2M OAuth (--client-id/--client-secret) instead of static tokens |
| Healthcheck fails | Verify app is ACTIVE: databricks apps get <name> --output json |
| 0 QPS / no results | Check load-test-runs/<run-name>/<label>/locust_output.log for errors |
| Low QPS despite high user count | App is saturated — try more workers or larger compute |
| High failure rate | App is overloaded — reduce --max-users or increase workers/compute |
| Dashboard shows no ramp data | Ensure results_stats_history.csv exists in each result subdir |
"""
Mock AsyncOpenAI client that simulates LLM streaming responses.
Handles two types of calls in the agent flow:
Call 1 (tool call): When messages don't contain tool results, returns a
streamed tool call for get_current_time().
Call 2 (summarize): When messages contain tool results, returns a streamed
text response summarizing the time.
Uses MOCK_CHUNK_DELAY_MS and MOCK_CHUNK_COUNT env vars to control timing.
"""
import asyncio
import os
import time
import uuid
# Response text for the summary (call 2) — split into chunks during streaming
SUMMARY_TEXT = (
"The current time is 2026-04-01T00:00:00+00:00. "
"That's April 1st, 2026 at midnight UTC. "
"Time zones are regions of the Earth that observe a uniform standard time. "
"They are based on longitudinal divisions of the globe, generally 15 degrees wide, "
"corresponding to one-hour intervals from Coordinated Universal Time (UTC). "
"The concept of standard time zones was first proposed in the late 19th century "
"to replace the many local solar times that were previously used. "
"Before time zones, each city set its clocks according to the local position of the sun, "
"which made scheduling train services and telegraph communications very difficult. "
"The adoption of time zones greatly simplified commerce and travel across long distances. "
"Today there are 24 primary time zones, though some regions use offsets of 30 or 45 minutes. "
"Countries like India use a single time zone for the entire nation despite spanning a wide longitude, "
"while countries like Russia and the United States use multiple time zones. "
"Daylight saving time further complicates the picture, as many regions shift their clocks forward "
"by one hour during summer months to extend evening daylight. "
"Modern computing systems typically store times in UTC and convert to local time zones for display, "
"which avoids many of the ambiguities that arise from daylight saving transitions "
"and varying regional offset rules."
)
def _get_config():
chunk_count = int(os.environ.get("MOCK_CHUNK_COUNT", "80"))
chunk_delay_s = int(os.environ.get("MOCK_CHUNK_DELAY_MS", "10")) / 1000
return chunk_count, chunk_delay_s
def _has_tool_output(messages):
"""Check if any message contains tool/function output (indicating call 2)."""
for msg in messages:
if isinstance(msg, dict):
role = msg.get("role", "")
if role == "tool":
return True
elif hasattr(msg, "role") and msg.role == "tool":
return True
return False
def _make_chunk(chunk_id, content=None, tool_calls=None, finish_reason=None, usage=None):
"""Build a ChatCompletionChunk-like object using SimpleNamespace for attribute access."""
from types import SimpleNamespace
delta = SimpleNamespace(
content=content,
role="assistant" if (content is not None or tool_calls is not None) else None,
tool_calls=tool_calls,
refusal=None,
function_call=None,
)
choice = SimpleNamespace(
delta=delta,
finish_reason=finish_reason,
index=0,
logprobs=None,
)
chunk = SimpleNamespace(
id=chunk_id,
choices=[choice],
created=int(time.time()),
model="mock-model",
object="chat.completion.chunk",
service_tier=None,
system_fingerprint=None,
usage=usage,
)
return chunk
def _make_tool_call_delta(index=0, tc_id=None, name=None, arguments=None):
"""Build a tool call delta object."""
from types import SimpleNamespace
func = None
if name is not None or arguments is not None:
func = SimpleNamespace(
name=name,
arguments=arguments,
)
return SimpleNamespace(
index=index,
id=tc_id,
function=func,
type="function" if tc_id else None,
)
class MockAsyncStream:
"""Async iterator that yields ChatCompletionChunk objects."""
def __init__(self, chunks):
self._chunks = chunks
self._index = 0
def __aiter__(self):
return self
async def __anext__(self):
if self._index >= len(self._chunks):
raise StopAsyncIteration
chunk = self._chunks[self._index]
self._index += 1
return chunk
async def __aenter__(self):
return self
async def __aexit__(self, *args):
pass
async def close(self):
pass
class _MockCompletions:
"""Mock chat.completions with a create() method."""
async def create(self, **kwargs):
messages = kwargs.get("messages", [])
is_stream = kwargs.get("stream", False)
if _has_tool_output(messages):
# Call 2: summarize — return text response
return await self._stream_text_response()
else:
# Call 1: decide to call tool — return tool call
return await self._stream_tool_call()
async def _stream_tool_call(self):
"""Simulate LLM deciding to call get_current_time()."""
chunk_count, chunk_delay_s = _get_config()
chunk_id = f"chatcmpl-{uuid.uuid4().hex[:24]}"
call_id = f"call_{uuid.uuid4().hex[:24]}"
chunks = []
# First chunk: role + tool call name
chunks.append(_make_chunk(
chunk_id,
tool_calls=[_make_tool_call_delta(
index=0,
tc_id=call_id,
name="get_current_time",
arguments="",
)],
))
# Stream the arguments "{}" across a few chunks to simulate real behavior
for arg_part in ["{", "}"]:
chunks.append(_make_chunk(
chunk_id,
tool_calls=[_make_tool_call_delta(
index=0,
arguments=arg_part,
)],
))
# Final chunk: finish_reason = tool_calls
chunks.append(_make_chunk(chunk_id, finish_reason="tool_calls"))
# Tool call chunks stream instantly (no delay)
return MockAsyncStream(chunks)
async def _stream_text_response(self):
"""Simulate LLM summarizing the tool output."""
chunk_count, chunk_delay_s = _get_config()
chunk_id = f"chatcmpl-{uuid.uuid4().hex[:24]}"
# Split summary text into chunks
words = SUMMARY_TEXT.split()
text_chunks = []
base_size = max(1, len(words) // chunk_count)
remainder = len(words) % chunk_count
idx = 0
for i in range(chunk_count):
size = base_size + (1 if i < remainder else 0)
if idx < len(words):
text_chunks.append(" ".join(words[idx:idx + size]) + " ")
idx += size
chunks = []
# First chunk: role
chunks.append(_make_chunk(chunk_id, content=""))
# Text delta chunks with delays
for text in text_chunks:
chunks.append(_make_chunk(chunk_id, content=text))
# Final chunk: finish_reason = stop
chunks.append(_make_chunk(chunk_id, finish_reason="stop"))
return _MockDelayedStream(chunks, chunk_delay_s)
class _MockDelayedStream:
"""Async iterator that adds delays between chunks to simulate LLM token generation."""
def __init__(self, chunks, delay_s):
self._chunks = chunks
self._delay_s = delay_s
self._index = 0
def __aiter__(self):
return self
async def __anext__(self):
if self._index >= len(self._chunks):
raise StopAsyncIteration
chunk = self._chunks[self._index]
self._index += 1
# Add delay between content chunks (skip first role chunk and last finish chunk)
if self._delay_s > 0 and 1 < self._index < len(self._chunks):
await asyncio.sleep(self._delay_s)
return chunk
async def __aenter__(self):
return self
async def __aexit__(self, *args):
pass
async def close(self):
pass
class _MockChat:
"""Mock client.chat namespace."""
def __init__(self):
self.completions = _MockCompletions()
class MockAsyncOpenAI:
"""
Drop-in replacement for AsyncDatabricksOpenAI that returns mock streaming responses.
Simulates:
- Call 1: LLM decides to invoke get_current_time (tool call response)
- Call 2: LLM summarizes tool output (text streaming response)
Timing controlled by MOCK_CHUNK_DELAY_MS and MOCK_CHUNK_COUNT env vars.
"""
def __init__(self, **kwargs):
self.chat = _MockChat()
# Satisfy any attribute checks the SDK might do
self.api_key = "mock-key"
self.base_url = "http://mock"
Related skills
FAQ
What does load-testing do?
load-testing skill documents Load test a Databricks App to find its maximum QPS.
When should I use load-testing?
User asks about load-testing, load test a databricks app to find its maximum qps. use when: (1) user says 'load test', '.
Is this skill safe to install?
Review the Security Audits panel on this page before installing in production.