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.
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.
| Job | Customer | Period | Invoiced | Lag | Open AR, ex retainage |
|---|---|---|---|---|---|
| 2420 | Pine Medical | Sep | Oct 14 | 14 days | $34,500 |
| 2431 | County | Sep | Oct 3 | 3 days | $80,000 |
| 2417 | Main St LLC | Sep | Oct 6 | 6 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
- Construction ERP and accounting holds job cost, budgets, billing, AP, and AR. For example: Sage 300 Construction and Real Estate, Viewpoint Vista (Trimble), and Foundation. Some smaller contractors use general accounting software such as QuickBooks with jobs set up as projects.
- Project management often holds change orders, commitments, and project documents. For example: Procore.
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.
Related metrics
- Retainage outstanding: the part excluded from DSO.
- Cash flow by job: the result.
- Change order cycle time: unapproved changes cannot be billed.
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.