Cost Insights brings cloud cost data into Backstage. It helps engineers see what they spend, why it changed, and where to act. You get daily cost views by team or catalog entity. You can compare periods, track trends, and relate spend to the business metrics you care about. It keeps the conversation in the developer portal instead of another dashboard.
The plugin highlights growth that matters. It can flag project cost spikes, or unlabeled spend that needs fixing. It breaks costs down by product or resource so you can drill into the services that drive the bill. It can express growth as an estimate of engineer time to make trade offs clear. Multiple currencies and a base currency are supported, so teams can speak in terms that fit the org.
This fits common use cases. Troubleshoot a sudden jump. Track the impact of a migration. Guide budget talks with clear numbers. Keep a steady watch without asking engineers to change tools.

Installation Instructions
These instructions apply to self-hosted Backstage only.
Install the frontend package
# run from your Backstage repo root
yarn --cwd packages/app add @backstage-community/plugin-cost-insights
Add required config
Add the required engineer cost. You can add optional fields later.
# app-config.yaml
costInsights:
engineerCost: 200000
Optional examples
# app-config.yaml
costInsights:
engineerCost: 200000
products:
productA:
name: Some Cloud Product
icon: storage
productB:
name: Some Other Cloud Product
icon: data
metrics:
metricA:
name: Metric A
default: true
metricB:
name: Metric B
baseCurrency:
locale: nl-NL
options:
currency: EUR
minimumFractionDigits: 3
currencies:
engineers:
label: Engineers
unit: engineers
usd:
label: USD
kind: USD
unit: dollars
prefix: $
rate: 1
engineerThreshold: 0.5
Implement a Cost Insights client
You need a client that implements the CostInsightsApi. Start simple. You can replace the fetchers with real calls later. Keep this in the app package.
// packages/app/src/costInsights/CostInsightsClient.ts
import {
CostInsightsApi,
costInsightsApiRef,
Duration,
ExampleCostInsightsClient,
Cost,
MetricData,
Entity,
Project,
Group,
} from '@backstage-community/plugin-cost-insights';
export class CostInsightsClient implements CostInsightsApi {
// start with the example client to get the UI working
private readonly demo = new ExampleCostInsightsClient();
async getLastCompleteBillingDate(): Promise<string> {
return this.demo.getLastCompleteBillingDate();
}
async getUserGroups(userId: string): Promise<Group[]> {
return this.demo.getUserGroups(userId);
}
async getGroupProjects(group: string): Promise<Project[]> {
return this.demo.getGroupProjects(group);
}
async getGroupDailyCost(group: string, intervals: string): Promise<Cost> {
return this.demo.getGroupDailyCost(group, intervals);
}
async getProjectDailyCost(project: string, intervals: string): Promise<Cost> {
return this.demo.getProjectDailyCost(project, intervals);
}
async getDailyMetricData(metric: string, intervals: string): Promise<MetricData> {
return this.demo.getDailyMetricData(metric, intervals);
}
async getProductInsights(options: {
product: string;
group: string;
intervals: string;
project: string | null;
}): Promise<Entity> {
return this.demo.getProductInsights(options);
}
async getCatalogEntityDailyCost(entityRef: string, intervals: string): Promise<Cost> {
return this.demo.getCatalogEntityDailyCost(entityRef, intervals);
}
async getAlerts(group: string) {
return this.demo.getAlerts(group);
}
}
You can keep the example client while you build your backend. Replace the demo calls with your fetch code when ready.
Wire the API into the app
Register your client in the app api registry.
// packages/app/src/api.ts
import { createApiFactory } from '@backstage/core-plugin-api';
import { costInsightsApiRef } from '@backstage-community/plugin-cost-insights';
import { CostInsightsClient } from './costInsights/CostInsightsClient';
export const apis = [
createApiFactory({
api: costInsightsApiRef,
deps: {},
factory: () => new CostInsightsClient(),
}),
];
Add the Cost Insights page and route
// packages/app/src/App.tsx
import React from 'react';
import { FlatRoutes } from '@backstage/core-app-api';
import { Route } from 'react-router';
import { CostInsightsPage } from '@backstage-community/plugin-cost-insights';
export const AppRoutes = () => (
<FlatRoutes>
{/* other routes */}
<Route path="/cost-insights" element={<CostInsightsPage />} />
</FlatRoutes>
);
Add a sidebar entry
// packages/app/src/components/Root/Root.tsx
import React, { PropsWithChildren } from 'react';
import MonetizationOn from '@material-ui/icons/MonetizationOn';
import {
SidebarPage,
Sidebar,
SidebarItem,
SidebarLogo,
SidebarGroup,
SidebarDivider,
SidebarScrollWrapper,
SidebarSpace,
SidebarSettings,
} from '@backstage/core-components';
import { UserSettingsSignInAvatar } from '@backstage/plugin-user-settings';
export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarPage>
<Sidebar>
<SidebarLogo />
{/* your existing groups */}
<SidebarDivider />
<SidebarScrollWrapper>
<SidebarItem icon={MonetizationOn} to="cost-insights" text="Cost Insights" />
</SidebarScrollWrapper>
<SidebarDivider />
<SidebarGroup label="Settings" icon={<UserSettingsSignInAvatar />} to="/settings">
<SidebarSettings />
</SidebarGroup>
<SidebarSpace />
</Sidebar>
{children}
</SidebarPage>
);
Show costs on entity pages
You can show per entity spend in your catalog pages.
// packages/app/src/components/catalog/EntityPage.tsx
import React from 'react';
import { Grid } from '@material-ui/core';
import { EntitySwitch, isKind } from '@backstage/plugin-catalog';
import { EntityCostInsightsContent } from '@backstage-community/plugin-cost-insights';
export const OverviewContent = () => (
<Grid container spacing={3}>
{/* other cards */}
<Grid item xs={12}>
<EntitySwitch>
<EntitySwitch.Case if={isKind('Component')}>
<EntityCostInsightsContent />
</EntitySwitch.Case>
</EntitySwitch>
</Grid>
</Grid>
);
New frontend system setup
If your app uses the new frontend system, use the alpha export. Keep your client in the api registry the same way.
// packages/app/src/App.tsx
import React from 'react';
import { createApp } from '@backstage/frontend-app-api';
import { createApiFactory } from '@backstage/core-plugin-api';
import costInsights from '@backstage-community/plugin-cost-insights/alpha';
import { costInsightsApiRef } from '@backstage-community/plugin-cost-insights';
import MonetizationOn from '@material-ui/icons/MonetizationOn';
import { CostInsightsClient } from './costInsights/CostInsightsClient';
const app = createApp({
features: [
costInsights.provide('page:cost-insights', { path: '/cost-insights' }),
costInsights.provide('nav-item:cost-insights', {
title: 'Cost Insights',
icon: MonetizationOn,
routeRef: costInsights.routes.root,
}),
costInsights.provide(
'api:cost-insights',
createApiFactory({
api: costInsightsApiRef,
deps: {},
factory: () => new CostInsightsClient(),
}),
),
],
});
export default app.createRoot();
Provide a backend with the new backend system
The plugin does not ship a backend. You can add a small backend module that exposes the cost data your client needs. This keeps provider credentials off the browser.
Install any SDKs you plan to call from the backend. Here is an example for the AWS Cost Explorer client.
yarn --cwd packages/backend add @aws-sdk/client-cost-explorer
Create a backend module that mounts a router.
// packages/backend/src/plugins/costInsights.ts
import { Router } from 'express';
import { CostExplorerClient, GetCostAndUsageCommand } from '@aws-sdk/client-cost-explorer';
export async function createCostInsightsRouter(): Promise<Router> {
const router = Router();
const ce = new CostExplorerClient({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? '',
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? '',
},
});
router.get('/group-daily-cost', async (req, res) => {
const { accountId, start, end } = req.query as Record<string, string>;
const cmd = new GetCostAndUsageCommand({
TimePeriod: { Start: start, End: end },
Metrics: ['UnblendedCost'],
Filter: { Dimensions: { Key: 'LINKED_ACCOUNT', Values: [accountId] } },
Granularity: 'DAILY',
});
const data = await ce.send(cmd);
// map AWS response to the Cost type expected by the frontend
const aggregation =
data.ResultsByTime?.map(d => ({
date: d.TimePeriod?.Start ?? '',
amount: Number(d.Total?.UnblendedCost?.Amount ?? 0),
})) ?? [];
res.json({ aggregation });
});
return router;
}
Register the router with the new backend system entry point.
// packages/backend/src/index.ts
import { createBackend } from '@backstage/backend-defaults';
import { createCostInsightsRouter } from './plugins/costInsights';
const backend = createBackend();
backend.addRouter('/api/cost-insights', await createCostInsightsRouter());
backend.start();
Point your frontend client to this backend route. Replace the demo calls.
// packages/app/src/costInsights/CostInsightsClient.ts
export class CostInsightsClient implements CostInsightsApi {
async getGroupDailyCost(group: string, intervals: string): Promise<Cost> {
// pick your own mapping from group to account id
const accountId = group;
const [start, end] = intervals.split('/');
const res = await fetch(
`/api/cost-insights/group-daily-cost?accountId=${encodeURIComponent(
accountId,
)}&start=${encodeURIComponent(start)}&end=${encodeURIComponent(end)}`,
);
if (!res.ok) throw new Error(`Failed to fetch group daily cost`);
return await res.json();
}
// implement the other methods the same way
}
Provide a backend with the classic backend
Create a router and mount it under the api path.
// packages/backend/src/plugins/costInsights.ts
import { Router } from 'express';
export default async function createPlugin(): Promise<Router> {
const router = Router();
router.get('/ping', async (_req, res) => {
res.json({ ok: true });
});
// add your endpoints here
// for example /group-daily-cost like in the new backend example above
return router;
}
Mount it in the backend index.
// packages/backend/src/index.ts
import costInsights from './plugins/costInsights';
// inside main bootstrap
const apiRouter = Router();
apiRouter.use('/cost-insights', await costInsights());
app.use('/api', apiRouter);
Update your frontend client to call /api/cost-insights/...
like in the new backend example.
Build real data into the client over time
You can implement the CostInsightsApi methods against any provider or internal billing store.
If you use AWS, the AWS SDK client can answer the methods your frontend expects. The Cost Explorer GetCostAndUsage command gives you daily totals and grouped totals. Cache where possible to keep requests low.
// example snippet inside your backend
const cmd = new GetCostAndUsageCommand({
TimePeriod: { Start: '2021-01-01', End: '2021-02-01' },
Metrics: ['UnblendedCost'],
Granularity: 'DAILY',
GroupBy: [{ Type: 'DIMENSION', Key: 'SERVICE' }],
});
const data = await ce.send(cmd);
Tips while you wire this up
- Start with ExampleCostInsightsClient so your page renders
- Replace methods one by one with calls to your backend
- Keep
engineerCost
in config up to date so the UI can compute changes - Add products and metrics when you have data to back them
What the plugin exports that you can use
Import these from the app where you need them.
// page
import { CostInsightsPage } from '@backstage-community/plugin-cost-insights';
// api ref and types
import {
costInsightsApiRef,
CostInsightsApi,
Duration,
EntityCostInsightsContent,
} from '@backstage-community/plugin-cost-insights';
// new frontend system
import costInsights from '@backstage-community/plugin-cost-insights/alpha';
Place CostInsightsPage in your top level routes so everyone can reach it. Place EntityCostInsightsContent in your entity pages so teams can see cost near their components.
Things to Know
To learn more about the Cost Insights plugin and how it is used inside Spotify, check out this RedMonk interview with Cost Insights product manager Janisa Anandamohan and her engineering colleague Tim Hansen. We also have brief notes from the video in this edition of our newsletter.
Changelog
This changelog is produced from commits made to the Cost Insights plugin since a year ago, and based on the code located here. It may not contain information about all commits. Releases and version bumps are intentionally omitted. This changelog is generated by AI.
Breaking changes
- Remove alpha New Frontend System pages CostInsightsProjectGrowthInstructionsPage and CostInsightsLabelDataflowInstructionsPage. Refactor new frontend system support to match the current flow. #4839 Merged 1 month ago
Documentation
- Update README links to point to the community plugins repository. #3931 Merged 5 months ago
Maintenance
Set up Backstage in minutes with Roadie
Focus on using Backstage, rather than building and maintaining it.