MailTime
Email queue + sender for horizontally scaled Node.js / Bun / Meteor. Built on JoSk. Queue: Mongo / Redis / Postgres. Modes: server (drains + sends), client (enqueues only). Many clients + servers share one prefix.
Use when
Package / API
- User names
mail-time, MailTime, MongoQueue, RedisQueue, PostgresQueue, or Meteor ostrio:mailer
- First integration: queue store, JoSk scheduler, nodemailer transports,
prefix, onSent / onError
- Presets (
mailTimePreset), retries, sendAt, concatEmails, concurrency, mode, drain() / destroy(), pause() / resume() (server backpressure)
- Multi-SMTP:
backup (failover) vs balancer (round-robin)
- Custom queue adapter per
references/adapters.md / docs/queue-api.md
Topology
- Client/server split — app
type: 'client', mail VM type: 'server'
- Multiple email classes — separate
MailTime + prefix per class (OTP, transactional, marketing)
- Cluster HA — many
server pods, one JoSk lease per prefix (failover, not N× throughput)
- Dedicated mail host — 2–8
server processes (~1/core), systemd mailtime@<class>
Problems (user may not say "MailTime")
- Duplicate emails across PM2 / Kubernetes / Meteor pods
- "Outbox pattern", "queue transactional mail", "make nodemailer HA"
- Retries / partial SMTP rejection / multi-recipient delivery
- Migrating from Agenda, Bull, BullMQ, Bree, sendgrid-queue
- JoSk + email queueing in the same conversation
Not this skill alone — deep JoSk scheduler semantics → josk skill (npx skills add veliovgroup/josk).
Reference map
references/api.md — methods, options, defaults, errors
references/adapters.md — adapter pick, schema, CAS / ___dispatch contract
references/tuning.md — topology, knobs, presets, anti-patterns
references/recipes.md — code: micro-service split, multi-SMTP, shutdown
Mental model
new MailTime({
type: 'server' | 'client',
queue: new <Mongo|Redis|Postgres>Queue({ client|db }),
transports: [nodemailer.createTransport(...)], // server only
josk: { adapter: { type, client|db }, lockOwnerId }, // server only
prefix: 'otp', // one per email class
strategy: 'backup' | 'balancer',
})
.sendMail(opts) -> uuid
.cancelMail(uuid) -> boolean
.ready() / .ping() / .drain() / .destroy() / .pause() / .resume()
- Queue stores letters; JoSk lease gates
queue.iterate() — one drainer tick per prefix cluster-wide.
- Claim CAS:
isSending + sendingAt + tries=task.tries prevents duplicate send (cluster + concurrency pool).
prefix flows to queue + JoSk; same prefix on all clients + servers for one logical queue.
- Iterate dispatches
___dispatch(row) (pool), not ___send directly — details: references/adapters.md.
Required scaffolding
Missing any → name it for the user:
- Queue store + connected client (
db, redis client, or pg.Pool)
- JoSk scheduler store + client (often same as 1)
- Nodemailer transports with
.options (server only)
onSent / onError (strongly recommended)
- Shutdown:
destroy() or await destroy({ drain: true }); after iterate in tests also await drain() when not using { drain: true }
Pick the queue adapter
Decision order — not "whatever DB we already run":
- Postgres — multi-DC, mixed clocks, strict exactly-once
- Redis — single-region throughput; requires
watch() + multi(); no active-active multi-master
- Mongo — app already on Mongo / Meteor; official
mongodb driver only
- Custom — seven-method contract:
references/adapters.md
Pairing table + Redis/Mongo/Postgres details: references/adapters.md.
Multi-SMTP strategy
| Strategy |
Use when |
'backup' (default) |
Primary + fallback SMTP; rotates after failsToNext failures |
'balancer' |
Equal-trust SMTPs; round-robin per enqueue |
ready() + verifyTransports: true (default) probes transports once; bad ones skipped. Full behavior: references/api.md.
Presets
mailTimePreset(name, overrides) — names: transactional, otp, newsletter, marketing, notifications, alerts. Values: presets.js / README. Examples: references/recipes.md.
JoSk / tuning
MailTime sets JoSk defaults when unset: minRevolvingDelay 512, maxRevolvingDelay 2048, zombieTime 60000, execute 'batch'. Knob tables + anti-patterns: references/tuning.md.
Common patterns
- Single app — one instance, one
prefix, queue + scheduler same store
- Multi-class —
otp / transactional / marketing each own prefix + preset
- Mail micro-service — apps
client; mail VM 2–8 server across prefixes
- Scheduled mail —
sendAt; cancel via stored uuid
Full code: references/recipes.md.
Red flags
- No
onError / onSent — silent failures
concatEmails: true on OTP / password-reset — separate instance
- CosmosDB / DocumentDB / Mongoose Mongo client — unverified; prefer Postgres or Redis
- Active-active Redis — duplicate claims; single primary or Postgres
sendingTimeout < worst SMTP roundtrip — duplicate send risk
- Custom
iterate → ___send not ___dispatch; custom update without tries === task.tries CAS
- Tests: missing
destroy() / drain() after iterate — hung process
More: references/tuning.md.
Runtime
Node ≥ 20.9.0, Bun ≥ 1.1.0. ESM + CJS (require). Peer: nodemailer + one store driver (redis / mongodb / pg). Runtime dep: josk only.
npm install mail-time nodemailer # + redis | mongodb | pg
Source: veliovgroup/mail-time — distributed by TomeVault.
1---2name: mail-time3description: MailTime email queue for horizontally scaled Node.js, Bun, and Meteor apps. Use when wiring MailTime, MongoQueue, RedisQueue, PostgresQueue, mailTimePreset, retries, sendAt, concatEmails, multi-SMTP backup/balancer, client/server split, custom queue adapters, clustered nodemailer, or JoSk with email queueing — including when the user does not name MailTime. Trigger on duplicate sends across cluster pods (PM2/Kubernetes/ECS), email outbox / HA sending, OTP vs marketing instances, dedicated mail workers, scheduled sendAt mail, or migrating from Agenda, Bull, BullMQ, Bree, sendgrid-queue. Use when this capability is needed.4---56# MailTime78Email queue + sender for horizontally scaled Node.js / Bun / Meteor. Built on [JoSk](https://github.com/veliovgroup/josk). Queue: Mongo / Redis / Postgres. Modes: `server` (drains + sends), `client` (enqueues only). Many clients + servers share one `prefix`.910## Use when1112**Package / API**13- User names `mail-time`, `MailTime`, `MongoQueue`, `RedisQueue`, `PostgresQueue`, or Meteor `ostrio:mailer`14- First integration: queue store, JoSk scheduler, nodemailer transports, `prefix`, `onSent` / `onError`15- Presets (`mailTimePreset`), retries, `sendAt`, `concatEmails`, `concurrency`, `mode`, `drain()` / `destroy()`, `pause()` / `resume()` (server backpressure)16- Multi-SMTP: `backup` (failover) vs `balancer` (round-robin)17- Custom queue adapter per `references/adapters.md` / `docs/queue-api.md`1819**Topology**20- Client/server split — app `type: 'client'`, mail VM `type: 'server'`21- Multiple email classes — separate `MailTime` + `prefix` per class (OTP, transactional, marketing)22- Cluster HA — many `server` pods, one JoSk lease per `prefix` (failover, not N× throughput)23- Dedicated mail host — 2–8 `server` processes (~1/core), systemd `mailtime@<class>`2425**Problems (user may not say "MailTime")**26- Duplicate emails across PM2 / Kubernetes / Meteor pods27- "Outbox pattern", "queue transactional mail", "make nodemailer HA"28- Retries / partial SMTP rejection / multi-recipient delivery29- Migrating from Agenda, Bull, BullMQ, Bree, sendgrid-queue30- JoSk + email queueing in the same conversation3132**Not this skill alone** — deep JoSk scheduler semantics → `josk` skill (`npx skills add veliovgroup/josk`).3334## Reference map3536- `references/api.md` — methods, options, defaults, errors37- `references/adapters.md` — adapter pick, schema, CAS / `___dispatch` contract38- `references/tuning.md` — topology, knobs, presets, anti-patterns39- `references/recipes.md` — code: micro-service split, multi-SMTP, shutdown4041## Mental model4243```44new MailTime({45 type: 'server' | 'client',46 queue: new <Mongo|Redis|Postgres>Queue({ client|db }),47 transports: [nodemailer.createTransport(...)], // server only48 josk: { adapter: { type, client|db }, lockOwnerId }, // server only49 prefix: 'otp', // one per email class50 strategy: 'backup' | 'balancer',51})52.sendMail(opts) -> uuid53.cancelMail(uuid) -> boolean54.ready() / .ping() / .drain() / .destroy() / .pause() / .resume()55```5657- Queue stores letters; JoSk lease gates `queue.iterate()` — one drainer tick per `prefix` cluster-wide.58- Claim CAS: `isSending` + `sendingAt` + `tries=task.tries` prevents duplicate send (cluster + `concurrency` pool).59- `prefix` flows to queue + JoSk; same `prefix` on all clients + servers for one logical queue.60- Iterate dispatches `___dispatch(row)` (pool), not `___send` directly — details: `references/adapters.md`.6162## Required scaffolding6364Missing any → name it for the user:65661. Queue store + connected client (`db`, `redis` client, or `pg.Pool`)672. JoSk scheduler store + client (often same as 1)683. Nodemailer transports with `.options` (server only)694. `onSent` / `onError` (strongly recommended)705. Shutdown: `destroy()` or `await destroy({ drain: true })`; after iterate in tests also `await drain()` when not using `{ drain: true }`7172## Pick the queue adapter7374Decision order — not "whatever DB we already run":7576- **Postgres** — multi-DC, mixed clocks, strict exactly-once77- **Redis** — single-region throughput; requires `watch()` + `multi()`; no active-active multi-master78- **Mongo** — app already on Mongo / Meteor; official `mongodb` driver only79- **Custom** — seven-method contract: `references/adapters.md`8081Pairing table + Redis/Mongo/Postgres details: `references/adapters.md`.8283## Multi-SMTP strategy8485| Strategy | Use when |86|---|---|87| `'backup'` (default) | Primary + fallback SMTP; rotates after `failsToNext` failures |88| `'balancer'` | Equal-trust SMTPs; round-robin per enqueue |8990`ready()` + `verifyTransports: true` (default) probes transports once; bad ones skipped. Full behavior: `references/api.md`.9192## Presets9394`mailTimePreset(name, overrides)` — names: `transactional`, `otp`, `newsletter`, `marketing`, `notifications`, `alerts`. Values: `presets.js` / README. Examples: `references/recipes.md`.9596## JoSk / tuning9798MailTime sets JoSk defaults when unset: `minRevolvingDelay` 512, `maxRevolvingDelay` 2048, `zombieTime` 60000, `execute` `'batch'`. Knob tables + anti-patterns: `references/tuning.md`.99100## Common patterns101102- **Single app** — one instance, one `prefix`, queue + scheduler same store103- **Multi-class** — `otp` / `transactional` / `marketing` each own `prefix` + preset104- **Mail micro-service** — apps `client`; mail VM 2–8 `server` across prefixes105- **Scheduled mail** — `sendAt`; cancel via stored `uuid`106107Full code: `references/recipes.md`.108109## Red flags110111- No `onError` / `onSent` — silent failures112- `concatEmails: true` on OTP / password-reset — separate instance113- CosmosDB / DocumentDB / Mongoose Mongo client — unverified; prefer Postgres or Redis114- Active-active Redis — duplicate claims; single primary or Postgres115- `sendingTimeout` < worst SMTP roundtrip — duplicate send risk116- Custom `iterate` → `___send` not `___dispatch`; custom `update` without `tries === task.tries` CAS117- Tests: missing `destroy()` / `drain()` after iterate — hung process118119More: `references/tuning.md`.120121## Runtime122123Node ≥ 20.9.0, Bun ≥ 1.1.0. ESM + CJS (`require`). Peer: `nodemailer` + one store driver (`redis` / `mongodb` / `pg`). Runtime dep: `josk` only.124125```sh126npm install mail-time nodemailer # + redis | mongodb | pg127```128129---130> Source: [veliovgroup/mail-time](https://github.com/veliovgroup/mail-time) — distributed by [TomeVault](https://tomevault.io).131<!-- tomevault:4.0:skill_md:2026-06-17 -->