See it run

Real output from populace demo, which runs everything against a built-in app with a bug planted in it.

populace demo
  Bringing 6 people to life across manila, mumbai…

   ✓ Jhun Ramirez (Manila, grab)
   ✓ Ramesh Kadam (Mumbai, uber)
   ✓ Maricel Santos (Manila, angkas)
   ✓ Priya Sharma (Mumbai, ola)
   ✓ Dante Cruz (Manila, joyride)
   ✓ Imran Shaikh (Mumbai, swiggy)

  Running 6 people for 1 min…

  tick 12/30 · 6 people · 1.0km · 6p 17l 6c 5m · 5 errors
  tick 30/30 · 6 people · 2.4km · 18p 41l 17c 15m · 13 errors

  POPULACE REPORT — Demo App
  ✖ Problems found:
      · 13 of 335 API calls failed (3.9%).
  ✖ like   53   13   42ms
      ↳ 13× new row violates row-level security policy "post_likes_insert"
  [YOUR APP]  The database refused the write on permissions, not on data.
  ✔ Cleanup complete — 6 accounts removed.

What it found

Its first run broke an app that had passed every manual test. Six simulated drivers used Waggle, our app for delivery riders, for three and a half minutes. Populace found three defects in it, and one meant no new account could work. It found two in itself as well. A fourth app defect turned up later, on the run meant to sign the work off. Every run is in the test report.

3.5 minto find the first five
6simulated drivers
4app defects, one root cause
2defects in Populace itself

The defects

Four of them were the same mistake in different places, invisible to one person testing and obvious within seconds to six.

“You cannot upsert a column you cannot select.”

  1. 01
    In the app

    Signup created no profile

    Every new account came out broken. Signup wrote to a column the app is deliberately not allowed to read, and an upsert has to read every column it touches.

  2. 02
    In the app

    Likes failed at random

    About one tap in four was rejected. Liking was an upsert on a table with no rule allowing updates.

  3. 03
    In the app

    Profile edits silently vanished

    The same upsert on the same unreadable column. No error on screen, and nothing saved.

  4. 04
    In the app

    Rejoining a group failed silently

    Found later, on the sign-off run. The same mistake a third time, on a different table.

  5. 05
    In Populace

    Two defects in our own adapter

    A wrongly named argument, and an ignored error that made the report blame the wrong method. Both fixed. The report now names the real cause.

Clean runs since

After the fixes, the same run made 400 API calls with no failures and removed every account. On a local backend, up to 300 simulated drivers at once made 3.17 million calls with zero API failures.

Local runs prove correctness under concurrency, not speed, so their latency isn't quoted here.

Some bugs need two people

Populace is for software where the second user is the problem. If nothing below describes your product, you don't need it.

You need it if your app has

  • A feed, a chat, or anything one person posts and another reads
  • Rules about who can see what
  • Presence, read receipts or live updates
  • Counters, memberships, or anything two people can change at once
  • An account deletion path nobody has really tested

You don't need it if

  • Your product is single-player
  • You want a load test. Populace checks correctness, not capacity.
  • You want to know if people want your product. No tool can tell you that.

Why not write the scripts yourself?

You can, and for one flow you should. A script checks what you already suspect. A population does what people do, and finds what you didn't think to check. Nobody told Populace where Waggle's bugs were. It found the same upsert mistake three times, because six strangers signing up at once is something a test script never does.

  1. 01

    What it costs

    Nothing. Free and open source under AGPL-3.0. No account, no telemetry, no paid tier.

    The AGPL only matters if you offer Populace itself as a hosted service. Testing your own product, commercial or not, carries no obligation.

  2. 02

    What it costs in time

    One adapter file. Thirteen small methods, two of them required.

    populace init writes a working REST adapter with every line to change marked EDIT. For an HTTP and JSON API, that's an afternoon.

The engine never learns your app

Populace knows how to be a person: where they go, when they get bored, who they reply to. It knows nothing about your software. Everything specific to your app lives in one small adapter you write.

01

Write an adapter

One file that turns “a person did something” into your API calls. Two methods are required, createUser and deleteUser. Add the rest as they apply.

02

Run a population

Simulated people across real cities, each with their own habits and patience. Person n always gets the same phone number, so reruns reuse accounts instead of piling up new ones.

03

Read the report

What broke, how often, how slow it got with everyone on at once, and what the run never reached.

What to connect it to

Point it here

Your API

The same endpoints, database and sign-in your app uses, in a test environment. Each simulated person signs in as a different user, so your server has to keep them apart.

Why: every bug Populace has found so far lived on the server, where no screen shows it.

Not this

Your .apk or .aab

A compiled app is a client. Driving it means automating taps, which tests the screens, not what happens when many people use them at once.

Populace works one layer below, so one adapter covers your Android, iOS and web apps together.

The whole integration is one file

A trimmed version of a working adapter. If your backend speaks HTTP, yours looks like this.

Implement the two required methods and you can run. Every method you add gets exercised. Any you leave out are listed under Not tested, and any the run never reached are listed under Not exercised.

// adapters/my-app.mjs — everything Populace needs to know about your app
export function createAdapter(target) {
  const api = target.url, key = target.key;

  return {
    name: "My App",

    // Make a real account.
    async createUser(person) {
      const r = await fetch(`${api}/auth/v1/signup`, {
        method: "POST",
        headers: { apikey: key, "content-type": "application/json" },
        body: JSON.stringify({ phone: person.phone, password: person.password }),
      });
      if (!r.ok) throw new Error(await r.text());
      return { id: (await r.json()).user.id, token: "…" };
    },

    // Populace decides WHEN; you decide HOW.
    async post(user, text) { /* one fetch */ },

    // Read other people's posts — the path one tester never exercises.
    async recentPostsByOthers(user) { /* one fetch */ },

    // Delete the account and its data. Runs after every run.
    async deleteUser(user) { /* one fetch */ },
  };
}

The 13-method contract

Implement the ones that apply. Coverage counts the methods a run actually called, and every gap is named.

MethodWhat a person is doingRequired
createUserSigning upRequired
setProfileFilling in who they areOptional
refreshSessionStaying signed in past token expiryOptional
reportLocationMoving through the cityOptional
postPosting publiclyOptional
recentPostsByOthersReading other people's postsOptional
likeLiking someone else's postOptional
commentReplying to someone else's postOptional
openConversationStarting a private chatOptional
sendMessageSending a private messageOptional
listGroupsBrowsing groupsOptional
joinGroupJoining a groupOptional
deleteUserDeleting their account and dataRequired

Also optional, outside the contract: signIn, which lets cleanup look for leftover accounts without creating any.

A report you can hand to someone else

The live run is the show. The report is the product. Anything unproven is called unproven, and there's no tolerance threshold for a real bug to hide under.

  1. 01

    One bug, one line

    Failures are grouped by cause, not by exact error text.

  2. 02

    Latency for every method

    Median and 95th percentile, with everyone on at once.

  3. 03

    What to do next

    Each failure says whether it's your app, the platform or the network, and how to fix it.

  4. 04

    Honest coverage

    Methods you didn't write, and methods the run never reached, are both named.

  5. 05

    Cleanup counted

    If an account couldn't be removed, the report says so.

  6. 06

    JSON for CI

    Plus a self-contained HTML page for people who weren't watching. It exits non-zero when anything is wrong.

populace-report
  POPULACE REPORT — Demo App
  test · 6 people · 78.6s
  ────────────────────────────────────
  ✖ Problems found:
      · 13 of 335 API calls failed (3.9%).

  YOUR API UNDER 6 CONCURRENT USERS
    method              calls fails   p50   p95
  ✖ like                   53    13  42ms  60ms
      ↳ 13× new row violates row-level
        security policy "post_likes_insert"
    reportLocation        148     0  32ms  49ms
    recentPostsByOthers    60     0 324ms 448ms

  WHAT TO DO
    [YOUR APP]  like × 13
      The database refused the write on
      permissions, not on data.

  NOT TESTED — adapter implements 12/13
    · refreshSession

  ✔ Cleanup complete — 6 accounts removed.

It tells you when the network was the problem

Test environments sit behind flaky VPNs, and CI machines drop connections. A tool that blames your API for a dropped connection gets ignored, and so do its real findings.

  1. 01

    Every call has a deadline

    A slow endpoint becomes a timeout in the report, and everyone else carries on. One stuck call can't freeze the run.

  2. 02

    Network failures retry. Your errors don't.

    A dropped connection is retried with backoff. An error your server actually returned is reported once, never retried until it happens to pass.

  3. 03

    Retries are visible

    Retry counts are shown, and latency is measured on the attempt that worked, so retries can't quietly inflate your numbers.

  4. 04

    It stops on a dead target

    If your server stops answering, Populace notices within seconds, stops, and says so. You don't wait out the whole run for nothing.

Three verdicts

VerdictMeansExit
cleanEvery call reached your API and none failed.0
problems-foundYour API returned failures. These are findings about your code.1
inconclusiveYour API never failed, but some calls never reached it, so nothing is proven either way.1

It refuses to touch production

Populace creates real accounts through your real API. Pointed at production, it would put invented people in front of real customers. So it checks three separate ways, and any one of them stops the run.

01

You declare a test environment

The config must say it's a non-production environment. Populace never assumes it.

02

You name production once

List your production hosts in neverRunAgainst. Any URL pointing at them is refused, however it's written, subdomains included.

03

It warns about the gap

An empty production list is flagged on every run, because forgetting to fill it in is the likeliest mistake.

Simulated people delete their accounts after every run, and populace clean removes anything a crashed run left behind.

Download Populace

Two ways in, one engine. The Windows app installs like any other program and needs nothing else. The command line is the same engine for people who'd rather type.

Populace Studio for Windows

Start a run, watch it live on a map, read the verdict, ask what a failure means, and check for updates. Under the hood it runs the same populace engine as the command line, so both always agree.

1.0.16current version102MB installer Windows 10/1164-bitNoNode required

Windows will warn you

Populace Studio is not code-signed yet, so Windows shows “Windows protected your PC.” Choose More info, then Run anyway. If you'd rather check the file first, the checksums are below.

# PowerShell: Get-FileHash .\PopulaceStudio-Setup-x64.exe
Setup 13ac4c30647f411451b776b3229d8b97e828702fa15367e7874229fafc877f03
Portable e01be494efa786cf1b8da247e5f01788c8dae8e4923c353b2f88d676c96b0967

Populace Studio mid-run: thirty simulated drivers pinned across twenty-eight cities on a world map from Lima to Manila, 1,625 API calls at 29 a second with no failures, and counts of the posts, likes, comments and messages they have made.
Thirty drivers across twenty-eight cities, driving Waggle through its own API.
Populace Studio showing a finished run: a clean verdict over 7,725 API calls with no failures, coverage of 12 of 13 methods with session refresh named as never reached, thirty of thirty accounts removed afterwards, and latency for every method with its median and 95th percentile.
The verdict from that run. The same engine has been driven against Gitea too, an application we didn't write.

Or use the command line

Needs Node 18 or newer, and nothing else: the engine has zero runtime dependencies. It runs on macOS and Linux too. The Windows app is Windows-only for now.

demo runs everything against a built-in app with a bug planted in it, so you see a real report before writing any adapter code.

install
# from npm
npm install -g @gigzen/populace

# see it work against a fake app, no setup
populace demo

# point it at your own app
populace init
populace doctor
populace smoke
populace run --agents 6 --minutes 5

Download a ZIP

No git needed. Get populace-main.zip, unzip it, then run node src/cli.mjs demo.

From source

git clone https://github.com/Shakhtar-Sankur/populace.git, then node src/cli.mjs demo. No install.

Get the right package

It's @gigzen/populace. A different package called populace exists and isn't this.

PowerShell

A ready-made runner ships in examples/waggle/run-test.ps1.

Commands and configuration

CommandWhat it does
populace demoRun everything against a built-in app, no setup
populace initCreate a config and a starter adapter
populace doctorCheck config, connection and coverage without running
populace smokeOne person, every method once, in seconds
populace runRun the population
populace explainExplain the failures in the last report
populace cleanDelete every account a run created
populace reportReopen the report from an earlier run
populace updateCheck npm for a newer version
populace versionVersion and environment, for bug reports

populace.config.mjs

export default {
  app: "My App",
  adapter: "./adapters/my-app.mjs",

  // Must be a non-production environment. Populace checks.
  environment: "test",

  // Your production hosts. Never run against,
  // however the URL is written.
  neverRunAgainst: ["https://api.myapp.com"],

  // Reliability settings
  timeoutMs:   20000,  // give up waiting on one call
  retries:     3,      // extra tries for calls that never landed
  giveUpAfter: 12,     // unreachable calls before stopping

  population: { agents: 8, cities: ["manila", "mumbai"], minutes: 10 },
}

Use it from CI

import { simulate } from "@gigzen/populace";

const report = await simulate({
  configPath: "./populace.config.mjs",
});

if (report.verdict.status !== "clean") {
  process.exit(1);
}

What it won't tell you

Whether people want your product. Simulated people follow patterns. They won't surprise you like real customers, and they can't tell you your onboarding is confusing or your pricing is wrong.

Use Populace to prove your app works. Use real people to decide what to build.

Populace is a Gigzen product

Full name · Gigzen Populace

Gigzen is a software company in Bhubaneswar, India. We built Populace because we needed it: our own app passed a full manual test and still couldn't create a working account.