Since starting Phasio, I’ve been asked numerous times about how to price CNC machined parts. In this post, I’m going to work through a simple demonstration of a pricing equation that can be used to quote CNC machined parts, and we’ll close with a complete working example that you can use as a starting point for your own pricing calculations.
Some caveats
I want to start with a point which we stress a lot at Phasio; there is no right price for a CNC machined part. The price that is right is a price you are comfortable with as a manufacturer, and that your customer is willing to pay. If you are stressing about the price after sending out the quote, you may well have been lacking an understanding of just how complex the part is.
The point of these pricing equations and, in fact, the entire Phasio platform, is to establish a baseline for pricing where you can be comfortable delegating pricing to your business development team and setting clear boundaries on where they need to seek advice from you or an engineer.
This all to say, the point is to give you a methodology that you can build on. It doesn’t need to be perfect 100% of the time, it just needs to be good enough to quote those quick, fast-turnaround parts that you would otherwise be wasting hours on, or simply no-quoting.
The methodology
So imagine a customer gives you a part and asks for a quote. Without any context on what this part is, or it’s use-case, you can only base your quote off of the geometry of the part itself. Our pricing equation uses the geometry of the part to infer this price.
The raw stock
To begin with, you have your raw stock. We need to map out which raw stock are available to us before we begin quoting, as that’s going to tell us just how much material we need to remove. In our example, we’ll work with just a small number of blocks. You can of course add as many blocks as you’d like into your equation:
const blockSizes: [number, number, number][] = [
[25, 25, 25],
[50, 50, 50],
[75, 75, 75],
[100, 100, 100],
[150, 125, 125],
[150, 150, 150],
[200, 200, 200],
]
Okay so we have a list of blocks - that’s great. But which one will we be using?
The part itself
To infer which block we’ll be using, we should probably start by making sure our part is small enough to fit into it. Obviously we can’t manufacture a 70x35x35mm part using a 25x25x25mm block. So then, the rule of thumb should be this:
We filter to remove blocks that are too small, then choose the minimum block that can fit our part
So if our part is 70x35x35mm, what would the block choice be?
const blockSizes: [number, number, number][] = [
[25, 25, 25], // too small
[50, 50, 50], // too small
[75, 75, 75], // fits <==== this one is the smallest of the filtered blocks, so let's use that
[100, 100, 100], // fits
[150, 125, 125], // fits
[150, 150, 150], // fits
[200, 200, 200], // fits
]
Great! So now we have our block selected, we know it fits the part and it’s going to be the 75x75x75mm one. But how does this get us to a price?
Milling speed
We’re going to start by simulating a “coarse milling” around the part. In computer geometry, there’s a useful concept called the ‘convex hull’. Without labouring the point too much, the convex hull is essentially the exterior cutout of a 3D model, ignoring any interior features. It’s useful to us in this case because, as a machinist, you can think of the convex hull as being the “coarse outline” of your part.
For this coarse outline, we can probably use a higher milling speed as we are expecting to do a finer run afterward anyway.
As a result, you’re likely to have a fairly high Material Removal Rate (MRR) on the coarse run. Here’s an image explaining the concept;
the orange is the material that will be removed in the coarse milling phase:
Once you’ve got your coarse outline for the part, things get more interesting. The milling rate drops because now you’re dealing with finer features. We’ve entered the Medium Milling part of the build.
At Phasio, we find it useful to find a geometric expression of this phase. Before we do that, I need to introduce you to the Shrink Wrap Volume.
Shrink Wrap Volume
The Shrink Wrap Volume is a smaller volume than the convex hull, but is still larger than the part’s volume. You can probably guess what it is based on its name. Imagine you took your part and put it into Shrink Wrap. The shape formed by the shrink wrap tape around the part is what we’re talking about.
So if we combine the concepts of Convex Hull Volume with Shrink Wrap volume, we get quite an interesting result from a machining perspective.
Here’s a scrappy illustration to show you what I mean:
So now this leave us with only the final volume, which is the fine milling. This can be determined by taking the Shrink Wrap Volume and subtracting the actual part volume. Here’s how it might look:
So now we have our three volumes:
- Coarse Volume: The volume of the block minus the convex hull volume.
- Medium Volume: The volume of the convex hull minus the shrink wrap volume.
- Fine Volume: The volume of the shrink wrap minus the part volume.
This is really useful from a machining perspective because it allows us adjust our milling rates based on the complexity of the features we are milling. When I say “adjust the milling rates”, I mean that we can use different Material Removal Rates (MRRs) for each of these volumes.
A material removal rate is a measure of how quickly a machine can remove material from a part. It is typically expressed in cubic millimeters per second (mm³/s) and depends on the type of milling operation being performed, the material being cut, and the machine’s capabilities. In this example we’ll use some common MRRs for CNC milling operations.
Generally speaking, MRRs (in metric) would be in the range of 1000mm3/second (seriously fast) to 0.1-1mm3/second (finishing and fine work). Since this process is just an approximation of the true behaviour of the machine, we can simply go with the following rates:
Coarse milling: 500mm3/sec
Medium milling: 10mm3/sec
Fine milling: 1mm3/sec
So performing this calculation, we could get an estimate of the time it would take to mill the part! Great!
An example
So let’s work through an example. We’ll use a part with the following dimensions:
Block volume: 75×75×75 = 421,875 mm³
Part volume: 15,000 mm³ (example)
Convex hull volume: 25,000 mm³
Shrink wrap volume: 18,000 mm³
Using the above pricing logic, this gives us our three milling volumes:
Coarse volume: 421,875 - 25,000 = 396,875 mm³
Medium volume: 25,000 - 18,000 = 7,000 mm³
Fine volume: 18,000 - 15,000 = 3,000 mm³
If we divide these volumes by the MRRs we defined earlier, we can estimate the machining time for each phase:
Coarse milling: 396,875 mm³ ÷ 500 mm³/sec = 794 seconds (13.2 minutes)
Medium milling: 7,000 mm³ ÷ 10 mm³/sec = 700 seconds (11.7 minutes)
Fine milling: 3,000 mm³ ÷ 1 mm³/sec = 3,000 seconds (50 minutes)
Which gives us a total machining time of: 4,494 seconds = 74.9 minutes = 1.25 hours
Great, so now we have our machining time, but how do we turn this into a price? Let’s assume that we bill our machining time at $72/hour. This is equivalent to $0.02/second.
The total cost for machining the part would be: 4,494 seconds × $0.02/second = $89.88
But wait, there’s more!
Before we finish, we need to consider a few more factors that can influence the final price:
- Precision: If the part requires a higher precision, we can apply a multiplier to the price. For example, if the precision multiplier is 1.5, the price would become: $89.88 × 1.5 = $134.82
- Quantity: If the customer orders multiple parts, we can apply a quantity multiplier. For example, if the quantity is 10 and the multiplier is 2.3, the price would become: $134.82 × 2.3 = $309.11
- Minimum price: We can set a minimum price for the part to ensure that we cover our costs. For example, if the minimum price is $1000, we would adjust the final price to: $309.11 < $1000, so final price = $1000
This enables us to price based on both the geometry of the part and the business context, ensuring that we cover our costs while remaining competitive.
Summary
In this post, we’ve walked through a simple methodology for pricing CNC machined parts based on their geometry. As a recap, the steps we took were:
- Identify the raw stock: Determine the available block sizes.
- Fit the part into a block: Filter blocks to find the smallest one that fits the part.
- Calculate the volumes: Compute the coarse, medium, and fine volumes based on the part’s geometry.
- Estimate machining time: Use the defined Material Removal Rates (MRRs) to estimate the time for each milling phase.
- Calculate the price: Multiply the total machining time by the hourly rate to get the final price.
This is obviously just a first step, and every business will have it’s own variations and improvement on this. However, I thought it would be useful to share this as a starting point for anyone looking to implement a pricing equation for CNC machined parts, since the quoting process can be a real pain point for so many manufacturers.
I’ve put the entire, finished pricing equation below, which you can use as a starting point for your own pricing calculations. If you want to try this, you can just go to the Phasio website, create a free account, and paste this code into the equation editor.
Note on implementation: While the example above demonstrates the pricing logic using time-based calculations (MRR → machining time → hourly rate), the final pricing equation uses direct cost-per-volume rates for each milling phase. These approaches are mathematically equivalent—for instance, coarse milling at $72/hour with 500mm³/sec MRR equals $0.00004/mm³. In practice, we find the volume-based approach easier to calibrate and maintain, as you can adjust rates based on real job costs without recalculating intermediate time values.
The pricing equation
Here’s the full equation for use at Phasio for 5-axis CNC machined parts:
const { material, width, height, length, volume, area, convexHullVolume, shrinkWrapVolume, precision } = specification
const { quantity } = requisition
const blockSizes: [number, number, number][] = [
[25, 25, 25],
[50, 50, 50],
[75, 75, 75],
[100, 100, 100],
[150, 125, 125],
[150, 150, 150],
[200, 200, 200],
]
function canFit(specDimensions: [number, number, number], blockDimensions: [number, number, number]) {
const sortedSpec = [...specDimensions].sort((a, b) => a - b)
const sortedBlock = [...blockDimensions].sort((a, b) => a - b)
return sortedSpec.every((dim, index) => dim <= sortedBlock[index])
}
function calculateVolume(dimensions: [number, number, number]): number {
return dimensions[0] * dimensions[1] * dimensions[2]
}
function findSmallestFittingBlock(): [number, number, number] | null {
const specDimensions: [number, number, number] = [width, height, length]
const fittingBlocks = blockSizes.filter(block =>
canFit(specDimensions, block)
)
if (fittingBlocks.length === 0) {
return null
}
// Find the block with minimum volume
return fittingBlocks.reduce((smallest, current) => {
const smallestVolume = calculateVolume(smallest)
const currentVolume = calculateVolume(current)
return currentVolume < smallestVolume ? current : smallest
})
}
const bestBlock = findSmallestFittingBlock()
const coarseCostPerMM = variable('Coarse cost per mm3', 0.00005)
const mediumCostPerMM = variable('Medium cost per mm3', 0.0003)
const fineCostPerMM = variable('Fine cost per mm3', 0.001)
const minPricePerPart = variable('Min price', 1000)
function getQuantityMultiplier(quantity: number): number {
const tiers: [number, number][] = [
[1, 3],
[10, 2.3],
[100, 1.9],
[250, 1.6],
[500, 1.4],
[750, 1.1],
[1000, 1]
];
if (quantity < 1) return tiers[0][1];
if (quantity >= 1000) return tiers[tiers.length - 1][1];
for (let i = 0; i < tiers.length - 1; i++) {
const [lowerQty, lowerMultiplier] = tiers[i];
const [upperQty, upperMultiplier] = tiers[i + 1];
if (quantity >= lowerQty && quantity <= upperQty) {
const t = (quantity - lowerQty) / (upperQty - lowerQty);
return lowerMultiplier + (upperMultiplier - lowerMultiplier) * t;
}
}
return tiers[0][1];
}
if (bestBlock) {
const blockVolume = calculateVolume(bestBlock)
const coarseVolume = blockVolume - convexHullVolume
const mediumVolume = convexHullVolume - shrinkWrapVolume
const fineVolume = shrinkWrapVolume - volume
const precisionMultiplier = precision?.value ?? 1
const price = (coarseVolume * coarseCostPerMM) + (mediumVolume * mediumCostPerMM) + (fineVolume * fineCostPerMM)
const precisionAdjustedPrice = precisionMultiplier * price
const quantityMultiplier = variable('Quantity multiplier', getQuantityMultiplier(quantity))
const quantityAdjustedPrice = quantityMultiplier * precisionAdjustedPrice
const calculatedPrice = variable('Calculated price', quantityAdjustedPrice)
if (calculatedPrice * quantity < minPricePerPart) {
const finalPrice = variable('Final price', minPricePerPart / quantity)
done(finalPrice)
} else {
const finalPrice = variable('Final price', calculatedPrice)
done(finalPrice)
}
} else {
done(0)
}
What topics would you like me to cover in future posts? Feel free to reach out on LinkedIn with your suggestions!