Tracking Billable Hours as a Dev (Without Hating It)
By Mark Fulton · 2026-09-09 · 11 min read

The lightest system that actually works is a single plain-text file, one line per work block, written the moment you stop working: date, hours in quarter-hour decimals, client, category, description. That file takes about ten seconds a day to maintain, survives every tool migration you will ever do, and reconstructs cleanly because your git history is already a timestamped record of the same work. You do not need a screen recorder, an idle detector, or a keystroke counter. You need a convention you will keep on a bad Thursday.
Most advice on this topic is a list of apps. Apps are not the problem. The problem is that hourly work asks you to do two jobs at once: the work, and the narration of the work. Every timer-based tool assumes you will remember to press start while you are three levels deep in a stack trace. You will not. So the system has to be one you can reconstruct after the fact, from evidence you generate anyway.
Here is the whole thing.
What granularity do clients actually need?
Less than you think, and more specific than you are probably giving them.
There are three levels of granularity, and only one of them is right for most developer engagements:
| Level | What it looks like | When it works | What goes wrong |
|---|---|---|---|
| Weekly lump | "Development, week of Sep 8: 22 hrs" | Long-running trusted retainers | Reads like a shrug. First thing questioned in a dispute. |
| Per feature or per day | "Auth session handling, Sep 8: 4.25 hrs" | Almost every hourly engagement | Nothing. This is the target. |
| Per task, minute-level | "Sep 8, 09:14 to 09:31, read Stripe webhook docs: 0.28 hrs" | Legal, insurance, court-adjacent billing | Invites line-by-line negotiation on 17-minute entries. |
Per-feature granularity is the honest middle. A client can read "payment webhook retry logic, 3.5 hrs" and understand what they bought. They cannot audit it into oblivion, because the unit is a piece of working software, not a fragment of your afternoon.
The failure mode nobody warns you about is over-disclosure. When you hand a client minute-level entries, you have not proven your diligence. You have handed them a list of things to argue about, and one of them will always be the 40 minutes you spent reading documentation. Bill the outcome at a defensible grain, and keep the raw detail for yourself.
If you are still deciding whether hourly is even the right model for a given project, the trade-offs are worth settling before you build a tracking habit around the wrong one. Hourly versus fixed price for developers walks through who carries the risk in each.
What's the lightest system that works?
One file. hours.txt or hours.md, in a folder you already open every day. Five fields, whitespace separated, one line per work block.
2026-09-08 1.50 acme dev auth: fix redirect loop on expired session
2026-09-08 0.50 acme call sprint check-in with Dana
2026-09-08 0.25 acme review PR #212 second pass
2026-09-09 2.00 acme dev webhook retry with backoff, plus tests
2026-09-09 0.75 acme deploy staging cut, migration dry run
2026-09-09 1.25 northw dev CSV importer: handle BOM and CRLF
The convention, spelled out
Field 1: date, ISO 8601. YYYY-MM-DD, always. It sorts correctly as plain text, which is the entire reason the format exists. The timestamp format is specified in RFC 3339, the internet date and time standard, and it is the same shape git uses, which matters in a minute.
Field 2: hours, decimal, quarter-hour steps. 0.25, 0.50, 0.75, 1.00. Never 1:15, never 75m. Decimals multiply against a rate without a conversion step, which removes an entire class of invoicing error. Two decimal places always, so the column stays aligned and mistakes are visible.
Field 3: client slug. Short, lowercase, no spaces, stable forever. acme, not Acme Corp (new project). You will grep on this.
Field 4: category. One word from a fixed list. Five is enough:
| Category | Covers |
|---|---|
dev |
Writing, debugging, and testing code |
review |
Code review, PR feedback, pairing |
call |
Meetings, calls, scheduled sync |
ops |
Deploys, incidents, environment and infra work |
admin |
Scoping, estimates, written status updates |
Fixed vocabulary is the whole trick. The moment you allow free text here, you lose the ability to total the file by category, and a category total is what turns a log into an invoice.
Field 5: description. Written for the client, not for you. "auth: fix redirect loop on expired session" is a line item. "auth stuff" is not. Write it as though it will be read by someone paying for it, because it will be.
The three rules that keep it alive
- Write the line when you stop, not when you start. Starting a timer is a promise to your future self. Writing a line is a record of something that already happened. Only one of those survives a hard day.
- Never let the file go two days unwritten. One day of reconstruction is accurate. Three days is fiction.
- Log non-billable time too, in the same file. Mark it with a
-client slug or anoprefix. You cannot know your real effective rate until you can see the unbilled hours next to the billed ones, and that number is the one that tells you when to raise your rates.
That is the entire system. No account, no sync, no vendor. It is a text file, so it diffs, greps, versions, and outlives every tool.
How does your git history double as a timesheet?
Because it already is one. Every commit carries an author timestamp and a subject line you wrote at the moment of the work. That is a contemporaneous record, which is exactly what a reconstructed timesheet is not.
Pull a week for one repo:
git log --author="you@example.com" \
--since="2026-09-08" --until="2026-09-13" \
--pretty=format:"%as %h %s" \
--no-merges --reverse
Output:
2026-09-08 a3f91c2 fix auth redirect loop on expired session
2026-09-08 7bd0e14 add session expiry test coverage
2026-09-09 c81a55f add webhook retry with exponential backoff
2026-09-09 2e40b9a harden webhook signature verification
2026-09-09 91cc7de bump staging to 1.4.2
The placeholders come straight from the git pretty-formats documentation: %as is the author date in short YYYY-MM-DD form, %h is the abbreviated hash, %s is the subject. --no-merges drops merge noise, --reverse puts the week in reading order.
Want the day boundaries only, so you can see which days had work at all:
git log --author="you@example.com" --since="30 days ago" \
--pretty=format:"%as" --no-merges | sort -u
And across every repo in a folder, which is the version you will actually use:
for r in ~/code/*/; do
git -C "$r" log --author="you@example.com" --since="2026-09-08" \
--pretty=format:"%as $(basename $r) %s" --no-merges 2>/dev/null
done | sort
The honest caveat: commits are evidence of work, not a measure of hours. A four-hour debugging session can produce one commit with a three-word subject. A twenty-minute cleanup can produce six. Git tells you what happened and when. Your log file tells you how long. Use the history to reconstruct the shape of a week you forgot to log, then assign the hours yourself, from memory, honestly.
How do hours become invoice line items?
By grouping. Take the log lines for the billing period, group by day or by feature, sum the hours, and write one description per group.
Working from the log above, plus the matching commits:
| Date | Category | Line item description | Hrs | Rate | Amount |
|---|---|---|---|---|---|
| Sep 8 | dev | Session auth: fix expired-session redirect loop, add test coverage | 1.50 | $110 | $165.00 |
| Sep 8 | call | Sprint check-in | 0.50 | $110 | $55.00 |
| Sep 8 | review | PR #212 review pass | 0.25 | $110 | $27.50 |
| Sep 9 | dev | Payment webhooks: retry with exponential backoff, signature hardening, tests | 2.00 | $110 | $220.00 |
| Sep 9 | ops | Staging release 1.4.2, migration dry run | 0.75 | $110 | $82.50 |
| Total | 5.00 | $550.00 |
Three things that make this pass a client's read:
- Every line names a thing, not an activity. "Payment webhooks: retry with exponential backoff" is a deliverable. "Backend work" is a bill for existing.
- Related commits collapse into one line. Four commits on the same feature in one day is one line item, not four. You are billing the feature, not the pushes.
- Grouping matches the log, so it is defensible. If the client ever asks, the raw lines exist and the commit hashes back them.
That grouping step is the one part worth automating. Paste git log output into Billable's git-log-to-invoice tool and it drafts the line items, grouped and dated, ready to edit. It runs entirely in your browser, so nothing about your client's repo leaves your machine. The longer version of the reasoning lives in turn your git log into an invoice.
Keep the log file after you invoice. The IRS guidance on what kind of records you should keep treats invoices and receipts as supporting documents for gross receipts, and a contemporaneous hours file is the thing that makes an invoice supportable years later. This is general information, not tax or legal advice.
What about non-coding time, calls, review, and deploys?
It is billable, and the reason developers under-bill is that they only feel entitled to charge for typing.
Some concrete positions worth holding:
- Calls and meetings are billable. Log them the same day, at the same rate, unless your agreement says otherwise. A standing weekly call is real capacity you cannot sell twice.
- Code review is billable. Reviewing someone else's PR on a client codebase is client work, and it is often the highest-value hour of your week.
- Deploys, incidents and on-call are billable. A 20-minute production fix at 11pm is 20 minutes of hourly work. Log it and bill it.
- Reading the client's existing code is billable. Onboarding to an unfamiliar repo is work the client is buying.
- Learning a general skill is not billable. If the knowledge follows you to the next client, it is your investment, not theirs.
- Rework caused by your own bug is not billable. Fixing your own defect inside the engagement is warranty. Fixing a bug that predates you is billable work.
- Scope negotiation sits on the line. A short estimate is a cost of sale. A day rebuilding a spec after a change of direction is billable, and it should go through a change order rather than quietly onto a timesheet. See scope creep and change orders for how to handle that conversation.
State these positions in the agreement, once, in a sentence. "Billable time includes development, code review, deploys, and scheduled calls." Then log against them without renegotiating with yourself every week.
When you are ready to send, the hours drop straight into a line-item invoice. Billable is free, runs in your browser, and never sends your data anywhere: start from the free invoice template for developers or the hourly invoice template.
FAQ
Do I bill for reading docs and debugging?
Debugging, yes, without hesitation. Debugging is the work. Nobody bills only for the line that ends up in the merge commit.
Reading docs splits by specificity. Reading the client's API documentation, their internal wiki, or the docs for a library they chose is billable, because it is required to do their job and it does not transfer. Reading a general tutorial on a framework you are learning for your own benefit is not billable, even if you happen to be learning it on their project. The test: if the client hired a different developer tomorrow, would that person have to spend the same time? If yes, bill it.
Should I share raw time logs with clients?
No, not by default. Share the grouped line items, keep the raw file.
Raw logs invite line-by-line negotiation, and the entries that draw fire are always the small honest ones. Your invoice should be specific enough to answer "what did I buy" and not so granular that it answers "what were you doing at 2:40pm."
The exception is when a client asks directly, or when your agreement requires detail-level reporting, which is common in agency subcontracting. In that case, hand over a clean export grouped by day and category, not the working file with your shorthand in it. That is also the moment the fixed category vocabulary pays for itself.
How do I round hours honestly?
Round to the nearest quarter hour, in both directions.
Rounding always up is the practice that damages trust, because clients notice when every entry lands on a quarter and none ever rounds down. A 10-minute fix is 0.25. A 20-minute fix is 0.25. A 25-minute fix is 0.50. Over a month it evens out, and it evens out visibly.
Two rules keep it clean. Round each work block once, never the daily total, and never the invoice total. And set a floor for interruptions: a 15-minute minimum on any client-initiated interruption is reasonable, because a two-minute question costs more than two minutes of context. Put the floor in the agreement rather than discovering it on an invoice.
What if I forgot to track a day?
Reconstruct it from evidence, the same day you notice, and mark it.
Your sources, in order of reliability: git history for the repos you touched, your calendar for calls, sent messages and PR comments with timestamps, and your shell history if you keep it. Run the git log command above for the missing date, look at what shipped, and assign hours from memory against that shape.
Then be conservative. When you genuinely cannot tell whether a block was 2 or 3 hours, log 2. The occasional under-bill costs you money once. An over-bill that a client later questions costs you the relationship and every invoice after it.
If it happens more than once a month, the system is too heavy. Drop back to the text file. A log you keep imperfectly beats a tool you keep perfectly for nine days.