Skip to content
Files SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

usage

Meter operation counts and bytes up/down across a Files instance, with lazy stream-level download counting - read totals back with files.usage().

The built-in usage() plugin tallies every operation on a Files instance and surfaces the running totals as files.usage(). Each call counts as one operation, upload adds its size to bytesUp, and download / head wrap the returned body so the bytes you actually read add to bytesDown — the lazy, stream-level accounting a fire-and-forget hook can’t do.

import { createFiles } from "files-sdk";
import { r2 } from "files-sdk/r2";
import { usage } from "files-sdk/usage";

const files = createFiles({
  adapter: r2({ bucket: "uploads" }),
  plugins: [usage()],
});

await files.upload("a.txt", "hello");
await (await files.download("a.txt")).text();

files.usage();
// { operations: 2, bytesUp: 5, bytesDown: 5, operationsByKind: { upload: 1, download: 1, … } }

Because usage() adds files.usage(), construct the instance with createFiles so the method shows up on the type — the same as versioning().

How it works

  • upload adds its result’s reported size to bytesUp. Nothing is buffered — the body still streams to the adapter.
  • download / head return a body wrapped so the bytes are counted as they flow: stream() is metered chunk-by-chunk (an aborted read counts only what was consumed), and arrayBuffer() / blob() / text() count the body’s length the first time one resolves. An unread body costs nothing — and a body read twice counts once.
  • Every verb increments operations and its operationsByKind entry. A call that throws (a missing key, a validation() veto) isn’t counted, and because plugins run outside retries a call counts once no matter how many attempts it takes.

Bulk upload([...]) / download([...]) count per item.

Reading the totals

extend adds three methods:

Method Returns
usage() A UsageStats snapshot aggregated across every group.
usageByGroup() A Record<string, UsageStats> keyed by the group label.
resetUsage() Zeroes every counter, starting a fresh accounting window.

Each snapshot is a fresh copy, so mutating it never touches the running totals.

Grouping by tenant or prefix

Pass group to bucket usage by a label derived from each operation — a tenant id, a key prefix, anything. It receives the full operation, so branch on op.kind to reach op.key, op.from / op.to, etc.

const files = createFiles({
  adapter: r2({ bucket: "uploads" }),
  plugins: [
    usage({
      group: (op) =>
        "key" in op ? (op.key.split("/")[0] ?? "shared") : "shared",
    }),
  ],
});

await files.upload("acme/logo.png", bytes);
await files.upload("globex/logo.png", bytes);

files.usageByGroup(); // { acme: { bytesUp: … }, globex: { bytesUp: … } }
files.usage(); // the two buckets summed

Ordering

Put usage() first (outermost) to meter what your application sees: a later body-transforming plugin like compression() or encryption() reports the logical size up the chain, and the internal sub-operations a plugin like dedup() issues stay below it, unmetered.

plugins: [usage(), compression(), encryption(key)];

Placed last (innermost) it instead meters the bytes-on-the-wire to the provider and the provider operations those plugins expand into. Both are valid — pick the layer whose numbers you want to bill against.

Things to keep in mind

  • bytesUp comes from each upload’s reported result size. The rare adapter that doesn’t report a size contributes 0 to bytesUp for that upload.
  • bytesDown is only the body you read. head() and list() return metadata only, so they cost nothing; a download() is tallied as you read its body.
  • It counts logical operations, not provider requests. Retries happen below the plugin, so a call that retries three times still counts once.
  • No metadata, no native deps. Unlike the transforming plugins it never reads or rewrites the body, so streaming, range downloads, url(), and signedUploadUrl() all keep working on any adapter.

Last updated on

Was this page helpful?