A controlled, application-layer HTTP load / DDoS-simulation testing platform for infrastructure that you own or have explicit written authorization to test. It is built for capacity planning, resilience testing, and authorized red-team performance work — not for attacking third parties.
⚠️ Authorized use only. Every test requires an authenticated user, a server-side authorization check, and a target that an administrator has explicitly moved toAPPROVEDwith a recorded authorization reference. There is no anonymous mode and no "test any host" mode. All activity is audited with the acting user's identity and the server-observed client IP.
Copyright © 2026 Ashish Naulach. All rights reserved.
This repository contains proprietary software. The original source code and original project materials are owned by Ashish Naulach and are not released under an open-source license — no permission is granted to copy, modify, distribute, sublicense, sell, or create derivative works from the original software except with explicit written permission. See LICENSE.
Third-party dependencies and components (see package.json) remain the property
of their respective authors and are governed by their own licenses; nothing here
claims ownership of them.
- RBAC with
ADMIN/OPERATOR/VIEWERroles. - Authorized target allowlist — tests only run against
APPROVEDtargets, each with its own rate / concurrency / duration ceilings. - Server-side safety policy — a single
runPreflight()gate that every test-start path funnels through. Client-supplied rate, concurrency, duration, payload size, headers, and target authorization are all re-validated and clamped server-side. Hard global ceilings come from environment variables and cannot be exceeded from the frontend. - Isolated load-generation worker — a separate process that only ever acts
on already-authorized
Testrows. It never accepts inbound HTTP. - Controlled HTTP load engine — fixed request rate, hard concurrency cap,
fixed duration, per-request timeout, graceful drain,
AbortControllercancellation. Nothing below the HTTP layer; no spoofing, reflection, amplification, or protection-bypass techniques. - Real-time telemetry — SSE stream of RPS, latency percentiles (p50/p95/p99), status-code distribution, error and timeout counts.
- Emergency stop — per-test stop and a global kill switch that aborts every running test and blocks new ones until an admin clears it.
- Immutable-style audit log — append-only, admin-only, with configurable retention.
- Fail-closed — if the database / telemetry / safety layer is unavailable, no new test starts. An unknown authorization state is never treated as authorized.
Browser ──► Next.js web app (auth, RBAC, safety preflight, audit, dashboard)
│ writes AUTHORIZED Test rows + audit
▼
PostgreSQL ◄──────────────┐
▲ │ claims AUTHORIZED tests,
SSE telemetry │ metrics + status │ streams metrics, honors stop
│ │
Load-Test Worker (isolated process) ──► HTTP requests ──► APPROVED target
See ARCHITECTURE.md for component detail and SECURITY.md for the threat model and control list.
Next.js 14 (App Router) · React 18 · TypeScript · Tailwind CSS · Node.js worker · PostgreSQL · Prisma · Vitest · Playwright · Docker Compose.
cp .env.example .env
# edit .env — at minimum set a strong SESSION_SECRET:
# node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))"
docker compose up --buildThis starts Postgres, runs prisma migrate deploy, seeds dev accounts and one
local APPROVED echo target, then starts web (http://localhost:3000) and
worker.
The seed (npm run db:seed) creates three local demo accounts
(admin@example.com, operator@example.com, viewer@example.com) and prints
each generated password to the console once. It is for local development only
and refuses to run when NODE_ENV=production. Do not use seeded accounts in a
real deployment.
The first account ever registered on a fresh database automatically becomes
ADMIN; everyone else starts as VIEWER and must be promoted. For any real
deployment, skip the seed and register your own first account.
npm install
cp .env.example .env # set SESSION_SECRET and a reachable DATABASE_URL
npm run prisma:migrate # create the schema
npm run db:seed # optional dev data
npm run dev # web on :3000
npm run worker # worker (separate terminal)Three independent paths, most convenient first:
-
Admin resets it — Administration → the user's row → Reset password. A new password is generated and shown once; the user's sessions are revoked. No email needed. (An admin resets their own via
/settings.) -
Self-service email —
/forgot-password→ reset link (valid 30 min, single-use). NeedsRESEND_API_KEY; with Resend's test sender it only delivers to the Resend account owner's address until you verify a domain at resend.com/domains and pointEMAIL_FROMat it. Without a key the link is written to the server log instead. -
Total admin lockout (offline, guaranteed):
# needs DATABASE_URL (+ DIRECT_URL) in .env, same as prisma migrate npm run reset-password -- admin@example.com # prints a new password npm run reset-password -- admin@example.com 'MyNewPass1234' # or set your own
Clears any lockout, reactivates the account, revokes its sessions.
npm run prisma:migrate # dev: create + apply a new migration
npm run prisma:deploy # prod/CI: apply committed migrations
npm run prisma:studio # inspect datanpm test # unit + engine tests (no database needed)
# Integration tests need a migrated test database:
createdb loadtester_test
DATABASE_URL=postgresql://loadtester:loadtester@localhost:5432/loadtester_test npm run prisma:deploy
RUN_DB_TESTS=1 DATABASE_URL=postgresql://loadtester:loadtester@localhost:5432/loadtester_test npm test
# End-to-end (needs a running, seeded app):
npm run build && npm run db:seed
npx playwright install --with-deps chromium
npm run test:e2enpm run build # prisma generate + next build
npm run start # serve the production buildLive deployment: https://ddos-loadtester.vercel.app Project
ashish-49ce/ddos· DB: Supabaseddos(ap-northeast-2) · functions pinned toicn1(co-located with the DB) · daily cron backstop at 03:00 UTC · auto-deploys on push tomain.Worker reliability on Hobby: a test is picked up by a
waitUntilkick the moment it's authorized; the dashboard and the test page additionally callPOST /api/worker/nudge(authed, rate-limited) while any test is non-terminal, which recovers a killed tick and drains the queue. Change your password at/settings.
Vercel has no persistent processes, so the long-lived worker/index.ts is
replaced by a serverless tick:
GET/POST /api/worker/tick— claims and runs one authorized test within a bounded wall-clock budget (WORKER_TICK_BUDGET_SECONDS, kept under the functionmaxDuration). Authorized byCRON_SECRET(Vercel Cron'sAuthorization: Bearerheader) orWORKER_TICK_SECRET(x-worker-secretheader for manual/external triggers).- Push-based scheduling — when a test reaches
AUTHORIZED, the API schedules akickWorker()viawaitUntil()that fires one tick. The tick re-fires itself while it keeps finding queued work, so the queue drains with no polling. vercel.jsoncron — a daily catch-up run that recovers orphaned tests and picks up anything missed. On Vercel Pro you can change the schedule to* * * * *and raisemaxDurationto 300 with matchingWORKER_TICK_BUDGET_SECONDS/MAX_TEST_DURATION.
Required Vercel env vars (Production + Preview):
| Key | Notes |
|---|---|
DATABASE_URL |
From the Neon (or other Postgres) integration. |
SESSION_SECRET |
32+ random bytes. |
CRON_SECRET |
32+ random bytes; Vercel Cron sends it automatically. |
WORKER_TICK_SECRET |
32+ random bytes; used for waitUntil self-nudge + manual triggers. |
APP_URL |
The production URL, e.g. https://ddos.vercel.app. |
MAX_TEST_DURATION |
45 on Hobby (function cap is 60s). |
WORKER_TICK_BUDGET_SECONDS |
50 on Hobby. |
MAX_GLOBAL_RPS, MAX_GLOBAL_CONCURRENCY, MAX_CONCURRENT_TESTS |
Tune to taste. |
Migrations are not run by the Vercel build. Run them once against the provisioned database:
vercel env pull .env.local --environment=production
DATABASE_URL="$(grep '^DATABASE_URL=' .env.local | cut -d= -f2- | tr -d '\"')" npm run prisma:deploy
# optional first admin + demo target:
DATABASE_URL="…" npm run db:seedManual worker trigger (e.g. from an external 1-minute pinger on Hobby):
curl -X POST -H "x-worker-secret: $WORKER_TICK_SECRET" https://<your-app>/api/worker/tickAll configuration is via environment variables — see .env.example. The safety-relevant ones:
| Variable | Meaning |
|---|---|
MAX_GLOBAL_RPS |
Absolute ceiling on requests/sec for any test. |
MAX_GLOBAL_CONCURRENCY |
Absolute ceiling on concurrency for any test. |
MAX_TEST_DURATION |
Absolute ceiling on test duration (seconds). |
MAX_PAYLOAD_BYTES |
Hard cap on request body size. |
MAX_CONCURRENT_TESTS |
Max tests running platform-wide at once. |
TRUSTED_PROXY_MODE |
none / single / list — how the client IP is derived. |
AUDIT_RETENTION_DAYS |
Audit log retention target. |
Per-target limits are always clamped down to these global values.
No IP spoofing, UDP reflection, DNS amplification, SYN floods, botnet
coordination, credential attacks, exploit delivery, CDN/WAF/rate-limit bypass,
stealth/evasion, identity hiding, persistence, or arbitrary third-party
targeting. Requests are ordinary fetch() calls that carry a static,
identifying User-Agent.