Guide
Find out where your Laravel Cloud bill is actually going
· Ed Grosvenor
Last updated
Laravel Cloud gives you one total, but that total is not the answer you usually need. You need to know which app is responsible, which resources are still useful, and whether something you deleted months ago is still billing you today.
You can get there. The important part is being clear about what Cloud links for you and what you have to infer.
Start with the billing-period usage
Export the previous billing period first:
cloud usage --json --period=previous > cloud-usage.json
Use previous when you are reconciling an invoice. current is still changing, and the figures can lag by 30 to 60 minutes. Cloud exposes four billing periods, based on your billing cycle rather than calendar months. It does not expose per-day or per-hour cost. The environment metrics endpoint has shorter CPU and memory series, but those are utilization numbers, not money.
All money in the response is integer cents. Read totalCents or totalCostCents rather than trying to rebuild a total from every subtype. Database fields such as storageCents and computeCents can be null while totalCents is correct.
The usage response is also not an inventory. An app deleted halfway through the period can still appear in usage but be absent from application:list, while a current zero-spend app can be absent from usage.
If you need instance-level compute for one suspicious environment, ask for it explicitly:
cloud usage --json --period=previous --environment=<environment-id> > environment-usage.json
Without --environment, environmentUsageItems is empty. Fetching it for every environment means one extra API call per environment, so investigate the app you care about instead of hammering the whole organization.
Prove that the totals reconcile
Before attributing anything, check the arithmetic in cloud-usage.json:
- Add every application's
totalCostCents. It must equalapplicationsTotalCostCents. - Add every database, cache, bucket, and WebSocket
totalCents. It must equalresourcesTotalCostCents. - Add applications, resources, add-ons, and bandwidth. It must equal
currentSpendCents. - For an individual app, add the compute totals of its environments. It must equal that application's compute total.
This is the trust check. Stop if the numbers do not reconcile. Do not build a tidy attribution report on top of an incomplete export or a changed billing period.
Bandwidth remains one organization-level figure against an allowance. Leave it unattributed rather than inventing a split.
Label linked cost differently from inferred cost
Application compute carries a real app- identifier. Join that identifier to the output of:
cloud application:list --json > cloud-applications.json
That cost is linked.
Attached resources are different. Databases, caches, buckets, and WebSockets do not provide a dependable application or environment key in either direction. Relation arrays can be empty even when a resource is in use. On one 22-app organization, compute was about 37% of the bill and resources were about 63%, so pretending the whole bill is linked would make most of the report false.
Give every line one of four labels:
linked: Cloud supplies the relationship, as it does for compute.inferred: a resource name matches an app by a repeatable rule.override: you pinned a known exception by hand.unattributed: no unique owner can be supported.
An explicit unattributed row is more useful than a confident guess.
Match resource names, then keep the exceptions
Cloud commonly names an auto-provisioned resource after its app, sometimes with the environment appended. Use that convention as evidence, not as a hidden assumption:
- Lowercase names and remove non-alphanumeric characters.
example-shop.comandexample_shopboth becomeexampleshop. - Index each app's slug and name, both intact and with a known top-level domain removed.
- Add variants with every environment name appended, such as
exampleshopproduction. - Try an exact match first, then the longest key that the resource name starts with.
- Drop keys shorter than four characters because they collide too easily.
- If one key matches more than one app, mark the resource unattributed and report the candidates.
Keep the exceptions in a small JSON file that can be reviewed with the report:
{
"legacy-primary": "example-shop"
}
Pass that same map into every run. Do not bury one-off decisions in code or make somebody rediscover them next month.
This method moved one real organization's resource attribution from 83.6% to 97.9%. The remaining rows were not failures of the report. They were the useful part of it.
Read the residue
A paid resource that matches no app is a finding. Nobody can name its owner, so nobody is likely to be watching it.
A zero-cost unmatched resource is still worth checking. It may be harmless clutter today and a charge tomorrow. Confirm what it is, record the decision, and remove it only when you can prove it has no owner.
Do not paste raw cache listings into an issue or chat. cloud cache:list --json includes live passwords inside its connection block. Extract only the identifiers, names, and state you need.
Also slow down enumeration loops. The CLI can rate-limit repeated per-cluster or per-bucket calls. A rate-limited check is inconclusive, not a pass.
Audit children against their parents
The dependable orphan audit runs from each environment to its children:
cloud application:list --json
cloud environment:get <environment-id> --json
cloud database-cluster:list --json
cloud cache:list --json
cloud websocket-cluster:list --json
cloud websocket-application:list <cluster-id> --json
cloud bucket:list --json
cloud bucket-key:list <bucket-id> --json
cloud instance:list <environment-id> --json
Fetch every environment individually and collect its databaseSchemaId, cacheId, and websocketApplicationId. That set is your reference graph. There is no reliable bulk shortcut.
Then enumerate every resource type. Anything outside the reference graph is a candidate, not yet an orphan. Fetch the candidate itself and inspect the environment that ought to own it before deciding. An empty relation array is never proof that a resource is unused.
Pay special attention to these cases:
- Database clusters and schemas are not the same thing. A cluster is orphaned only when every schema inside it is unreferenced. A used schema and an abandoned schema often share one cluster. Delete the unused schema, not the cluster.
- Buckets cannot be proved from Cloud's relationships. They have no environment field, and Cloud injects default-disk configuration outside the environment variables you can inspect. Classify them as
unproven. A bucket named after a deleted app is a probable orphan, but inspect its contents before deleting it. Bucket deletion is not recoverable. - Look for hollow environments. An environment can be running and billing compute while having no database, no cache, and no meaningful deployment.
- Inspect managed queues. Queue creation defaults
minReplicasto1, which keeps a worker alive permanently. Check whether each queue should scale to zero. Read a queue back after creating it too; options can be dropped rather than rejected.
Deleting an app does not guarantee that Cloud deletes what the app provisioned. Treat teardown as its own operation.
Make deletion boring
Build a manifest before deleting anything. Store the full identifier, resource type, proposed owner, evidence, and the number of resources you expect to remove.
Then follow these rules:
- Compare full identifiers. A fictional WebSocket cluster
ws-00000000-1111-2222-3333-444444444444and its applicationwsa-00000000-1111-2222-3333-444444444444share a UUID stem. Deleting the cluster deletes every application in it. - Prove deletion by absence from a fresh list. A
getagainst a deleted WebSocket application can return an unrelated "multiple clusters found" error rather than not-found. Absence is the evidence; an error is not. - Never test a write with a made-up environment identifier. The CLI can resolve an unknown identifier to the default environment and perform the write there.
- Record the manifest count separately from its list. Stop if the count and list length disagree.
- Re-enumerate immediately before deletion. A new app or attachment can invalidate yesterday's decision.
- After deletion, verify that every environment is still running and every expected resource reference remains intact.
Holding an uncertain candidate costs nothing beyond its ordinary bill. Deleting the wrong parent can cost the data beneath it. Make uncertainty stop the run.
If you want the attribution algorithm and report format ready-made, use the public laravel-cloud-cost skill.
Take it further
Cost data can tell you that a serverless database never slept. It cannot tell you what kept waking it up.
Hone records requests, queries, jobs, scheduled tasks, outgoing HTTP calls, and cache activity in daily aggregates. Use it to identify the actual activity behind a flat compute line: a scheduler running every minute, a health check that queries the database, a persistent job loop, or requests arriving more frequently than the scale-to-zero delay.
That evidence lets you fix the cause instead of merely resizing the resource. Remember the trade-off: when an app really does hibernate, its first request after a quiet period can take 10 to 20 seconds and may return a 502 while replicas wake. Sleeping saves money by accepting a cold start; it is not a broken deployment.
Changelog
- Published.