Contents
- Why this reference exists
- The eight accounting record types
- Core columns every record carries
- Proxy fields added in PowerMTA 5.5
- Sample records with field-by-field breakdown
- The acct-file configuration directive
- Parsing dsnDiag for operational signal
- Ingestion patterns for ClickHouse and Postgres
- Queries operators run during incidents
- Common log gaps and how to plug them
Why this reference exists
Most PowerMTA deployments leak the same operational insight every day: the accounting CSV gets written, rotated, archived, and never queried beyond a basic "did this message deliver" lookup. That is a waste. The accounting log is the highest-fidelity record of what each receiving MTA actually told PowerMTA, and operators who learn to read it pull deliverability signal out of it that no third-party reputation tool can offer. This reference exists because the official Bird documentation lists the fields but rarely explains which ones matter during an incident, and incident is when operators need this information most.
The structure is operator-first: record types in the order you will see them under load, then the columns that show up in every record, then the proxy additions that landed in PowerMTA 5.5 and confused everyone for two quarters, then sample records broken down field by field, then the acct-file directive that controls what gets written, then the practical work of parsing dsnDiag, ingesting into ClickHouse or Postgres, and running the four or five queries operators reach for during incidents.
Verified against PowerMTA 5.5 and 5.6 release notes as of early 2026. Bird (which acquired the SparkPost portfolio including PowerMTA) has kept the accounting format stable across minor versions, so this reference applies to any 5.x deployment, though specific proxy fields are 5.5+ only.
The eight accounting record types
PowerMTA writes one record per recipient per delivery attempt, and the first column of every record tells you which type it is. There are eight types, and operators rarely enable all eight at once because some duplicate information for downstream consumers.
| Type | Meaning | When written | Default? |
|---|---|---|---|
d | Delivered | Receiving MTA returned 2xx after DATA | Yes |
b | Bounced | Permanent failure (4xx after retry exhaustion or 5xx) | Yes |
t | Transient | Each temporary failure that triggers a retry | No |
tq | Transient-queue | Moment the message enters the transient queue | No |
r | Received | Message accepted into PowerMTA | No |
f | Feedback loop | ARF complaint received at the FBL address | No |
rb | Remote bounce | Bounce captured from an upstream PowerMTA node | No |
rs | Remote status | Status update from an upstream PowerMTA node | No |
The default setup of d and b only is a holdover from an era when log volume mattered more than operational insight. Modern operators running deliverability programs enable d, b, t, rb at minimum. The transient records are the most undervalued: by the time a deferral becomes a bounce, the reputation damage is already done. Operators who watch t records by destination domain catch ISP throttling within hours instead of days.
The f records are essential the moment you have a feedback loop set up with any provider that supports it (Microsoft SNDS-JMRP, Yahoo Complaint Feedback Loop, Comcast, AT&T). PowerMTA parses incoming ARF messages and writes one f record per complaint, with the original recipient address preserved in orig. Suppression list maintenance reads from this stream.
Core columns every record carries
Every accounting record carries a common set of columns regardless of type. The order in the CSV is fixed and matches the PowerMTA documentation, but operators frequently re-order on ingestion to match downstream schemas. The full set as of PowerMTA 5.6:
| Column | Type | Meaning |
|---|---|---|
type | char(1) | Record type: d, b, t, tq, r, f, rb, rs |
timeLogged | timestamp | When PowerMTA wrote this record (ISO 8601 with timezone) |
timeQueued | timestamp | When the message originally entered PowerMTA |
orig | string | MAIL FROM address (return-path / envelope sender) |
rcpt | string | RCPT TO address (final delivery recipient) |
orcpt | string | Original recipient if rewritten (DSN ORCPT parameter) |
dsnAction | string | relayed, delivered, failed, delayed, expanded per RFC 3464 |
dsnStatus | string | Enhanced status code per RFC 3463 (e.g. 2.0.0, 4.2.1, 5.7.1) |
dsnDiag | string | Full diagnostic text, smtp;CODE TEXT format |
dsnMta | string | Receiving MTA hostname (from EHLO or banner) |
srcType | string | api, smtp, pickup, file (how message was submitted) |
srcMta | string | Submitter hostname or IP for smtp sources |
dlvSourceIp | ip | PowerMTA outbound IP used for delivery |
dlvDestinationIp | ip | Receiving MTA IP after MX resolution |
dlvDestinationPort | int | Port used (25, 587, 465) |
dlvEsmtpAvailable | bool | Whether EHLO succeeded with extensions |
dlvSize | int | Message size in bytes as delivered |
vmta | string | Virtual MTA name handling this delivery |
jobId | string | X-Job header value if injected at submission |
envId | string | DSN ENVID parameter for tracking correlation |
queue | string | Queue name (typically destination-domain-based) |
vmtaPool | string | VirtualMTA-pool grouping the vmta belongs to |
retryCount | int | Attempt number (0 for first attempt) |
Operators frequently need to add custom columns through the log-headers directive (capturing X-Mailer, List-Unsubscribe, or other header values into the accounting record). Custom columns appear after the standard set in the order declared, and downstream parsers need to know about them or they will misalign fields.
Proxy fields added in PowerMTA 5.5
PowerMTA 5.5 introduced proxy support and three new accounting fields to expose what was happening when a delivery went through a proxy. These fields trip operators up because they are blank for non-proxied deliveries and people forget they exist until they are debugging reputation issues on a proxied setup.
| Column | Meaning | Blank when |
|---|---|---|
dlvProxyClientIp | IP PowerMTA used to connect to the proxy (typically internal) | No proxy in use |
dlvProxyServerIp | Public IP the proxy used to reach destination MTA | No proxy in use |
dlvProxyClientHostname | EHLO hostname PowerMTA presented to the proxy | No proxy in use |
The critical operational point: when PowerMTA delivers through a proxy, dlvSourceIp shows the IP PowerMTA used to reach the proxy (often a private/internal IP), and dlvProxyServerIp shows the public IP the receiving MTA actually saw. Reputation analysis must use dlvProxyServerIp when set, falling back to dlvSourceIp when blank. Query patterns frequently look like coalesce(dlvProxyServerIp, dlvSourceIp) AS effective_source_ip to make this unambiguous.
If an operator reports "all my deliveries are going from 10.x.x.x and my IP warmup is broken", check whether the deployment is behind a proxy. The dlvSourceIp field will show internal IPs that look broken but are actually correct given the architecture, and dlvProxyServerIp will hold the public IPs that ISPs actually saw. Update Grafana dashboards and alerting queries to use the coalesce pattern above when migrating to a proxied setup.
Sample records with field-by-field breakdown
A successful Gmail delivery looks like this in the accounting CSV:
d,2026-04-12 14:23:18+0000,2026-04-12 14:23:15+0000,bounce@mail.example.com,alice@gmail.com,,relayed,2.0.0,smtp;250 2.0.0 OK 1639424728 r4-20020a056808100400b0021bb15c5f33si4856230oih.71 - gsmtp,gmail-smtp-in.l.google.com,api,,203.0.113.42,142.250.80.27,25,true,42891,marketing-pool-vmta-01,J-AX9482,E-7723,gmail.com,marketing-pool,0,,,,
Reading left to right: type d (delivered), logged at 14:23:18 UTC, originally queued 3 seconds earlier at 14:23:15, return-path bounce@mail.example.com, recipient alice@gmail.com, no orcpt rewrite, dsnAction relayed, dsnStatus 2.0.0 (success), full Gmail acknowledgement in dsnDiag, receiving MTA gmail-smtp-in.l.google.com, submitted via api, no submitter MTA (api submission), delivered from outbound IP 203.0.113.42 to Google's 142.250.80.27 on port 25, ESMTP available, 42,891 bytes, handled by virtual MTA marketing-pool-vmta-01, job ID J-AX9482 from X-Job header, envelope ID E-7723 for downstream correlation, queue gmail.com, pool marketing-pool, first attempt (retryCount 0). The trailing four commas are the proxy fields blank because this delivery did not use a proxy.
A Microsoft Outlook bounce after content filtering looks like this:
b,2026-04-12 14:25:02+0000,2026-04-12 14:24:59+0000,bounce@mail.example.com,bob@outlook.com,,failed,5.7.1,smtp;550 5.7.1 [PR2EUR03FT051.eop-EUR03.prod.protection.outlook.com 12] Our system has detected that this message is suspicious due to the nature of the content,outlook-com.olc.protection.outlook.com,api,,203.0.113.42,52.96.165.18,25,true,43102,marketing-pool-vmta-02,J-AX9483,E-7724,outlook.com,marketing-pool,0,,,,
Same shape but type b (bounced), dsnAction failed, dsnStatus 5.7.1 (policy rejection), and the dsnDiag carries Microsoft's specific suspicion message naming the EOP (Exchange Online Protection) cluster that made the call. The PR2EUR03 prefix identifies a European region cluster which can matter for reputation work because Microsoft reputation is partially regional.
A transient failure during Yahoo throttling looks like this:
t,2026-04-12 14:30:11+0000,2026-04-12 14:28:45+0000,bounce@mail.example.com,charlie@yahoo.com,,delayed,4.2.1,smtp;421 4.2.1 [TSS04] Messages from 203.0.113.42 temporarily deferred due to user complaints - 4.16.55.1; see https://help.yahoo.com/kb/postmaster/SLN3434.html,mta7.am0.yahoodns.net,api,,203.0.113.42,98.137.157.43,25,true,43210,marketing-pool-vmta-01,J-AX9484,E-7725,yahoo.com,marketing-pool,2,,,,
This is a t record (transient), with dsnStatus 4.2.1 indicating mailbox over quota or deferred, retryCount 2 meaning this is the third attempt, and the dsnDiag explicitly naming the TSS04 throttling code with a link to Yahoo's documentation. Operators watching for TSS04 across many recipients in a short window detect reputation slips before they become 421-then-bounce patterns.
The acct-file configuration directive
The acct-file directive in /etc/pmta/config controls what gets written, where, and how files rotate. A production-grade configuration:
<acct-file /var/log/pmta/accounting.csv>
records d,b,t,rb,f,p,rp
record-fields *,header_x-customer-id,header_x-campaign-id
max-size 500M
max-age 1d
move-to /var/log/pmta/archive
move-interval 1h
</acct-file>
Reading the directive: write delivered, bounced, transient, remote bounce, feedback, pickup, and received-by-pickup records; include all standard fields plus two custom columns extracted from X-Customer-Id and X-Campaign-Id headers; rotate when either 500MB or 1 day reached, whichever first; move rotated files to the archive directory; check for rotation conditions every hour rather than only on PowerMTA restart.
The record-fields * wildcard is critical: without it, only the default fields are written and any custom header columns added later will not appear retroactively. Operators frequently miss this and find their custom fields empty for weeks before noticing.
The move-to with archive directory is essential for any downstream ingestion pipeline. Without it, log rotation rewrites the live file and ingestion processes that have the file open get confused about what they have already consumed. The standard pattern is move-to to a separate directory, then a cron-driven ingestion job picks up files from the archive in timestamp order, ingests, and deletes (or moves to long-term storage).
Parsing dsnDiag for operational signal
The dsnDiag field is freeform text but carries patterns that operators can match against to surface specific ISP signals. The standard structure is smtp;CODE TEXT where the smtp; prefix and the numeric code are reliable and the text varies by receiving MTA. A few patterns worth alerting on:
Gmail. 421-4.7.0 [203.0.113.42 15] Our system has detected an unusual rate of unsolicited mail indicates rate-based throttling. 421-4.7.28 [203.0.113.42 15] Our system has detected an unusual rate of unsolicited mail from a domain in your SPF record indicates SPF-aligned reputation problem. 550-5.7.26 This message does not have authentication information or fails to pass indicates DMARC alignment failure. Specific KB links sometimes appear in the dsnDiag and operators can route alerts based on which KB URL is referenced.
Microsoft. S3140 and S3150 codes appear in dsnDiag for spam-related rejection. OU-002 indicates suspicious sending pattern from the source IP. The EOP cluster name (PR2EUR03, SJ0EUR01, BL0EUR01) tells operators which Microsoft region made the call, which matters because reputation is partly regional. The 5.7.1 codes from Microsoft carry detailed sub-reasons in the dsnDiag text that operators can classify into bucket types (content filter, IP reputation, domain reputation, authentication failure).
Yahoo. TSS04 in dsnDiag means deferred-due-to-complaints, which is one of the strongest reputation signals in the entire delivery ecosystem because it appears before bounces and signals that complaint rate has crossed Yahoo's threshold. TSS01 means user-mailbox-temporarily-unavailable which is benign. Operators alert on TSS04 with extreme priority because it predicts a 24-48 hour window before complaint-based blocking.
Apple iCloud. 552 5.2.0 1n0CBg-002OPM-3K Internal Error patterns occasionally indicate iCloud throttling. iCloud is less verbose than other providers and patterns require accumulated experience to interpret. Watch for sudden shifts in iCloud delivery rates rather than specific codes.
One ESP operator runs a query every five minutes against the past 15 minutes of accounting records counting transient failures per destination domain per outbound IP. When the rate of transient failures for any (IP, domain) pair exceeds historical baseline by more than 3 standard deviations, an alert fires. The alert routes to the deliverability on-call who reviews the dsnDiag patterns to classify whether this is volume-induced throttling (expected, slow down sending), content-based suspicion (investigate the campaign), or reputation degradation (deeper investigation). This pattern caught 11 reputation incidents in 2025 that would otherwise have become full blocks within 24-48 hours.
Ingestion patterns for ClickHouse and Postgres
The two practical choices for ingestion are ClickHouse and Postgres. ClickHouse wins when log volume exceeds a few hundred million records per month and when the analytical workload is mostly time-series aggregation across many dimensions. Postgres wins when log volume is modest and the workload mixes lookup queries with transactional joins against other operational data.
The ClickHouse table definition mirrors the accounting columns directly:
CREATE TABLE pmta_accounting (
type LowCardinality(String),
timeLogged DateTime,
timeQueued DateTime,
orig String,
rcpt String,
orcpt String,
dsnAction LowCardinality(String),
dsnStatus LowCardinality(String),
dsnDiag String,
dsnMta String,
srcType LowCardinality(String),
srcMta String,
dlvSourceIp IPv4,
dlvDestinationIp IPv4,
dlvDestinationPort UInt16,
dlvEsmtpAvailable UInt8,
dlvSize UInt32,
vmta LowCardinality(String),
jobId String,
envId String,
queue LowCardinality(String),
vmtaPool LowCardinality(String),
retryCount UInt8,
dlvProxyClientIp Nullable(IPv4),
dlvProxyServerIp Nullable(IPv4),
dlvProxyClientHostname Nullable(String)
) ENGINE = MergeTree
PARTITION BY toYYYYMM(timeLogged)
ORDER BY (queue, dsnStatus, timeLogged);
The LowCardinality wrapper around enumerated fields (type, dsnAction, srcType, vmta, queue, vmtaPool) saves substantial storage and accelerates aggregation queries by 5-10x. The ORDER BY (queue, dsnStatus, timeLogged) optimizes the most common query pattern: "what happened on this destination domain with this status code over time".
Ingestion runs from a cron job that consumes files from /var/log/pmta/archive:
#!/bin/bash
for f in /var/log/pmta/archive/accounting-*.csv; do
clickhouse-client --query "INSERT INTO pmta_accounting FORMAT CSVWithNames" < "$f"
if [ $? -eq 0 ]; then
mv "$f" /var/log/pmta/ingested/
else
echo "FAILED: $f" >> /var/log/pmta-ingest-failures.log
fi
done
The Postgres equivalent uses COPY FROM and a similar schema. Postgres performance on billion-row accounting tables requires careful partitioning (by month) and an index on (rcpt, timeLogged) for lookup queries plus a BRIN index on timeLogged for time-range scans. Postgres works well up to a few billion rows on modest hardware; beyond that ClickHouse is the better fit.
Queries operators run during incidents
The queries that earn their keep during incidents are surprisingly few. The same four or five patterns answer almost every "what's happening right now" question.
Delivery rate by hour for a specific outbound IP over the past 24 hours:
SELECT
toStartOfHour(timeLogged) AS hour,
countIf(type = 'd') AS delivered,
countIf(type = 'b') AS bounced,
countIf(type = 't') AS deferred,
delivered / (delivered + bounced + deferred) AS delivery_rate
FROM pmta_accounting
WHERE coalesce(dlvProxyServerIp, dlvSourceIp) = '203.0.113.42'
AND timeLogged >= now() - INTERVAL 1 DAY
GROUP BY hour
ORDER BY hour;
Bounce reason classification for a destination domain over the past hour:
SELECT
dsnStatus,
substring(dsnDiag, 1, 80) AS diag_preview,
count() AS occurrences
FROM pmta_accounting
WHERE type = 'b'
AND queue = 'outlook.com'
AND timeLogged >= now() - INTERVAL 1 HOUR
GROUP BY dsnStatus, diag_preview
ORDER BY occurrences DESC
LIMIT 20;
Transient failure spike detection across all destinations:
SELECT
queue,
coalesce(dlvProxyServerIp, dlvSourceIp) AS source_ip,
count() AS transient_count
FROM pmta_accounting
WHERE type = 't'
AND timeLogged >= now() - INTERVAL 15 MINUTE
GROUP BY queue, source_ip
HAVING transient_count > 100
ORDER BY transient_count DESC;
Complaint rate by job ID over the past 7 days:
SELECT
jobId,
countIf(type = 'd') AS delivered,
countIf(type = 'f') AS complaints,
complaints / delivered AS complaint_rate
FROM pmta_accounting
WHERE timeLogged >= now() - INTERVAL 7 DAY
AND jobId != ''
GROUP BY jobId
HAVING delivered > 1000
ORDER BY complaint_rate DESC
LIMIT 20;
These four queries cover roughly 80% of incident response work. The remainder is custom investigation that builds on the same patterns with additional filters.
Common log gaps and how to plug them
Operators frequently report "the accounting log is missing X". The usual culprits:
Custom header fields are empty. The record-fields directive needs to be set to *,header_x-your-header to capture X- headers into accounting. Without record-fields, only standard fields are written.
Transient records are missing. Default config writes d and b only. Add records d,b,t,rb,f to the acct-file directive.
Proxy fields are blank when delivery clearly went through a proxy. Verify PowerMTA is actually 5.5+ and that the proxy directive in config is active. Some operators upgraded PowerMTA but did not migrate proxy config from a legacy mode.
dlvDestinationIp is the same as a known MX but dsnDiag mentions a different host. Receiving MTAs frequently rotate dsnMta hostnames within a delivery cluster even though the receiving IP stays constant. This is benign.
Records appear out of order in time. Multiple PowerMTA worker processes write the accounting file concurrently and order within a single second is not guaranteed. Sort by timeLogged in queries rather than relying on file order.
The archive directory is filling up. Operators sometimes set move-to but forget to set up downstream consumption. Add a retention policy that deletes ingested files after 90 days or moves them to S3-class storage.
Treated as a reference rather than an article, this document is the cheat sheet operators keep open during incidents. The fields are stable across PowerMTA 5.x and the query patterns generalize to any analytical store. The point is not to memorize columns but to know where to look when a delivery rate suddenly drops or a reputation incident is underway.