Axiom, logs and events without sampling
Axiom is a closed-source service for collecting logs and events, with its own APL query language and MIT-licensed JavaScript clients. The current @axiomhq/js version is 2.0.0, published on 4 August 2026, and moving from version 1 changes the default shape of a query result, so this is not an upgrade you do without reading.
What Axiom does and does not do
The starting assumption is that you send logs in full, not as a sample. Instead of deciding at write time which events to keep, you push everything into a dataset, and the cost lands on storage and on the compute burned by queries. The engine is called EventDB and it is columnar and schema-less, so a new field in an event needs no prior migration.
Queries are written in APL, a Kusto-style language with operators joined by a pipe character. The documentation states one important limitation outright: APL does not query metrics. Metrics use a separate language, MPL, with its own endpoint and its own result type in the client. If you were planning on one language for everything, that split has to be accounted for from the start.
What Axiom does not do. It does not group exceptions into issues with an assigned owner, which is Sentry territory. It does not compute signup funnels or user retention, the job of PostHog. It does not break a language model call into steps with token cost, which belongs to Langfuse or Helicone. It is also not a general-purpose database you would wire into financial reporting, the way you would ClickHouse.
The license: MIT stops at the client
I checked the license from three directions and this time all three agree without reservation, at least for the libraries.
First, the repository. The LICENSE file on the main branch of axiomhq/axiom-js returns 200 and holds nineteen lines of the full MIT text, headed Copyright (c) 2021, Axiom, Inc.. The file does not open with an MIT License title line, so scanners that look for a title rather than a body may report it as unrecognised. There is no LICENSE.md in that repository; the address returns 404.
Second, the registry. The license field in the npm registry reads MIT for every checked package in the @axiomhq namespace, meaning js, logging, nextjs, react, pino, winston and tanstack-start.
Third, the contents of the published tarball. The @axiomhq/js 2.0.0 archive carries LICENSE, README.md, CHANGELOG.md, a src directory with TypeScript sources, and a dist directory of roughly 668 kilobytes with CommonJS and ESM builds plus source maps. That is real code, not a placeholder holding a name. The @axiomhq/logging 0.4.0 tarball looks the same, with a test directory on top.
And now the thing that has to be said plainly, because an MIT license on the clients can lull you. MIT covers the client libraries only. The data store and the query engine are closed, and there is no self-hosted variant at all. The plan comparison table on the pricing page lists a single deployment mode, managed, and carries no row for a self-installed option.
The consequence is heavier here than with most services. Log data is one of the worst things to migrate: the volume runs into terabytes, the format is schema-less, and the queries and dashboards written in APL port nowhere, because APL is not a standard. Leaving Axiom means rewriting every query, every alert, and exporting a history you will pay for in query compute. If you keep audit data there with a one-year retention window, vendor lock-in is a real business risk rather than a theoretical one.
Package versions and the jump to 2.0
The package family ships from one repository through tooling that synchronises release numbers in batches. As of 22 August 2026 the state is this.
| Package | Version | npm publish | Notes |
|---|---|---|---|
@axiomhq/js | 2.0.0 | 2026-08-04 | one dependency, fetch-retry, engines.node at least 20 |
@axiomhq/logging | 0.4.0 | 2026-08-04 | no production dependencies, peer @axiomhq/js in the ^1.0.0 range |
@axiomhq/nextjs | 0.4.0 | 2026-08-04 | peer next at least 13, peer @axiomhq/logging exactly 0.4.0 |
@axiomhq/react | 0.4.0 | 2026-08-04 | depends on web-vitals and use-deep-compare |
@axiomhq/pino | 2.0.0 | 2026-08-04 | depends on @axiomhq/js exactly 2.0.0 |
@axiomhq/winston | 2.0.0 | 2026-08-04 | depends on @axiomhq/js exactly 2.0.0 |
@axiomhq/tanstack-start | 0.2.0 | 2026-06-29 | peer @axiomhq/logging exactly 0.3.0 |
@axiomhq/ai | 0.8.0 | 2025-08-04 | abandoned, succeeded by the axiom package |
Three mismatches show up immediately. The first is the most serious, because it affects the combination most people will pick: @axiomhq/logging 0.4.0, released the same day as the 2.0 core, declares a peer dependency on @axiomhq/js in the ^1.0.0 range. The current core is numbered 2.0.0, so the range does not cover it, and installing both current packages together reports a conflict. The workaround is holding the core on the version 1 line or silencing the warning, which means running on a combination the vendor never declared supported.
Second: @axiomhq/tanstack-start was not bumped in the August batch and still pins @axiomhq/logging to exactly 0.3.0. Install @axiomhq/nextjs 0.4.0 in the same project and you get a peer dependency conflict, because both declarations are rigid, with no range.
The third mismatch is worse because it is silent. @axiomhq/ai stopped at 0.8.0 on 4 August 2025 and still carries the latest tag, yet it was never marked deprecated. Development moved to a package simply named axiom, version 0.52.2 from 12 May 2026, pointing at the axiomhq/ai repository under packages/ai. I checked whether this is a squatted name: the description, repository address and maintainer list line up with the rest of the Axiom packages. That tarball carries a LICENSE.md with MIT text and a dist directory of roughly 2 megabytes, but no src directory. In the axiomhq/ai repository the license file is named LICENSE.md, so a scanner looking for LICENSE will report it missing.
The 2.0 jump itself has exactly one breaking change, spelled out in the changelog: APL queries without an explicit format now return a tabular result. The previous default was the legacy format with a different response structure. Migration comes down to deciding whether you add format: 'legacy' to every existing call or rewrite the code that reads the result. Beyond that, a fetch option landed in ClientOptions, letting you supply your own implementation, along with handling for responses that are not JSON.
The earlier releases are worth tracing, because several changes affect runtime behaviour. Version 1.6.0 from 30 March 2026 turned on gzip compression of ingest payloads by default. Version 1.6.1 from 6 May 2026 fixed the event batching logic. Version 1.7.0, published on 17 June 2026, moved to the v2 datasets API, added metrics and MPL support, and read-only operations for dashboards, saved queries and users. Version 1.8.0 from 29 June 2026 added the X-Axiom-Client header.
Watch the dates. The changelog dates 1.7.0 to 9 June and 1.8.0 to 25 June, while the npm publishes are 17 and 29 June respectively. That is a normal gap between merge and release, but if you build a timeline from one source you will get different numbers than from the other. A separate curiosity from the registry: more than thirteen months passed between the 1.3.1 and 1.4.0 publishes, from 16 December 2024 to 27 January 2026.
Every current package requires Node 20 or higher. The Next.js package README says it unambiguously: Node 18 is no longer supported.
What it looks like in code
Installing the set for a Next.js application is one command from the package README.
npm install @axiomhq/js @axiomhq/logging @axiomhq/nextjs @axiomhq/reactThe client itself takes token as its only required field. orgId is needed only with a personal token, url points at the API address for administrative operations, and edge routes ingest and queries to an edge domain.
import { Axiom } from '@axiomhq/js'
const axiom = new Axiom({
token: process.env.AXIOM_TOKEN!,
orgId: process.env.AXIOM_ORG_ID,
url: 'https://api.eu.axiom.co'
})
const status = await axiom.ingest('web-logs', [{ path: '/checkout', ms: 128 }], {
timestampField: 'ts',
timestampFormat: 'unix'
})
console.log(status.ingested, status.failed, status.processedBytes)
await axiom.flush()The Axiom class batches events, so in a short-lived process, a serverless function for instance, flush is mandatory. If you prefer immediate delivery, reach for the AxiomWithoutBatching class exported alongside it. The returned IngestStatus carries ingested, failed, failures, processedBytes, blocksCreated and walLength.
An APL query, and the spot where the 2.0 change bites.
const tabular = await axiom.query(`
['web-logs']
| where ms > 500
| summarize count() by bin_auto(_time), path
`, {
startTime: '2026-08-01T00:00:00Z',
endTime: '2026-08-22T00:00:00Z',
noCache: true
})
const legacy = await axiom.query("['web-logs'] | count", { format: 'legacy' })With no format field you get a TabularQueryResult. With 'legacy' you get a QueryResult in the old shape. The signatures are overloaded, so TypeScript flags the difference straight away, but plain JavaScript code will only break at runtime.
The logger is a separate package, and since 0.4.0 it can validate event fields with any validator implementing Standard Schema.
import { z } from 'zod'
import { Logger, ConsoleTransport, AxiomJSTransport } from '@axiomhq/logging'
const LogFieldsSchema = z.object({
userId: z.string(),
action: z.enum(['login', 'logout'])
}).strict()
export const logger = new Logger({
transports: [
new AxiomJSTransport({ axiom, dataset: process.env.AXIOM_DATASET!, logLevel: 'info' }),
new ConsoleTransport({ prettyPrint: true })
],
schema: LogFieldsSchema,
onValidationError: (context) => {
console.warn('Dropped invalid log', context.stage, context.reason, context.issues)
}
})
logger.info('User action', { userId: '123', action: 'login' })An event that fails validation is dropped before it reaches a transport, so onValidationError is the only place you will hear about it. The logger.raw method bypasses validation. An outputSchema field checks the event after it has passed through formatters.
Wiring it into a Next.js route looks like this.
import { createAxiomRouteHandler, nextJsFormatters } from '@axiomhq/nextjs'
export const withAxiom = createAxiomRouteHandler(logger)
export const GET = withAxiom(async () => {
logger.info('Hello World!')
return new Response('Hello World!')
})Pricing: three meters and the arithmetic
The pricing page renders without JavaScript, so the numbers below come from raw HTML fetched on 22 August 2026, not from reading a screen.
There are three meters and this is the key thing to grasp: data loading measured in gigabytes, query compute measured in gigabyte-hours, and storage measured in gigabytes. The answer to whether queries are billed separately is therefore yes, as a separate gigabyte-hour meter.
| Item | Personal | Axiom Cloud |
|---|---|---|
| Fixed fee | 0 USD per month, permanent | 25 USD per month plus usage |
| Data loading in allowance | 500 GB per month | 1 TB per month |
| Query compute in allowance | 10 GB-hours per month | 100 GB-hours per month |
| Storage in allowance | 25 GB | 100 GB |
| Retention | 30 days | configurable |
| Volume discounts | none | automatic, threshold based |
| Prepaid credits | none | up to 30 percent off, never expire |
| Spending cap | not applicable | yes, in console |
| Support | community | email included, SLA as an add-on |
Enterprise add-ons are bought in the console at fixed prices: SAML SSO at 100 USD per month, SCIM Directory Sync at 100 USD per month, role-based access control at 50 USD per month, audit log at 50 USD per month. All four together come to 300 USD per month, twelve times the fixed fee and 3,600 USD per year on top of usage. Anyone who needs SSO for compliance reasons should start the arithmetic from that number, not from twenty-five dollars.
What happens past the allowance. The questions section says pay-as-you-go kicks in automatically, but the scope of that sentence is narrowed to the Axiom Cloud plan. Unit rates are said to drop at higher thresholds. A separate question about spending limits answers that usage beyond the limit is paused rather than auto-billed. That means you buy a hard spending cap at the price of losing data, because paused ingest is events you will not recover later.
What the page does not give you. Nowhere in the fetched HTML are there unit rates per gigabyte of loaded data, per gigabyte-hour, or per gigabyte of storage. The calculator computes them in the browser. The page advertises a machine-readable rate card at /pricing.md, but that address returns 404, which I checked the same day. So a bill above the allowance cannot be computed from published numbers, and I will not guess those rates.
Now the promised examples. I am assuming storage counts as the volume actually held within the retention window, because the page does not specify whether it means data before or after compression. That is an assumption, not a confirmed rule.
An application producing 10 GB of logs per month. Loading fits in both plans with plenty of headroom. At thirty-day retention roughly 10 GB sits in storage, under the 25 GB limit on Personal. Queries fit in ten gigabyte-hours, as long as you are not refreshing dashboards over the whole history every few seconds. The Personal bill is zero. On Axiom Cloud it is 25 USD per month, or 300 USD per year, and those 300 dollars pay for the fixed fee and add-ons, not for log volume, because volume contributes nothing here.
An application producing 100 GB of logs per month. Loading still fits the allowance on both plans. Storage stops fitting. At thirty-day retention you hold roughly 100 GB, four times the Personal limit. The page does not say what happens then on a plan with no pay-as-you-go, because the overage answer is scoped to Axiom Cloud only. On Axiom Cloud 100 GB is exactly the allowance boundary, so the bill is 25 USD per month for as long as you do not cross it by a single gigabyte. Above that, rates apply that the vendor does not publish.
Finally, a discrepancy I found on that same page. The plan card and the comparison table both put the Axiom Cloud storage allowance at 100 GB. The calculator in its default setting, at one terabyte loaded per month, shows a storage row in the breakdown reading 12 TB retained · within free allowance with the value Included. Twelve terabytes is a hundred and twenty times a hundred gigabytes. I am reporting both numbers, because both sit in the same document and I cannot determine which one describes the billing rule.
Axiom next to Sentry, PostHog and the rest
The most common mistake in this category is not picking the wrong tool, it is buying four at once and paying for the same event four times. These tools do not replace one another, they answer different questions.
| Tool | Question it answers | Unit of data | Model |
|---|---|---|---|
| Axiom | what exactly happened in the system at 3:14 | an event with arbitrary fields | closed service, MIT clients |
| Sentry | which exception breaks the most sessions | an error with a stack trace and grouping | service with a self-hosted variant |
| PostHog | do users finish signing up | a product event with an identity | service with a self-hosted variant |
| Langfuse, Helicone | what a model call cost and where it stalled | a call trace with tokens | service with a self-hosted variant |
| ClickHouse | anything, if you build the platform yourself | a row in a table with an imposed schema | a database you operate |
The difference from Sentry is the one most often blurred, so let me spell it out separately. Sentry takes an exception, computes its fingerprint, folds a thousand occurrences into one issue, attaches release context and assigns an owner. Axiom takes a raw event and assumes nothing about it. When you know something is breaking and want to understand why, you go to Axiom for the full request context. When you want to know what breaks most often and from which deploy, you go to Sentry. Pushing every exception into Axiom and trying to reconstruct grouping in APL ends with your own Sentry maintained after hours.
Against the classic logging stack, Elasticsearch with a search layer on top, Axiom mostly sells the absence of a cluster to operate. That is a genuine saving in a team with no administrator, but you buy it with the lock-in described above. Against ClickHouse the arrangement is simpler still: ClickHouse is an engine, Axiom is a finished platform on an engine of that class. The question is whether you want to run a database, not which engine is faster.
Two layers are worth separating, because they are easy to confuse. Axiom stores events and lets you query them, while Grafana stores nothing: it draws dashboards over data living elsewhere. That means they are not alternatives and in many deployments they sit side by side. Two things about Grafana are worth knowing before deciding: since version 8 it is under AGPL, with a separate LICENSING.md listing thirteen directories left on Apache 2.0, and a full picture still needs data sources, because Grafana does not collect anything itself.
Common mistakes
Upgrading to 2.0.0 without reading a single line of the changelog. A query with no format field now returns a different result shape. In TypeScript you get a compile error, in plain JavaScript you get undefined in production.
Installing @axiomhq/ai in the belief that it is the current package for tracing model calls. It carries the latest tag, it is not marked deprecated, and its last publish is from August 2025. Development lives in the axiom package.
Dropping @axiomhq/tanstack-start next to a current @axiomhq/nextjs. The first pins @axiomhq/logging to 0.3.0, the second to 0.4.0, both declarations without a range.
Staying on Node 18. Every current package sets engines.node to at least 20, and the README says so outright.
Buying a hard spending cap without understanding what it does. Usage beyond the limit is paused, so you protect the bill at the price of a hole in the data exactly when something went wrong and the logs are heaviest.
Watching only the loading meter. Query compute is billed separately, so a dashboard refreshing every ten seconds over a year-long window can eat the gigabyte-hour allowance at negligible log volume.
Trusting an automated license scanner. In axiomhq/axiom-js the file is called LICENSE and has no title line, in axiomhq/ai it is called LICENSE.md, and the axiom package tarball has no src directory. Any of those can produce a false alarm or false comfort.
FAQ
Can Axiom be self-hosted?
No. The MIT license covers the client libraries in the axiom-js repository only. The EventDB store and the query engine are closed, and the plan comparison table lists one deployment mode, managed by the vendor.
What will one hundred gigabytes of logs per month actually cost?
On Axiom Cloud, 25 USD per month for as long as you stay within the hundred gigabyte storage allowance, which at thirty-day retention is a hair's-breadth boundary. Rates above the allowance are not published on the pricing page, and the advertised rate card at /pricing.md returns 404.
What changed in version 2.0.0 of the JavaScript client?
One breaking change: an APL query without an explicit format field now returns a tabular result instead of the old legacy format. The release shipped on 4 August 2026. A fetch option was also added to ClientOptions.
Does Axiom replace Sentry?
No. Sentry groups exceptions into issues with a fingerprint, release context and an owner. Axiom stores raw events and assumes nothing about their structure. The two answer different questions and most often stand side by side.
Is the free plan production-ready?
For a small service, yes: 500 GB of loading per month and 30 days of retention cover many applications. The constraints are 25 GB of storage, ten gigabyte-hours of queries and community-only support. The page does not specify what happens once you exceed the allowance on Personal.
Does APL query metrics?
No. The documentation states plainly that metrics use a separate language, MPL, with its own endpoint. The client mirrors that split through mplQuery and the MetricsQueryOptions type alongside the ordinary query.