IT services bench ageing and shadow allocation: where margin leaks
In this article (6 sections)
Related programme: the Data Analytics with Generative AI course (3–4 months; ₹90,000 (inclusive of GST) · EMI from ₹4,000/month).
Bench ageing in an IT services firm is the number of days each unallocated person has gone since their last billable assignment ended, or since joining if they have never been placed. It belongs beside two other numbers from the same timesheets: billable utilisation on a stated capacity denominator, and the allocation rate, whose gap from utilisation measures shadow work, meaning client work nobody is billed for. Bench and shadow time together are where services margin leaks.
One team, three different utilisation numbers
A delivery head at a mid-sized services company in Noida or Pune gets a utilisation figure every Monday. It rarely says which of these it is:
| Measure | Numerator | Denominator | What it tells you |
|---|---|---|---|
| Utilisation on standard hours | billable hours | working days × 8 × headcount | revenue capacity used, leave counted as waste |
| Utilisation on available hours | billable hours | standard hours − leave | how well present people are deployed |
| Allocation rate | billable + non-billable project hours | available hours | how many are on some project, paid or not |
The gap between the second and third is where margin leaks: people working on client projects without anyone paying for their time.
Source extracts
| Extract | Columns | Watch for |
|---|---|---|
| timesheets | emp_id, week_start, project_code, task_type, hours, approval_status | unapproved rows; bench staff often log nothing |
| allocations | emp_id, project_code, start_date, end_date, allocation_pct, billable_flag | overlapping allocations above 100% |
| leave_calendar | emp_id, date, leave_type, half_day | public holidays differ by location |
| employee_master | emp_id, grade, practice, location, join_date, exit_date | notice-period staff still in headcount |
| project_master | project_code, contract_type, client, billing_model | internal codes that look like client codes |
September for eight engineers, in pandas
A synthetic month: 22 working days, so 176 standard hours each. A5 is a team lead whose 60 hours of client work are coded non-billable; A4 was released from a project in June; A6 joined in August and has not been placed.
import pandas as pd
# Synthetic: 8 engineers, September 2026 timesheets (hours) and allocation history
ts = pd.DataFrame({
"emp_id": ["A1","A2","A3","A4","A5","A6","A7","A8"],
"grade": ["SE","SE","SSE","SSE","TL","SE","SSE","TL"],
"billable": [168, 152, 120, 0, 84, 0, 160, 144],
"non_billable_project": [0, 0, 24, 0, 60, 0, 8, 0], # shadow / unfunded work
"internal": [0, 8, 16, 24, 8, 40, 0, 8], # training, pre-sales, bench tasks
"leave": [0, 8, 8, 16, 16, 0, 8, 16],
})
STANDARD = 176 # 22 working days x 8 hours in this synthetic month
ts["available"] = STANDARD - ts["leave"]
ts["logged"] = ts[["billable","non_billable_project","internal"]].sum(axis=1)
team = ts[["billable","non_billable_project","internal","leave","available"]].sum()
print("billable / standard hours (%):", round(100 * team.billable / (STANDARD * len(ts)), 1))
print("billable / available hours (%):", round(100 * team.billable / team.available, 1))
print("allocated / available hours (%):",
round(100 * (team.billable + team.non_billable_project) / team.available, 1))
print("unlogged hours:", int(team.available - ts["logged"].sum()))
# Bench: no project hours in the month. Ageing from last billable allocation end date.
last_billable_end = {"A4": "2026-06-12"} # A6 has never been allocated
join_date = {"A4": "2023-07-03", "A6": "2026-08-17"}
asof = pd.Timestamp("2026-09-30")
bench = ts[(ts["billable"] + ts["non_billable_project"]) == 0][["emp_id","grade"]].set_index("emp_id")
bench["bench_since"] = [pd.Timestamp(last_billable_end.get(e, join_date[e])) for e in bench.index]
bench["bench_days"] = (asof - bench["bench_since"]).dt.days
bench["reason"] = ["released from project", "new joiner, never allocated"]
print(bench.to_string())
ts["util_pct"] = (100 * ts["billable"] / ts["available"]).round(1)
print(ts[["emp_id","grade","billable","available","util_pct"]].to_string(index=False))billable / standard hours (%): 58.8
billable / available hours (%): 62.0
allocated / available hours (%): 68.9
unlogged hours: 312
grade bench_since bench_days reason
emp_id
A4 SSE 2026-06-12 110 released from project
A6 SE 2026-08-17 44 new joiner, never allocated
emp_id grade billable available util_pct
A1 SE 168 176 95.5
A2 SE 152 168 90.5
A3 SSE 120 168 71.4
A4 SSE 0 160 0.0
A5 TL 84 160 52.5
A6 SE 0 176 0.0
A7 SSE 160 168 95.2
A8 TL 144 160 90.0What the numbers are saying
The headline depends on the denominator. The same month is 58.8% on standard hours and 62.0% on available hours. Neither is wrong; a report that switches between them from month to month is.
Seven points of allocated-but-unbilled time. Allocation is 68.9% against billable utilisation of 62.0%. Most of that gap is A5, a team lead doing 60 hours of client work under a non-billable code. That may be an agreed investment in a new account, or it may be a lead covering an unfilled position the client has not been asked to pay for. The analyst cannot tell from the data; the account manager can.
312 hours were never logged. Most of it belongs to the two bench engineers, who logged 24 and 40 hours. Bench time is often simply not recorded, so a report computing utilisation from logged hours rather than available hours would look healthier than it is.
Two very different bench cases. A4 is a senior engineer 110 days past a project release; A6 is a new joiner 44 days in. The first is a redeployment or skills problem, the second an onboarding pipeline question. One bench-count number hides the difference.
For the monthly timesheet mechanics behind the first two rows, including missing days and leave calendars, see calculating billable utilisation from a timesheet.
Mistakes that recur in services data
- Overlapping allocations. Someone at 100% on one project and 50% on another is either overworked or the allocation system is stale. Sum allocation_pct per person per day and flag anything above 100.
- Notice-period staff counted as bench. A resigned employee serving notice will not be deployed. Report them separately, or bench ageing rises every time someone resigns.
- Location-specific holidays. Standard hours for a team split across Noida and Bengaluru differ in any month with a state holiday. Build capacity from each location's calendar.
- Fixed-price projects. Hours on a fixed-price contract are coded billable but do not map to revenue hour for hour. Keep contract_type in the output.
- Unapproved timesheets. Pulling the report before the approval cut-off undercounts the last week. Freeze the extract date and print it.
The delivery head's one page
Three lines at the top, with denominators printed: billable utilisation on available hours, allocation rate, and the gap between them in hours. Then the bench as a short list with grade, days on bench and reason bucket, sorted by days. Individual utilisation belongs in the resource manager's view, not the leadership page.
Headcount time series like this connect naturally to exits; the attrition by tenure playbook uses the same person-period thinking. Joining timesheets, allocations and calendars in pandas is the day-to-day work the Data Analyst programme prepares you for.
Frequently asked questions
What is a shadow resource in IT services?
A person working on a client project whose hours are not billed, often coded as non-billable project time. Some shadowing is planned, such as a knowledge transfer or an account investment. Unplanned shadowing is lost revenue and should be visible as the gap between allocation rate and billable utilisation.
What is bench ageing?
Bench ageing is the number of days an employee has gone without a project allocation, counted from the end of their last billable allocation or, for someone never allocated, from their joining date. It is usually shown in buckets such as under 30, 30 to 60 and over 60 days.
Why do utilisation reports and finance revenue disagree?
Timesheet billable hours are not the same as invoiced hours. Fixed-price work, capped time-and-material contracts and unapproved timesheets all create gaps. Reconcile a sample of projects to invoices before presenting utilisation as a revenue driver.
Pankit Kumar has 10 years in Data Science & AI, building and shipping production systems in regulated pharma and clinical environments. He is a freelance trainer at Boston Institute of Analytics, AnalytixLabs and Scaler, and has taught this material to thousands of working professionals.
This article is part of our Data Analytics with Generative AI programme — 3–4 months. The full analyst stack — Excel, SQL, Power BI and Python pipelines — then a generative-AI layer you can prove is right.
Explore Data Analytics with Generative AI