Skip to content

Getting started ​

This page builds a working metric from nothing. It takes about five minutes.

Install ​

bash
npm install metrichouse
bash
pnpm add metrichouse
bash
yarn add metrichouse

You need Node 20 or newer. Nothing else is required to start.

Step 1: declare a metric ​

A metric is a plain value you export from a file. Declaring one does nothing on its own. It opens no connections, starts no timers and touches no storage.

ts
// metrics/schema.ts
import { counter, str } from 'metrichouse/core'

export const pageViews = counter('page_views', {
  // The labels you want to break the number down by.
  dims: { path: str() },

  // Keep one row per path per minute.
  resolution: '1m',

  // Never ship more often than once every five minutes.
  flush: '5m',

  // The one function you write. MetricHouse calls it with finished rows.
  write: async (rows) => {
    console.log(rows)
  },
})
A counter declaration with each field labelled: the name, the dimensions, the resolution, the flush cadence and the write function.
Every setting on a metric, and what each one decides.

Four things are worth noticing:

  • resolution is how much detail you keep. One row per minute here. Set it to '1s' and you get one row per second.
  • flush is how often rows leave. These are separate settings. See Buckets and time.
  • write is required. A metric that measures something and ships it nowhere is almost always a mistake, so the type system will not let you declare one.
  • dims is optional. Leave it out for a metric that is a single number. dims covers what makes a good label and what one costs.

Step 2: create a house ​

A house connects your metrics to somewhere that can hold running totals. That somewhere is called a driver. The memory() driver keeps everything in plain JavaScript maps and is the right choice for a single server.

ts
// metrics/house.ts
import { createHouse } from 'metrichouse/core'
import { memory } from 'metrichouse/memory'
import * as schema from './schema.js'

export const house = createHouse({
  driver: memory(),
  schema,
})

Passing the whole imported module is deliberate. The house picks out the metrics and ignores every other export, so you never maintain a list by hand.

Until a house registers a metric, writing to it throws. That is on purpose: a metric that silently drops data because you forgot to register it is far worse than one that fails loudly at startup.

Step 3: write to it ​

ts
import { pageViews } from './metrics/schema.js'

pageViews.add({ path: '/pricing' })
pageViews.add({ path: '/pricing' })
pageViews.add({ path: '/docs' })

add() returns immediately. It does not wait for storage, and it never throws because storage was slow.

Step 4: read it back ​

You do not need to wait for a flush to see the numbers.

ts
await pageViews.current({ path: '/pricing' })   // 2
await pageViews.current()                       // 3, every path added together

This reads the window that is still filling, so it is live.

Step 5: get the rows out ​

ts
// Run this once the minute the writes landed in has ended, and its grace with it.
const report = await pageViews.flush()
// { buckets: 1, rows: 2, skipped: false }

Called right after add(), the same line returns { buckets: 0, rows: 0 }, because the minute is still open. The tip below shows how to get past that in a test. Once the minute has closed, your write function runs with an array that looks like this:

ts
[
  {
    id: '4f2c18a6e1d9b0c73a5e8f21b6d40c99',
    bucket_ts: 2026-09-17T14:03:00.000Z,
    path: '/pricing',
    value: 2,
  },
  {
    id: 'a91e5b7c2d380f4419ce6a05d7b2f318',
    bucket_ts: 2026-09-17T14:03:00.000Z,
    path: '/docs',
    value: 1,
  },
]

One row per path, per minute. That is the whole point.

Nothing ships until its window has closed

A flush only takes windows that have finished. If you call flush() one second after add(), the minute is still open and you will get { buckets: 0, rows: 0 }. flush({ force: true }) returns the same, since force waives the cadence and leaves the open window alone. In a test, give the house a clock you control and move it past the end of the window and its two second grace:

ts
let clock = Date.parse('2026-09-17T14:03:00Z')
const house = createHouse({ driver: memory(), schema, now: () => clock })

pageViews.add({ path: '/pricing' })
clock += 62_000               // the minute has ended, and so has its grace
await pageViews.flush()       // { buckets: 1, rows: 1, skipped: false }

Otherwise, move on to the next step and let the scheduler handle it.

Step 6: make it ship on its own ​

On a server that stays running, house.start() gives every metric its own timer at its own cadence. Nothing else has to call anything.

ts
house.start()

process.on('SIGTERM', async () => {
  await house.stop()   // stop the timers, finish pending writes, ship what has closed
  process.exit(0)
})

On memory() that last flush ships every window that has ended. With several servers sharing one Redis it waits for the metric's shared turn instead, and what it leaves ships with the next server to take it. See Stopping cleanly.

On serverless or edge platforms the process is frozen between requests, so a timer never fires. There you call house.flush() from a cron job or a request handler instead. See Deployment targets.

The finished file ​

ts
import { counter, createHouse, str } from 'metrichouse/core'
import { memory } from 'metrichouse/memory'

const pageViews = counter('page_views', {
  dims: { path: str() },
  resolution: '1m',
  flush: '5m',
  write: async (rows) => {
    await db.insertInto('page_views').values(rows).execute()
  },
})

const house = createHouse({ driver: memory(), schema: { pageViews } })
house.start()

pageViews.add({ path: '/pricing' })

A table to put the rows in ​

MetricHouse never creates tables, so here is one that fits the rows above.

sql
CREATE TABLE page_views (
  id         String,
  bucket_ts  DateTime64(3),
  path       String,
  value      Int64
)
ENGINE = ReplacingMergeTree
ORDER BY (bucket_ts, path);
sql
CREATE TABLE page_views (
  id         TEXT PRIMARY KEY,
  bucket_ts  TIMESTAMPTZ NOT NULL,
  path       TEXT NOT NULL,
  value      BIGINT NOT NULL
);

The id column matters. If the same rows are sent twice after a failed write, the ids are identical, so a table that treats id as unique collapses them automatically. See Reliability.

Next steps ​

  • How it works explains what happens between add() and your write function.
  • Metric types covers gauges, events, logs and timers.
  • Examples has complete small setups you can copy.

Released under the MIT License.