PowerMTA Accounting Log Field Reference: Complete 2026 Operator Guide

← Downloads

PowerMTA Accounting Log Field Reference: Complete 2026 Operator Guide to CSV Columns, Record Types, and Production Analysis

April 12, 2026·13 min read·Henrik Larsen

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.

TypeMeaningWhen writtenDefault?
dDeliveredReceiving MTA returned 2xx after DATAYes
bBouncedPermanent failure (4xx after retry exhaustion or 5xx)Yes
tTransientEach temporary failure that triggers a retryNo
tqTransient-queueMoment the message enters the transient queueNo
rReceivedMessage accepted into PowerMTANo
fFeedback loopARF complaint received at the FBL addressNo
rbRemote bounceBounce captured from an upstream PowerMTA nodeNo
rsRemote statusStatus update from an upstream PowerMTA nodeNo

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:

ColumnTypeMeaning
typechar(1)Record type: d, b, t, tq, r, f, rb, rs
timeLoggedtimestampWhen PowerMTA wrote this record (ISO 8601 with timezone)
timeQueuedtimestampWhen the message originally entered PowerMTA
origstringMAIL FROM address (return-path / envelope sender)
rcptstringRCPT TO address (final delivery recipient)
orcptstringOriginal recipient if rewritten (DSN ORCPT parameter)
dsnActionstringrelayed, delivered, failed, delayed, expanded per RFC 3464
dsnStatusstringEnhanced status code per RFC 3463 (e.g. 2.0.0, 4.2.1, 5.7.1)
dsnDiagstringFull diagnostic text, smtp;CODE TEXT format
dsnMtastringReceiving MTA hostname (from EHLO or banner)
srcTypestringapi, smtp, pickup, file (how message was submitted)
srcMtastringSubmitter hostname or IP for smtp sources
dlvSourceIpipPowerMTA outbound IP used for delivery
dlvDestinationIpipReceiving MTA IP after MX resolution
dlvDestinationPortintPort used (25, 587, 465)
dlvEsmtpAvailableboolWhether EHLO succeeded with extensions
dlvSizeintMessage size in bytes as delivered
vmtastringVirtual MTA name handling this delivery
jobIdstringX-Job header value if injected at submission
envIdstringDSN ENVID parameter for tracking correlation
queuestringQueue name (typically destination-domain-based)
vmtaPoolstringVirtualMTA-pool grouping the vmta belongs to
retryCountintAttempt 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.

ColumnMeaningBlank when
dlvProxyClientIpIP PowerMTA used to connect to the proxy (typically internal)No proxy in use
dlvProxyServerIpPublic IP the proxy used to reach destination MTANo proxy in use
dlvProxyClientHostnameEHLO hostname PowerMTA presented to the proxyNo 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.

Proxy field misreading is the #1 PowerMTA 5.5+ confusion

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.

Operator pattern: pre-bounce reputation alerting

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.

H
Henrik Larsen

Email Infrastructure Engineer at Cloud Server for Email. Operates production PowerMTA clusters and builds the accounting ingestion pipelines that feed downstream deliverability tooling. Related: PowerMTA Accounting Log Format Fields, dsnDiag Field Analysis, Domain Block Reference.