Construction · Cash and revenue

DSO and billing lag

Days sales outstanding (DSO) measures how many days of billings are sitting in receivables. Billing lag measures how long after a billing period the invoice actually goes out. Together they show how long work takes to become cash.

Plan your metrics

What it is

Contractors usually bill monthly with a pay application. Billing lag is the days from the end of the billing period to the date the pay app is submitted. DSO is accounts receivable expressed as days of billings, so it covers the time after the invoice goes out.

Retainage is often excluded from DSO and tracked on its own, because it is not due until later.

Why operators track it

  • Cash. Every day of lag or DSO is cash the contractor is financing.
  • Controllable. Billing lag depends on the contractor's own process: getting quantities, PM review, and pay app approval.
  • Owner behavior. DSO by customer shows who pays slowly.
  • Lenders. Receivables are part of working capital, and lenders review them.

How it is calculated

DSO = accounts receivable ÷ billings in period × days in period

  • Exclude retainage from AR and from billings for DSO; report it separately.
  • Billing lag = invoice date − billing period end date, per pay app.
  • Use a 90-day window to smooth monthly swings, or a rolling 3 months.

Worked example

Job 2417's September pay app went out October 6, a billing lag of 6 days, and was paid October 29. Across the company, $1,240,000 of non-retainage AR against $2,790,000 billed in 90 days is a DSO of 40 days.

From work done to cash: job 2417, September workWork performed30 daysBilling lag6 daysInvoice to payment23 daysSep 1Sep 30invoiced Oct 6paid Oct 29Billing lag = invoice date − end of the billing period = 6 days. Days from invoice to payment = 23.Company DSO (excluding retainage) = $1,240,000 AR ÷ $2,790,000 billed in the last 90 days × 90 = 40 days.
Example data Billing lag is the part the contractor controls directly. DSO covers the time after the invoice goes out.

How to read it

  • Split the timeline. Lag before the invoice and DSO after it have different owners and fixes.
  • Exclude retainage or DSO will look worse on jobs near completion.
  • By customer. A few slow owners can drive the total.
  • DSO norms vary by owner type and contract, so we do not publish a benchmark.

Definitional choices and common pitfalls

  • Retainage in or out. Out is common for DSO. Say which.
  • Window. 30, 90 days, or rolling. A short window swings with billing timing.
  • Lag start. Period end date, or the pay app due date in the contract. Due date shows whether the contractor met the deadline.
  • Disputed invoices. Show separately so they do not mask normal collections.

How different roles use it

Role How they use it
CFO / controller Tracks DSO and aging by customer and sets collection priorities.
Project manager Gets pay apps approved and submitted on time.
Owner / CEO Watches DSO as a cash signal.
Surety / lender Reviews receivables and aging.

Questions it answers

  • How fast do we invoice after month end?
  • Which owners pay slowest?
  • How much cash is in receivables?
  • Is DSO getting better or worse?

How to present it in a report

  • KPIs for DSO, average billing lag, and AR over 60 days.
  • Billing lag by job and PM for the latest period.
  • AR aging by customer, retainage in its own column.
  • Trend of DSO by month.
Customer report mockupExample data
Billing and collectionsOctober 2026
DSO, ex retainage40 days90-day window
Avg billing lag7.7 daysSeptember pay apps
Slowest pay app14 daysjob 2420
JobCustomerPeriodInvoicedLagOpen AR, ex retainage
2420Pine MedicalSepOct 1414 days$34,500
2431CountySepOct 33 days$80,000
2417Main St LLCSepOct 66 days$0

Open AR by job as of Oct 31, all pay apps, excluding retainage.

Data you need

Fields

Field What it means Example value
Invoice / pay app Job, period, number Job 2417, pay app 7
Billing period end Last day of work billed Sep 30
Invoice date When it was submitted Oct 6
Amount and retainage Gross billed and retainage held $160,000 / $16,000
Payments Date and amount received Oct 29, $144,000
Customer Owner or GC being billed Main St LLC

What the raw data looks like

Example data A few rows as they might come out of the source system.

Job Pay app Period end Invoiced Gross Retainage Paid Paid date
2417 7 Sep 30 Oct 6 $160,000 $16,000 $144,000 Oct 29
2420 3 Sep 30 Oct 14 $35,000 $3,500 $0
2431 11 Sep 30 Oct 3 $210,000 $21,000 $189,000 Oct 29

Where this data usually lives

The same field can live in more than one system. Agree with the customer which one is the source of truth.

Common data gaps and traps

  • No billing period on invoices. Without period end, lag can only be estimated from invoice dates.
  • Pay apps outside the ERP. Pay apps may be prepared in a PM tool or spreadsheet and entered later.
  • Retainage not split. Hard to exclude from DSO.
  • Payments applied late. Unapplied cash makes AR look higher than it is.

FAQ

Should DSO include retainage?
Often not. Many teams track retainage separately since it is not due until later. Say which you use.
What is billing lag?
Days from the end of the billing period to when the pay app or invoice is submitted.
Why use a 90-day window?
Construction billing is lumpy. A longer window smooths it.
For your data teamFormula variants, tables, SQL sketch, and dashboard spec

Formula details

ar_ex_ret   = SUM(open_invoice_balance) - SUM(open_retainage)
billings    = SUM(gross_billed - retainage) over last :days
dso         = ar_ex_ret / billings * :days
billing_lag = invoice_date - period_end_date

Worked example detail

Item Value
AR excluding retainage $1,240,000
Billed in last 90 days, excluding retainage $2,790,000
DSO 1,240,000 ÷ 2,790,000 × 90 = 40 days
Billing lag, job 2417 pay app 7 Oct 6 − Sep 30 = 6 days

Tables and fields

Table Fields Notes
ar_invoices invoice_id, job_id, customer_id, period_end, invoice_date, gross_amount, retainage_amount
ar_payments invoice_id, paid_at, amount

SQL sketch

WITH ar AS (SELECT SUM(i.gross_amount - i.retainage_amount - COALESCE(p.paid,0)) AS ar_ex_ret
  FROM ar_invoices i LEFT JOIN (SELECT invoice_id, SUM(amount) paid FROM ar_payments WHERE paid_at <= :as_of GROUP BY 1) p USING (invoice_id)
  WHERE i.invoice_date <= :as_of),
b AS (SELECT SUM(gross_amount - retainage_amount) AS billed FROM ar_invoices WHERE invoice_date > :as_of - 90 AND invoice_date <= :as_of)
SELECT ar_ex_ret / NULLIF(billed,0) * 90 AS dso FROM ar, b;

Build spec

Copy this into a ticket. It lists the grain, inputs, logic, and edge cases.

metric: construction.dso_billing_lag
grain: company and customer per period; billing lag per pay app
inputs:
  - invoice_date
  - period_end
  - gross_amount
  - retainage_amount
  - payments
logic:
  - dso = AR excluding retainage / billings excluding retainage over window * days
  - lag = invoice_date - period_end
edge_cases:
  - no billings in window: return null
  - credit memos: net against billings
  - negative AR from overpayment: show, do not drop

As a dashboard card

  • KPIs for DSO and average billing lag, a lag bar by job, and AR aging by customer.

Common mistakes

  • Including retainage in DSO without saying so.
  • Using one month's billings in a lumpy business.
  • Ignoring unapplied cash.

Version 1.0 · Last reviewed 2026-10-20 · Machine-readable definition