Data AnalyticsIndustry analytics playbooks

IT services bench ageing and shadow allocation: where margin leaks

PK
Pankit Kumar
Sr. Data Scientist at Parexel (a Goldman Sachs–backed company) · 27 September 2026 · 5 min read
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).

The short answer

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:

MeasureNumeratorDenominatorWhat it tells you
Utilisation on standard hoursbillable hoursworking days × 8 × headcountrevenue capacity used, leave counted as waste
Utilisation on available hoursbillable hoursstandard hours − leavehow well present people are deployed
Allocation ratebillable + non-billable project hoursavailable hourshow 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

ExtractColumnsWatch for
timesheetsemp_id, week_start, project_code, task_type, hours, approval_statusunapproved rows; bench staff often log nothing
allocationsemp_id, project_code, start_date, end_date, allocation_pct, billable_flagoverlapping allocations above 100%
leave_calendaremp_id, date, leave_type, half_daypublic holidays differ by location
employee_masteremp_id, grade, practice, location, join_date, exit_datenotice-period staff still in headcount
project_masterproject_code, contract_type, client, billing_modelinternal 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.

python
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))
text
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.0

What 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.

PK
Pankit Kumar
Lead Instructor, NeuraPath Academy

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
Counselling is free · no obligation

Not sure which programme fits?

Tell us your background and we will map it to the right entry point — including saying so when a cheaper programme is the better fit. A counsellor replies within one working day.