Tutorials / Backend integrations / Log management with Axiom
πŸ“ Written ● Intermediate Updated 2026-05-13

Log management with Axiom

Ship structured logs from any backend to Axiom, grep them with APL (SQL-like), keep 30 days on the free tier. Sentry catches exceptions; Axiom catches everything else you printed.

Pick Axiom when

0
  • Pick Axiom when β€” you want a free tier that's actually usable (0.5 TB ingest, 30-day retention), a query language that doesn't feel like the 2000s, and no per-seat pricing.
  • Alternatives β€” Better Stack Logs (same vendor as Better Stack uptime), Datadog (much more powerful, much more expensive), Elastic (self-host friendly), Loki (open-source, you operate it), SigNoz (open-source observability).
  • Don't use Axiom for β€” exception capture with grouping/alerting. Use Sentry for that. Different shape.
  • Sign up β€” app.axiom.co/register. No credit card.

Create a dataset

1

A dataset is the bucket your logs land in. Dashboard β†’ Datasets β†’ New dataset. Name it after the service: api-prod, web-prod, worker-prod. Separate datasets per service make queries cheaper and retention configurable per service.

Don't dump everything into one logs dataset β€” once you have 10M events you'll regret it.

Get an API token

2

Settings β†’ API tokens β†’ New API token. Scope: Ingest only (for the app). For the read side (queries from a dashboard), create a separate Query token.

Add to .env:

AXIOM_TOKEN=xaat-…
AXIOM_DATASET=api-prod
AXIOM_ORG_ID=your-org-id  # under Settings β†’ Organization

Ship logs from Node

3

Install the SDK:

npm install @axiomhq/js

Wire it up β€” preferably behind pino so your code stays portable:

import pino from "pino";
import { AxiomJSTransport } from "@axiomhq/pino";
import { Axiom } from "@axiomhq/js";

const axiom = new Axiom({ token: process.env.AXIOM_TOKEN });

const logger = pino(
  { level: "info" },
  new AxiomJSTransport({
    axiom,
    dataset: process.env.AXIOM_DATASET,
  }),
);

// Usage
logger.info({ userId: user.id, action: "signup" }, "user signed up");
logger.error({ err, requestId: req.id }, "stripe webhook failed");

The transport batches every 1s or 1MB, so per-log overhead is near-zero. Crash-safe: a synchronous flushSync() on shutdown ships the tail.

Docs: Send data from Node.js.

Ship logs from Vercel / Cloudflare / Fly

4

You don't need the SDK β€” install the integration. Native log drain, no code change.

  • Vercel β€” Vercel Axiom integration. One-click install, every console.log + request log streams to your dataset.
  • Cloudflare Workers β€” Workers Logpush guide. Configure once, every Worker invocation logs.
  • Fly.io β€” Fly log shipper. Runs as a sidecar.
  • Railway / Render / DO App Platform β€” no native integration; use the SDK from app code.
  • Kubernetes / VPS β€” install Vector as a daemon and point it at Axiom's HTTP endpoint. Vector + Axiom guide.

Query with APL

5

APL = Axiom Processing Language. SQL-shaped, optimized for time-series. Open Datasets β†’ api-prod β†’ Query:

// Last hour of errors
['api-prod']
| where _time > ago(1h) and level == "error"
| project _time, msg, requestId, userId

// Top 10 endpoints by error rate
['api-prod']
| where _time > ago(24h)
| summarize errors=countif(level == "error"), total=count() by route
| extend errorRate = round(100.0 * errors / total, 2)
| top 10 by errorRate

// Drill into one user's session
['api-prod']
| where userId == "u_abc123" and _time > ago(7d)
| order by _time asc

Save useful queries as starred queries (kept in the sidebar) and turn high-value ones into Monitors (next step).

APL reference: axiom.co/docs/apl/introduction.

Alert on log patterns (monitors)

6

Monitors β†’ New monitor. Pick a query, a threshold, a window. Examples:

  • countif(level == "error") > 50 over 5 min β†’ page oncall
  • countif(msg contains "OOM") > 0 β†’ Slack #infra
  • countif(path == "/api/checkout" and status >= 500) > 5 β†’ page founders

Notification channels: Slack, PagerDuty, Discord, email, webhook. Wire Opsgenie or PagerDuty via webhook if you have on-call rotations.

Common failures

7
  • Logs missing fields β€” pino with a plain string (logger.info("user signed up")) ships a single msg field. Pass an object first: logger.info({ userId }, "user signed up"). Fields become queryable columns.
  • PII leaks β€” don't log raw email addresses, payment tokens, or auth headers. Axiom is a third party with access. Add a redaction layer in pino: redact: ["req.headers.authorization", "*.email", "*.password"].
  • Ingest quota burst β€” a runaway debug log can eat 100 MB in a minute. Set per-dataset ingest caps in Settings β†’ Datasets. Axiom drops past the cap with a warning rather than billing surprise.
  • Logs trail behind by 10s β€” that's batch interval, not a bug. For "live tail" feel, lower flushInterval to 500ms or use the dashboard's live mode (it polls every 2s).
  • Time skew on self-hosted clock-broken servers β€” Axiom uses event _time from the payload. If your host's clock is off by hours, logs appear in the wrong bucket. Run chrony or timesyncd.

Pricing reality

8
  • Free β€” 0.5 TB ingest/mo, 30-day retention, 3 monitors. Plenty for <100 req/s services.
  • Team β€” $25/mo + $0.30/GB above quota. 95-day retention. Unlimited monitors.
  • Comparison β€” Datadog Logs is $0.10/GB ingest but $1.70/M events for retention; Axiom flat ingest pricing is typically 5–20Γ— cheaper for log-heavy workloads. Loki is free if you operate it (S3 storage, you pay for that).

Pricing page: axiom.co/pricing.

Official references

Pair it with Sentry. Sentry handles "exceptions with stack traces grouped + alerted"; Axiom handles "everything I logged and want to grep later." They overlap by ~5% and complement by 95%.