Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,44 @@ jobs:

- run: bash conformance/vm/e2e.sh "$USER@localhost" --no-firewall --no-ssh-hardening --no-swap --no-auto-updates

# nextship-loadtest for real: k6 has to accept the script the package writes and
# return a summary its report can read, which no unit test can show. Both ways of
# finding k6 are run, Docker first, while no k6 is installed to be preferred to it.
loadtest:
name: Load test command
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: pnpm

- run: pnpm install --frozen-lockfile
- run: pnpm build

- name: Run it with k6 in Docker
working-directory: packages/loadtest
run: node scripts/e2e.mjs docker

# The release K6_IMAGE in packages/loadtest/src/engine.ts names, checked against
# the checksums published with it.
- name: Install k6
run: |
curl -fsSLO https://github.com/grafana/k6/releases/download/v2.3.0/k6-v2.3.0-linux-amd64.tar.gz
curl -fsSLO https://github.com/grafana/k6/releases/download/v2.3.0/k6-v2.3.0-checksums.txt
grep 'k6-v2.3.0-linux-amd64.tar.gz$' k6-v2.3.0-checksums.txt | sha256sum -c -
tar -xzf k6-v2.3.0-linux-amd64.tar.gz
echo "$PWD/k6-v2.3.0-linux-amd64" >> "$GITHUB_PATH"

- name: Run it with k6 on PATH
working-directory: packages/loadtest
run: node scripts/e2e.mjs local

site:
name: Site
runs-on: ubuntu-latest
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ All notable changes to this repository. Attribution rules: `AGENTS.md` 搂1.1.

## [Unreleased]

### Added

- **`nextship-loadtest`, a separate command that reports how many users at once a site
holds.** Give it an address and it runs k6 for you, the one on PATH or the official
Docker image, in up to five rising steps, then says which step the site stopped coping
at: a step is held when at most 1% of its requests fail and 95% finish within a second.
It works on any site, needs no nextship project, prints its plan and sends nothing
until `--yes`, and reports a target that never answers as an error rather than a result.
Asked for by a reader who wanted to know what an app can take before deploying it. In
`packages/loadtest`, with 39 unit tests and an end-to-end script that CI runs with Docker
and with k6 on PATH, and that passed on Windows with k6 on PATH and with Docker Desktop.
Not published to npm yet, and not run with Docker Desktop on macOS; both are in
`docs/roadmap.md`. (Gowtham)

### Docs

- **The evidence page compares nextship with Coolify, measured.** The same app deployed
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,17 @@ egress is $0.02 per GiB against Vercel's $0.15 and up, and App Platform needs no
balancer, which was the line item that made the AWS path cost more than Vercel at
small scale.

## Load testing

`packages/loadtest` is a separate command, `nextship-loadtest`, that reports how many users
at once a site holds: give it an address and it runs [k6](https://k6.io) for you, in rising
steps, and says which step the site stopped coping at. It works on any site and does not
need nextship. It prints its plan and sends nothing until `--yes`, because a load test is
real traffic.

It is in this repository and **not published to npm yet**. How to run it, what the numbers
mean and what has been run are in [`packages/loadtest/README.md`](./packages/loadtest/README.md).

## Known limitations

The full table with consequences and status is
Expand Down Expand Up @@ -1084,6 +1095,8 @@ packages/
adapter/ Next.js Adapter API implementation, injected via NEXT_ADAPTER_PATH
cli/ every command; runtime/ holds the files copied into a build,
and runtime/vm/ the setup and watchdog scripts sent to a server
loadtest/ nextship-loadtest, a separate command that load tests any site
with k6; not published yet
site/ the marketing site and documentation, exported as static files
```

Expand Down
52 changes: 52 additions & 0 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -1180,3 +1180,55 @@ here rather than lost:
That claim depends entirely on the v2 correctness work (shared cache, distributed tag
coordination, atomic HTML and RSC invalidation), because that is the one part of this
problem no existing tool solves. Convenience is not a wedge. Correctness is.

## 15. Load testing (Implemented, not published)

`packages/loadtest` is a second command, `nextship-loadtest`, in its own package with no
dependency on the CLI in either direction. It answers one question, how many users at once
a site holds, for any site. It was asked for by a reader who wanted to know what an app can
take before deploying it.

**k6 does the work.** Load generation and timing are k6's, the tool people already trust
for them; the package writes the script, runs k6 and reads the result. k6 is found on PATH,
and failing that run from the official image, pinned as `grafana/k6:2.3.0`, the release the
script and the report were written against. k6 is AGPL-3.0 and is only ever run as a
separate process, never bundled.

**A staircase, not a flat load.** The users asked for are reached in up to five equal
steps, because the question is where a site stops coping and only a rising load shows the
step where it did. Each step is its own k6 scenario with thresholds that cannot fail: k6
reports figures per scenario only for a scenario a threshold names.

**The rule is stated with the result.** A step is held when at most 1% of its requests
fail and 95% finish within 1000 ms. The headline is the last step held before the first
that was not. Requests per second are counted from the length of each step, since k6's own
per-scenario rate divides by the whole run.

**Plan first.** Like every nextship command it prints the plan and sends nothing until
`--yes`. Users are capped at 1000 and the run at 600 s. An address carrying credentials is
refused, because the plan would print them. The address reaches k6 through its
environment and the numbers come from a validated plan, so nothing typed is interpolated
into the script.

**A target on the same machine.** A container has its own loopback address. On Linux the
container is given the host's network; Docker Desktop has none to give, so there the
address is requested as `host.docker.internal`, and the plan says so.

**A target that never answers is an error.** Every request failing from the first step
says the address is wrong or the site is down, not how much load it takes, so no result is
printed.

Verified by `packages/loadtest/scripts/e2e.mjs`, which runs the built command against a
server that answers one request at a time, and fails unless the run holds an early step,
fails a later one on response time and refuses a closed port. On Windows it passes with
k6 2.3.0 on PATH, and with Docker Desktop (Docker 29.7.2), where the server on the same
machine was reached as `host.docker.internal`. CI runs it on Linux with Docker and with k6
on PATH.

Known limitations:

- **Not run with Docker Desktop on macOS.** It takes the path that passed on Windows, and
nobody has run it there.
- **One address, GET only.** No logins or journeys; those need a hand-written k6 script.
- **The test runs from the user's machine**, so the times include their network.
- **Not published.** The npm name and the release path are in `docs/roadmap.md`.
14 changes: 14 additions & 0 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,20 @@ Known from the Droplet runs, to fix or document here: `conformance/vm/e2e.sh` ne
with no other app on it, and after adding swap `server add` prints `done swap already on`,
the post-apply check's wording rather than what it did.

## Load testing (Built, not published)

`nextship-loadtest` is in `packages/loadtest` and described in `design.md` 搂15. What is
left before anyone can install it:

| Item | State |
|---|---|
| Run it with Docker Desktop on macOS, against a target on the same machine | Not run. Windows passed on 2026-10-09; macOS needs a Mac with Docker Desktop running |
| Choose the npm name and publish it | `nextship-loadtest` is free on npm and is the working name. Publishing needs its own trusted publisher on npm and a job in `release.yml`, which today publishes `packages/cli` only |
| A page for it on the site | After it is published, so the site never documents a command nobody can install |

Not planned until someone asks: a step in `nextship deploy` that load tests the new
deployment before it takes traffic.

## AWS: on demand, after the Lightsail streaming experiment

Previously v1.1. Moved behind the VM target: an EC2 instance is a Linux server over SSH, which
Expand Down
109 changes: 109 additions & 0 deletions packages/loadtest/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# nextship-loadtest

Find how many users at once a site holds, with one command. You give it an address; it
runs [k6](https://k6.io) for you and reports the result in one line. It works on any site,
whatever it is built with and wherever it is hosted. You do not need nextship to use it.

**Not published to npm yet.** Until it is, run it from this repository (see
[Running it from the repository](#running-it-from-the-repository)).

```
nextship-loadtest https://example.com/ # shows the plan, sends nothing
nextship-loadtest https://example.com/ --yes # runs it
```

Only test a site you own or have permission to test. A load test is real traffic, and
against someone else's site it is an attack. That is why the command prints its plan and
sends nothing until you add `--yes`.

## Requirements

| Requirement | Why |
|---|---|
| Node.js 22 or newer | Runs the command |
| k6 on your PATH, or Docker running | k6 sends the requests and measures them. With no k6 installed, the official `grafana/k6:2.3.0` image is used |

## What it does

The test climbs in steps. With the defaults it holds 10 users at once, then 20, 30, 40 and
50, for 12 seconds each. Every user requests the address again as soon as it is answered,
so 50 users means 50 requests in flight at all times, which is far more than 50 people
browsing.

```
> Plan
target http://127.0.0.1:4599/
load 5 step(s) of 4 s: 12, 24, 36, 48, 60 users at once
each user requests the address again as soon as it is answered
engine k6 2.3.0, installed on this machine
traffic real requests to the target for 20 s
> Running, for 20 s
> Result
users requests/s median 95% within failed
12 30.5 390 ms 418 ms 0.0%
24 28.0 775 ms 965 ms 0.0%
36 25.0 1155 ms 1742 ms 0.0%
48 22.3 1533 ms 2542 ms 0.0%
60 19.0 2284 ms 3356 ms 0.0%
v Holds 24 users at once
With 36 users at once, 95% of requests took up to 1742 ms.
A step is held when at most 1.0% of its requests fail and 95% finish within 1000 ms.
```

That run is real: k6 2.3.0 against a test server built to answer one request at a time.

A step is **held** when at most 1% of its requests fail and 95% of them finish within one
second. The headline is the last step held before the first one that was not. If every
step is held, it says so, and that the limit was not found: raise `--users`.

If no request succeeds at all, you get an error instead of a result, because that says
the address is wrong or the site is down, not how much load it takes.

## Options

| Option | Meaning |
|---|---|
| `--users <n>` | The most users at once, reached in up to five steps. Default 50, at most 1000 |
| `--duration <s>` | The length of the whole test in seconds. Default 60, from 10 to 600 |
| `--yes` | Send the requests. Without it, only the plan is printed |
| `-h`, `--help` | Show the usage |
| `-v`, `--version` | Show the version |

## Testing a site on your own machine

`http://localhost:3000/` works. With k6 installed, nothing special happens. With Docker,
a container cannot reach your machine's `localhost` by that name, so on Linux the
container shares the host's network, and on macOS and Windows the address is requested as
`host.docker.internal`, which the plan shows before anything runs.

## What the numbers do and do not mean

- **The test runs from your machine.** The times include your network's path to the site.
Run it from a server near the site for figures about the site alone.
- **One address, requested with GET.** No logins, forms or user journeys. For those, write
a k6 script; this command is for the first question, not every question.
- **One run is one sample.** Run it more than once before you rely on a number.
- **It is not a stress tool.** 1000 users at once is the ceiling, since one machine running
k6 beside everything else cannot reliably generate more.

## What has been run

- With k6 2.3.0 on PATH, on Windows: `scripts/e2e.mjs local` passes.
- With Docker on Linux: `scripts/e2e.mjs docker` runs in this repository's CI.
- With Docker Desktop on Windows (Docker 29.7.2): `scripts/e2e.mjs docker` passes, with
the test server on the same machine requested as `host.docker.internal`.
- With Docker Desktop on macOS: **not run yet.** It takes the same path as Windows, which
unit tests cover, but nobody has run it there.

## Running it from the repository

```
pnpm install
pnpm build
node packages/loadtest/dist/index.js https://example.com/
```

## License

Apache-2.0. k6 is a separate program under its own license (AGPL-3.0), run as a separate
process and never bundled or redistributed by this package.
41 changes: 41 additions & 0 deletions packages/loadtest/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "nextship-loadtest",
"private": true,
"version": "0.1.0",
"license": "Apache-2.0",
"author": "Gowtham <gowtham9112006@gmail.com>",
"contributors": [
"Ragul D <raguld2105@gmail.com>"
],
"description": "Find how many users at once a site holds, with one command. Runs k6 for you. Not published yet.",
"keywords": [
"load-testing",
"k6",
"performance",
"cli"
],
"engines": {
"node": ">=22"
},
"type": "module",
"bin": {
"nextship-loadtest": "dist/index.js"
},
"files": [
"dist",
"!dist/**/*.test.*"
],
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "node --test \"dist/**/*.test.js\""
},
"repository": {
"type": "git",
"url": "git+https://github.com/gowtham472/nextship.git"
},
"homepage": "https://nextship.doodlebytestudio.in",
"bugs": {
"url": "https://github.com/gowtham472/nextship/issues"
}
}
Loading
Loading