Zoning & Housing Supply
Supply elasticity and housing prices
Housing supply elasticity determines how prices respond to demand shocks. When demand increases (population growth, rising incomes, low interest rates), the outcome depends entirely on how easily builders can add new housing:
- Elastic supply (few regulations, available land): builders respond to higher demand by constructing more units. Prices stay relatively stable. Think Houston, Dallas, Atlanta.
- Inelastic supply (strict zoning, geographic constraints): builders can’t respond. Demand increases → prices spike. Think San Francisco, New York, Boston.
The supply curve is simple:
\[Q^s = \alpha + \eta \cdot P\]
where \(\eta\) is the supply elasticity. When \(\eta\) is large, quantity responds easily to price signals. When \(\eta\) is small, prices bear the full burden of demand shifts.
The regulatory tax
Glaeser, Gyourko & Saks (2005) showed that in highly regulated cities, housing prices far exceed physical construction costs. The gap — what they call the regulatory tax — is the implicit cost of zoning, permitting delays, building height restrictions, and neighborhood opposition (NIMBYism).
In Manhattan, the regulatory tax can exceed $100,000 per apartment. In Houston, it’s close to zero. The difference is not construction technology — it’s regulation.
Saiz (2010)
Saiz (2010) estimated housing supply elasticity for US metro areas, finding enormous variation:
| City | Supply Elasticity |
|---|---|
| Houston | ~3.0 (very elastic) |
| Atlanta | ~2.5 |
| Chicago | ~1.5 |
| Boston | ~0.8 |
| San Francisco | ~0.6 (very inelastic) |
| Miami | ~0.6 |
Geographic constraints (water, mountains) and regulatory constraints both contribute. San Francisco has both — water on three sides and strict zoning.
#| standalone: true
#| viewerHeight: 680
library(shiny)
ui <- fluidPage(
tags$head(tags$style(HTML("
.stats-box {
background: #f0f4f8; border-radius: 6px; padding: 14px;
margin-top: 12px; font-size: 14px; line-height: 1.9;
}
.stats-box b { color: #2c3e50; }
.good { color: #27ae60; font-weight: bold; }
.bad { color: #e74c3c; font-weight: bold; }
.info-box {
background: #eaf2f8; border-radius: 6px; padding: 14px;
margin-top: 12px; font-size: 13px; line-height: 1.8;
}
.info-box b { color: #2c3e50; }
"))),
sidebarLayout(
sidebarPanel(
width = 3,
sliderInput("elasticity", "Supply elasticity:",
min = 0.3, max = 5, value = 1.5, step = 0.1),
sliderInput("demand0", "Initial demand (population index):",
min = 50, max = 200, value = 100, step = 10),
sliderInput("shock", "Demand shock (% increase):",
min = 0, max = 50, value = 20, step = 5),
sliderInput("cost", "Construction cost ($1000s):",
min = 50, max = 300, value = 150, step = 10),
checkboxInput("rent_control", "Impose rent control", value = FALSE),
actionButton("go", "Update market", class = "btn-primary", width = "100%"),
uiOutput("info")
),
mainPanel(
width = 9,
plotOutput("sd_plot", height = "500px")
)
)
)
server <- function(input, output, session) {
market <- reactive({
input$go
eta <- input$elasticity
d0 <- input$demand0
shock_pct <- input$shock / 100
c_cost <- input$cost
rent_ctrl <- input$rent_control
# Demand curve: P = a - b*Q
# Higher d0 shifts demand right
a_init <- d0 * 4
b_dem <- 1.5
# Supply curve: P = c_cost + (1/eta)*Q
# Higher eta = flatter supply
# Initial equilibrium: a_init - b_dem*Q = c_cost + Q/eta
# Q* = (a_init - c_cost) / (b_dem + 1/eta)
q_init <- (a_init - c_cost) / (b_dem + 1 / eta)
q_init <- max(q_init, 1)
p_init <- c_cost + q_init / eta
# After demand shock: demand shifts right
a_new <- a_init * (1 + shock_pct)
q_new <- (a_new - c_cost) / (b_dem + 1 / eta)
q_new <- max(q_new, 1)
p_new <- c_cost + q_new / eta
# With rent control: price stays at p_init, shortage develops
if (rent_ctrl && shock_pct > 0) {
p_ctrl <- p_init
q_supplied_ctrl <- (p_ctrl - c_cost) * eta # supply at controlled price
q_demanded_ctrl <- (a_new - p_ctrl) / b_dem # demand at controlled price
shortage <- q_demanded_ctrl - q_supplied_ctrl
} else {
p_ctrl <- p_new
q_supplied_ctrl <- q_new
q_demanded_ctrl <- q_new
shortage <- 0
}
# Regulatory tax
reg_tax <- p_new - c_cost
# Price change
price_change_pct <- (p_new - p_init) / p_init * 100
quantity_change_pct <- (q_new - q_init) / q_init * 100
# Affordability (price / income proxy)
income <- d0 * 0.8 # rough income proxy
afford_init <- p_init / income
afford_new <- p_new / income
list(q_init = q_init, p_init = p_init,
q_new = q_new, p_new = p_new,
a_init = a_init, a_new = a_new,
b_dem = b_dem, eta = eta, c_cost = c_cost,
reg_tax = reg_tax,
price_change = price_change_pct,
quantity_change = quantity_change_pct,
afford_init = afford_init, afford_new = afford_new,
rent_ctrl = rent_ctrl, shortage = shortage,
p_ctrl = p_ctrl, q_supplied_ctrl = q_supplied_ctrl,
q_demanded_ctrl = q_demanded_ctrl,
shock_pct = shock_pct)
})
output$sd_plot <- renderPlot({
m <- market()
par(mar = c(5, 5, 4, 2))
q_max <- max(m$q_new, m$q_init, m$q_demanded_ctrl, na.rm = TRUE) * 1.4
p_max <- max(m$p_new, m$p_init, m$a_new) * 1.1
q_range <- seq(0, q_max, length.out = 200)
# Supply curve
p_supply <- m$c_cost + q_range / m$eta
# Demand curves
p_demand_init <- m$a_init - m$b_dem * q_range
p_demand_new <- m$a_new - m$b_dem * q_range
plot(NULL, xlim = c(0, q_max), ylim = c(0, p_max),
xlab = "Quantity (housing units)",
ylab = "Price ($1000s)",
main = "Housing Supply and Demand")
# Supply
lines(q_range, p_supply, lwd = 3, col = "#e74c3c")
# Initial demand
lines(q_range[p_demand_init > 0], p_demand_init[p_demand_init > 0],
lwd = 2, col = "#3498db", lty = 2)
# New demand (if shock > 0)
if (m$shock_pct > 0) {
lines(q_range[p_demand_new > 0], p_demand_new[p_demand_new > 0],
lwd = 3, col = "#3498db")
}
# Construction cost line
abline(h = m$c_cost, lty = 3, col = "#95a5a6", lwd = 1.5)
text(q_max * 0.02, m$c_cost, "Construction cost", pos = 3, cex = 0.75, col = "#95a5a6")
# Initial equilibrium
points(m$q_init, m$p_init, pch = 19, cex = 2, col = "#7f8c8d")
# New equilibrium
if (m$shock_pct > 0) {
if (m$rent_ctrl) {
# Show rent control price and shortage
abline(h = m$p_ctrl, lty = 2, col = "#f39c12", lwd = 2)
text(q_max * 0.5, m$p_ctrl, "Rent control ceiling", pos = 3,
cex = 0.85, col = "#f39c12", font = 2)
# Shortage bracket
arrows(m$q_supplied_ctrl, m$p_ctrl * 0.92,
m$q_demanded_ctrl, m$p_ctrl * 0.92,
code = 3, lwd = 2, col = "#e74c3c", length = 0.1)
text((m$q_supplied_ctrl + m$q_demanded_ctrl) / 2, m$p_ctrl * 0.85,
paste0("Shortage = ", round(m$shortage, 0)),
cex = 0.85, col = "#e74c3c", font = 2)
}
points(m$q_new, m$p_new, pch = 19, cex = 2, col = "#2c3e50")
# Regulatory tax bracket
if (m$reg_tax > 5) {
arrows(m$q_new * 1.05, m$c_cost,
m$q_new * 1.05, m$p_new,
code = 3, lwd = 2, col = "#8e44ad", length = 0.08)
text(m$q_new * 1.08, (m$c_cost + m$p_new) / 2,
paste0("Reg. tax\n$", round(m$reg_tax, 0), "k"),
cex = 0.8, col = "#8e44ad", font = 2)
}
}
legend("topright", bty = "n", cex = 0.85,
legend = c("Supply", "Initial demand", "New demand",
"Initial eq.", "New eq."),
col = c("#e74c3c", "#3498db", "#3498db", "#7f8c8d", "#2c3e50"),
lwd = c(3, 2, 3, NA, NA), lty = c(1, 2, 1, NA, NA),
pch = c(NA, NA, NA, 19, 19), pt.cex = 2)
})
output$info <- renderUI({
m <- market()
tags$div(class = "info-box",
HTML(paste0(
"<b>Price change:</b> ",
ifelse(abs(m$price_change) < 1,
"<span class='good'>+", "<span class='bad'>+"),
round(m$price_change, 1), "%</span><br>",
"<b>Quantity change:</b> +", round(m$quantity_change, 1), "%<br>",
"<b>Affordability ratio:</b> ",
round(m$afford_init, 1), " -> ", round(m$afford_new, 1), "<br>",
"<b>Regulatory tax:</b> $", round(m$reg_tax, 0), "k<br>",
if (m$rent_ctrl && m$shock_pct > 0)
paste0("<b>Shortage:</b> <span class='bad'>", round(m$shortage, 0), " units</span>")
else ""
))
)
})
}
shinyApp(ui, server)
Things to try
- Set elasticity to 0.5 (San Francisco): a 20% demand shock causes a large price increase and a small quantity increase. Most of the adjustment is in prices.
- Set elasticity to 4 (Houston): the same demand shock causes a large quantity increase and a small price increase. Builders absorb the demand.
- Compare the regulatory tax at different elasticities: with low elasticity, the gap between market price and construction cost is enormous. That gap is the cost of regulation.
- Turn on rent control: the price is capped, but a shortage develops. Quantity supplied is less than quantity demanded at the controlled price.
The housing affordability crisis
The divergence in housing supply elasticity across US cities has enormous consequences. Since the 1970s, coastal cities with inelastic supply (San Francisco, New York, Boston, LA) have seen housing prices dramatically outpace incomes. Interior cities with elastic supply (Houston, Dallas, Phoenix, Atlanta) have not.
This isn’t about demand differences — all these cities experienced income growth and population growth. The difference is supply response. When demand rises and supply can’t respond, prices absorb everything.
Hsieh and Moretti (2019) estimated that housing constraints in high-productivity cities like New York and San Francisco caused a massive spatial misallocation of labor. Workers who would be more productive in these cities can’t afford to live there, so they stay in lower-productivity locations. The aggregate cost: roughly 36% of US GDP growth between 1964 and 2009 was lost to this misallocation. Relaxing housing constraints in just New York, San Francisco, and San Jose to the median US level would have raised GDP growth substantially.
The policy implication is stark: local zoning decisions have national macroeconomic consequences.
Did you know?
- Houston is the largest US city with no formal zoning ordinance. Instead, it uses deed restrictions and minimum lot-size requirements. The result: Houston has some of the most affordable housing among large US metros, with median home prices roughly one-third those of San Francisco.
- Japan takes a radically different approach: zoning is set nationally, not locally. The national government defines 12 zone types, and local municipalities cannot restrict land use beyond these categories. This means NIMBYs can’t block new housing at the neighborhood level. Tokyo, a metro of 37 million people, has housing costs roughly comparable to Houston.
- Hsieh and Moretti’s (2019) estimate of 36% lost GDP growth is one of the most cited numbers in recent urban economics. The mechanism is simple: productive cities can’t grow because they can’t build housing, so workers are stuck in less productive locations. The aggregate output loss is the difference between actual GDP and what GDP would have been with mobile workers and elastic housing supply.