Why Cities Exist

The basic tradeoff

Cities are expensive. Land costs more, commutes are longer, and the air is dirtier. Yet most people live in them. They come because being close to other people and firms makes them more productive. A city exists when these gains are larger than the costs of crowding.

Gains from size

Bigger cities offer more. Workers find better job matches. Firms find specialized suppliers. Ideas spread faster. Economists call these gains agglomeration economies. A simple way to write them is that output per worker rises with city size \(N\):

\[\text{Gain}(N) = A \cdot N^{\gamma}\]

where \(\gamma\) is the agglomeration elasticity. Rosenthal and Strange (2004) summarize the evidence: doubling a city’s size raises productivity by about 3 to 8 percent.

Costs of size

Bigger cities also cost more. Each new resident pushes the city edge outward, so commutes grow and land near jobs gets scarcer:

\[\text{Cost}(N) = c \cdot N^{\theta}\]

with \(\theta > \gamma\). Costs eventually grow faster than gains.

Optimal size vs equilibrium size

Utility per resident is \(U(N) = A N^{\gamma} - c N^{\theta}\). It first rises, then falls. The optimal size \(N^*\) is the peak.

But no one chooses city size. People move in as long as the city beats their outside option \(\bar{u}\). So the city keeps growing past the peak, until \(U(N) = \bar{u}\). This is why many economists argue that free migration makes cities too big. Each newcomer adds to crowding but does not pay for the crowding they cause.

The Model View. In the simulation below, you set the strength of agglomeration, the cost of crowding, and the outside option. The plot shows utility per resident at each city size, the optimal size, and the size that migration produces.

#| standalone: true
#| viewerHeight: 620

library(shiny)

ui <- fluidPage(
  tags$head(tags$style(HTML("
    .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("gamma", "Gains from size (agglomeration):",
                  min = 0.02, max = 0.10, value = 0.06, step = 0.01),
      sliderInput("cost", "Cost of crowding:",
                  min = 2, max = 10, value = 5, step = 0.5),
      sliderInput("ubar", "What people get elsewhere (outside option):",
                  min = 80, max = 105, value = 96, step = 1),
      uiOutput("info")
    ),
    mainPanel(
      width = 9,
      plotOutput("u_plot", height = "480px")
    )
  )
)

server <- function(input, output, session) {

  A <- 100
  theta <- 0.5

  model <- reactive({
    g <- input$gamma
    cc <- input$cost
    nmax <- max(40, 3 * (A * g / (cc * theta))^(1 / (theta - g)))
    N <- seq(0.05, nmax, length.out = 1500)
    U <- A * N^g - cc * N^theta
    n_opt <- (A * g / (cc * theta))^(1 / (theta - g))
    u_opt <- A * n_opt^g - cc * n_opt^theta
    # equilibrium: largest N where U >= ubar (to the right of the peak)
    right <- N >= n_opt & U >= input$ubar
    n_eq <- if (any(right)) max(N[right]) else NA
    list(N = N, U = U, n_opt = n_opt, u_opt = u_opt, n_eq = n_eq, nmax = nmax)
  })

  output$u_plot <- renderPlot({
    m <- model()
    par(mar = c(4.5, 4.5, 3, 1))
    plot(m$N, m$U, type = "l", lwd = 3, col = "#2c3e50",
         xlab = "City population (millions)",
         ylab = "Utility per resident",
         main = "Gains minus crowding costs",
         ylim = c(min(m$U, input$ubar) - 5, max(m$U) + 5))
    abline(h = input$ubar, lty = 2, col = "#e74c3c", lwd = 2)
    abline(v = m$n_opt, lty = 3, col = "#27ae60", lwd = 2)
    points(m$n_opt, m$u_opt, pch = 19, cex = 1.6, col = "#27ae60")
    if (!is.na(m$n_eq)) {
      abline(v = m$n_eq, lty = 3, col = "#e67e22", lwd = 2)
      points(m$n_eq, input$ubar, pch = 19, cex = 1.6, col = "#e67e22")
    }
    legend("topright", bty = "n", cex = 0.9,
           legend = c("Utility per resident", "Outside option",
                      "Optimal size", "Equilibrium size"),
           col = c("#2c3e50", "#e74c3c", "#27ae60", "#e67e22"),
           lwd = c(3, 2, 2, 2), lty = c(1, 2, 3, 3))
  })

  output$info <- renderUI({
    m <- model()
    eq_txt <- if (is.na(m$n_eq)) {
      "no city forms (outside option too good)"
    } else if (m$n_eq >= 0.999 * m$nmax) {
      "off the chart"
    } else {
      paste0(round(m$n_eq, 1), " million")
    }
    tags$div(class = "info-box",
      HTML(paste0(
        "<b>Optimal size:</b> ", round(m$n_opt, 1), " million<br>",
        "<b>Equilibrium size:</b> ", eq_txt, "<br>",
        "<b>Utility at optimum:</b> ", round(m$u_opt, 1), "<br>",
        "<b>Outside option:</b> ", input$ubar
      ))
    )
  })
}

shinyApp(ui, server)

Things to try

  • Raise the gains from size: the peak moves right and up. Stronger agglomeration supports bigger cities.
  • Raise the crowding cost: the peak moves left. Better transit or more housing works like a lower crowding cost.
  • Lower the outside option: the equilibrium city grows far past the optimum. When rural life is poor, people keep coming even as crowding gets worse.
  • Raise the outside option above the peak: no city forms. Gains from size must beat what people can get elsewhere.

Why the model matters

The tradeoff explains a lot. It explains why cities grew when transport costs fell and factories needed many workers. It explains why some cities shrink when their main industry leaves. It also explains why policy can make cities better: anything that lowers the cost of crowding, like transit or housing supply, raises the utility of everyone in the city.


How big do cities get? Zipf’s law

Zipf’s law is a pattern in city sizes: a city’s size is roughly the largest city’s size divided by its rank.

Take a country whose largest city has 10 million people:

  • The 2nd largest has about 10 ÷ 2 = 5 million.
  • The 3rd has about 10 ÷ 3 ≈ 3.3 million.
  • The 10th has about 10 ÷ 10 = 1 million.

So rank × size stays about the same for every city.

Why a straight line on a log scale? Size = 10 ÷ rank. Taking logs turns division into subtraction: log(size) = log(10) − log(rank). That is a straight line with slope −1. Double the rank, and size halves.

The usual explanation is Gabaix (1999). If cities grow at random rates that don’t depend on their size, and none can shrink below a small minimum, this pattern appears on its own over time.

It holds best for the largest cities. Once you include every small town it fits less well (Eeckhout 2004). George Zipf, a linguist, first found the same pattern in how often words are used.

The Model View. Set the population of the largest city. The bars show the population Zipf’s law predicts for each city by rank: the largest city’s population divided by the rank.

#| standalone: true
#| viewerHeight: 560

library(shiny)

ui <- fluidPage(
  tags$head(tags$style(HTML("
    .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("top", "Population of the largest city (millions):",
                  min = 1, max = 30, value = 10, step = 1),
      uiOutput("info")
    ),
    mainPanel(
      width = 9,
      plotOutput("bars", height = "460px")
    )
  )
)

server <- function(input, output, session) {

  pop <- reactive(input$top / seq_len(10))

  output$bars <- renderPlot({
    p <- pop()
    par(mar = c(4.5, 4.5, 3, 1))
    b <- barplot(p, names.arg = seq_along(p), col = "#2980b9", border = "white",
                 xlab = "Rank of the city", ylab = "Population (millions)",
                 main = "City population by rank", ylim = c(0, 1.15 * input$top))
    text(b, p, sprintf("%.1f", p), pos = 3, cex = 0.85)
  })

  output$info <- renderUI({
    p <- pop()
    tags$div(class = "info-box",
      HTML(paste0(
        "<b>Largest city:</b> ", sprintf("%.1f", p[1]), " million<br>",
        "<b>2nd:</b> ", sprintf("%.1f", p[1]), " / 2 = ", sprintf("%.1f", p[2]), " million<br>",
        "<b>3rd:</b> ", sprintf("%.1f", p[1]), " / 3 = ", sprintf("%.1f", p[3]), " million<br>",
        "<b>Rank x population:</b> ", sprintf("%.1f", p[1]), " million for every city"
      ))
    )
  })
}

shinyApp(ui, server)

Things to try

  • Set the largest city to 10 million: the 2nd city has 5 million, the 3rd 3.3 million, and the 10th 1 million.
  • Double the largest city to 20 million: every other city doubles too. Each city’s population is always the largest city’s divided by its rank.

Connections

Did you know?

  • More than half of the world’s population now lives in urban areas. The United Nations estimates the world crossed that line around 2007.
  • Jane Jacobs argued in The Economy of Cities (1969) that cities came first and farming grew around them. Her idea that cities create new kinds of work is still central to urban economics.

Key papers