---
title: "Record rounds"
description: "Challenge.record(metric, score) at the end of every round, why it ships before anything chargeable, exactly what it feeds and what it does not, the throttle and queue limits, and how metric names are normalised."
url: https://www.withchallenge.com/docs/record-rounds
canonical_markdown: https://www.withchallenge.com/docs/record-rounds.md
docs_index: https://www.withchallenge.com/llms.txt
api_version: v1
lastUpdated: 2026-09-22T16:56:18.000Z
---

# Record rounds

One line at the end of every round your game already has, from the day you install Challenge.js.

**Runs in the browser**

```javascript
Challenge.record('moves', 34)
```

Paid or free. Signed in or not. Practice included. There is no condition under which you should skip it, and it charges nothing, blocks nothing and renders nothing.

Ship this before you have a Challenge, before you have a server handler, before you have anything to charge for. It is the one part of the integration that gets better the earlier it lands.

## Why it goes first

A Challenge's bar is measured, never chosen. A bar you picked would set the pass rate, which would set what Challenge expects to pay, so Challenge insists on measuring it from real rounds. The question is only where those rounds come from.

Recorded rounds are the answer that costs nobody anything. Your game reports what an ordinary round looks like while nothing is at stake, and when you later create a Challenge on a metric that already has a sample behind it, the bar is computed and the Challenge is live **in the same request**. No player ever meets it sitting in calibration unable to pay anyone.

A metric qualifies when it has **5,000 recorded rounds from at least 200 distinct player refs**. Below either target nothing bad happens: the Challenge is created `calibrating` and collects its sample the slower way instead. [Calibration](https://www.withchallenge.com/docs/calibration.md) covers all three paths to a bar.

The Developer Portal reports listening progress per metric, and beside it the metric key each of your Challenges is waiting on.

## What it does not do

Worth being exact about, because the name invites the wrong assumption.

- Recorded rounds **do not advance a calibrating Challenge's own counter**. That counter is adjudicated attempts, and it is reported as `calibration.collected` on `GET /v1/challenges?status=all`.
- Recorded rounds are **never merged into a later recalibration**. They place a bar once, at creation, and only at creation. A recalibration has to be able to remove one player's contribution whole, and a recorded round is keyed on a device ref that cannot always be attributed to an account.
- Recorded rounds **do not feed the drift monitor**. Drift is judged on settled outcomes from paid attempts, which is the only population that says what a bar is actually costing.

Keep the line in forever anyway. Every new metric you add, and every Challenge you create on one, is priced from the sample this call is building.

## The contract

`record()` lives inside somebody else's game loop, which is the reason for every rule below.

**It never rejects.** A listener that throws is a listener that ends someone's round. It resolves `{ recorded: true }` or `{ recorded: false }`, and never anything else. A bad argument warns at the developer, a dead network writes one `console.debug`, and both resolve `{ recorded: false }`.

**It never floods.** At most one request per second per metric. Rapid calls are not dropped and not batched into one body: they queue in that metric's own lane and go out in order, a second apart, so a game that finishes ten rounds in a burst still reports ten. The clock starts when a request goes out, not when it comes back, so a slow response cannot make the next round wait twice.

**It never waits on the island.** Calls made before `init()` has finished its bootstrap are held and flushed as soon as there is a program to file them under, including when the bootstrap itself failed. A program id is all a recorded round needs.

## Limits, published

| Limit | Value | What happens at it |
|---|---|---|
| Throttle | 1 request per second per metric | Calls queue in order. Nothing is dropped |
| Queue depth | 50 rounds per metric | Over it, the round is dropped and a `console.warn` names the metric |
| Metric name length | 40 characters | Longer names are refused in the browser and resolve `{ recorded: false }` |
| `player_ref` length | 64 characters | Enforced by the server |
| Per device, per metric | 300 scores per rolling 24 hours | The call still answers success. The score is silently not stored |

The 50-round queue is only reachable by a game reporting far faster than it plays, which is an integration bug rather than a very quick player. That is why the overflow is dropped and said out loud instead of queued into a drain that would run for minutes.

The 300-per-day bound is a limit on junk from one device, not a security control. It answers success because a listener that can fail is a listener a partner eventually removes. At the 5,000-round target across 200 refs the average ref contributes 25 rounds, so the cap only binds on something automated.

After `Challenge.destroy()`, anything still queued resolves `{ recorded: false }` rather than hanging.

## Metric names

Pass **your game's own metric name**. Not the example's, not a generic one. It has to be the same string as the `metric_name` on the Challenge you eventually create, because that string is the key the sample is stored under.

Names are trimmed and lowercased before they are sent, so `'Moves'`, `'moves '` and `'moves'` are one metric rather than three quarter-full ones. The server additionally collapses runs of internal whitespace, so `'combo  length'` and `'combo length'` are also one.

**A mismatched name fails silently.** There is no error for recording under a metric no Challenge uses. `record()` resolves `{ recorded: true }`, the server answers `202`, and the sample accumulates under a key nothing is reading. The listening progress you are building goes to the wrong metric, and the Challenge waiting on the right one never sees a round. Nothing in the response, the console or the network tab tells you.

So verify it rather than trusting it. Fetch `GET /v1/challenges?status=all` with your server key, read the `metric_name` field off the Challenge, and compare it character for character, after trimming and lowercasing both sides, with the string your `record()` call passes. The Developer Portal shows the same key per Challenge if you would rather read it there.

The name identifies *what is measured*, not what it is measured in. Two Challenges on one program can share a unit while measuring unrelated things, so the unit is never the key.

```javascript
// Good: the game's own vocabulary, one metric per quantity.
Challenge.record('moves', movesUsed)
Challenge.record('solve time', secondsElapsed)

// Wrong: a unit is not a metric. Two unrelated quantities would pool.
Challenge.record('seconds', secondsElapsed)
```

The score must be a finite number. Anything else warns and resolves `{ recorded: false }`.

## In place

```javascript
async function playRound() {
  const result = await runTheRound()

  // Every round. Not awaited, not branched on, not conditional on a
  // Challenge existing or a player being signed in.
  Challenge.record('moves', result.moves)

  showRoundSummary(result)
}
```

If you want to confirm it is landing, `record()` resolves:

```javascript
const { recorded } = await Challenge.record('moves', 34)
console.log(recorded) // true
```

On the wire this is one `POST /api/island/record` carrying the program, the metric, the score and a per-device ref, answered `202` with `{ "recorded": true }`. No session is required. When a player happens to be signed in the round is stored as practice and carries their account; signed out it is stored anonymously. A client cannot name its own source, because a client that could would claim the stronger one.

## Next

- [Calibration](https://www.withchallenge.com/docs/calibration.md) explains what the sample is turned into, and the two other ways a Challenge can get a bar if you do not want to wait.
- [Start a paid attempt](https://www.withchallenge.com/docs/start-attempt.md) is the other browser call, and the only one that costs a player anything.
- [Challenges](https://www.withchallenge.com/docs/challenges.md) covers the status machine a new Challenge walks through.

---

Source: https://www.withchallenge.com/docs/record-rounds.md
Docs index for AI agents: https://www.withchallenge.com/llms.txt
