Transportation and Congestion

Why traffic is an economic problem

When you drive onto a busy road, you slow down everyone else a little. You pay for your own time, but not for theirs. Since no one pays the full cost of their trip, people drive too much. The result is congestion. It is a classic externality.

Travel time rises with traffic

On an empty road, travel time is short. As traffic gets close to the road’s capacity, travel time climbs fast. The last few cars add much more delay than the first few.

Private cost vs social cost

Each driver pays the private cost: the value of their own time on the road. But one more car also adds a small delay to every other driver. The social cost of a trip is the private cost plus that delay to everyone else.

Drivers keep entering until the value of a trip equals the private cost. The efficient number of trips is lower, where the value of a trip equals the social cost.

The fix: a congestion toll

A toll equal to the gap between social and private cost, at the efficient level of traffic, makes each driver pay for the delay they cause. This idea comes from William Vickrey in the 1950s and 1960s. Some drivers switch times, routes, or modes. The ones who stay get a faster trip.

Why new lanes do not fix traffic

Duranton and Turner (2011) found that vehicle miles in US cities rise about one for one with new highway lanes. They call this the fundamental law of road congestion. New capacity lowers travel time for a while, but new trips fill the road again.

The Model View. The plot shows demand for trips, the private cost, and the social cost. Traffic settles where demand meets private cost. The efficient level is where demand meets social cost. Try the toll and the capacity sliders to see each policy.

#| standalone: true
#| viewerHeight: 640

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("demand", "Demand for trips (value of first trip, $):",
                  min = 20, max = 60, value = 40, step = 1),
      sliderInput("cap", "Road capacity (cars per hour):",
                  min = 1000, max = 4000, value = 2000, step = 250),
      sliderInput("vot", "Value of time ($ per hour):",
                  min = 10, max = 50, value = 25, step = 5),
      sliderInput("toll", "Toll ($ per trip):",
                  min = 0, max = 20, value = 0, step = 0.5),
      uiOutput("info")
    ),
    mainPanel(
      width = 9,
      plotOutput("cost_plot", height = "500px")
    )
  )
)

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

  t0 <- 0.5          # free-flow trip time, hours
  slope <- 0.008     # demand falls $0.008 per extra car per hour

  model <- reactive({
    a <- input$demand; K <- input$cap; v <- input$vot; tau <- input$toll
    tt  <- function(V) t0 * (1 + 0.15 * (V / K)^4)
    dtt <- function(V) t0 * 0.6 * V^3 / K^4
    pc  <- function(V) v * tt(V)
    msc <- function(V) pc(V) + V * v * dtt(V)
    dem <- function(V) a - slope * V
    vmax <- a / slope
    solve_at <- function(f) {
      g <- function(V) dem(V) - f(V)
      if (g(0) <= 0) return(0)
      uniroot(g, c(0, vmax))$root
    }
    v_eq  <- solve_at(function(V) pc(V) + tau)
    v_opt <- solve_at(msc)
    toll_opt <- msc(v_opt) - pc(v_opt)
    # deadweight loss of the untolled equilibrium
    v_free <- solve_at(pc)
    dwl <- if (v_free > v_opt) integrate(function(V) msc(V) - dem(V), v_opt, v_free)$value else 0
    list(tt = tt, pc = pc, msc = msc, dem = dem, vmax = vmax,
         v_eq = v_eq, v_opt = v_opt, toll_opt = toll_opt, dwl = dwl, tau = tau)
  })

  output$cost_plot <- renderPlot({
    m <- model()
    V <- seq(0, m$vmax, length.out = 400)
    par(mar = c(4.5, 4.5, 3, 1))
    ymax <- max(m$dem(0), 1.3 * m$msc(max(m$v_eq, m$v_opt)))
    plot(V, m$dem(V), type = "l", lwd = 3, col = "#2c3e50",
         ylim = c(0, ymax), xlab = "Cars per hour",
         ylab = "Dollars per trip", main = "Demand, private cost, and social cost")
    lines(V, m$pc(V), lwd = 3, col = "#3498db")
    lines(V, m$msc(V), lwd = 3, col = "#e74c3c")
    if (m$tau > 0) lines(V, m$pc(V) + m$tau, lwd = 2, lty = 2, col = "#3498db")
    abline(v = m$v_eq, lty = 3, col = "#3498db", lwd = 2)
    abline(v = m$v_opt, lty = 3, col = "#27ae60", lwd = 2)
    legend("topright", bty = "n", cex = 0.9,
           legend = c("Demand (value of a trip)", "Private cost",
                      "Social cost", "Private cost + toll",
                      "Traffic with current toll", "Efficient traffic"),
           col = c("#2c3e50", "#3498db", "#e74c3c", "#3498db", "#3498db", "#27ae60"),
           lwd = c(3, 3, 3, 2, 2, 2), lty = c(1, 1, 1, 2, 3, 3))
  })

  output$info <- renderUI({
    m <- model()
    tags$div(class = "info-box",
      HTML(paste0(
        "<b>Traffic now:</b> ", format(round(m$v_eq), big.mark = ","), " cars/hour<br>",
        "<b>Trip time now:</b> ", round(60 * m$tt(m$v_eq)), " minutes<br>",
        "<b>Efficient traffic:</b> ", format(round(m$v_opt), big.mark = ","), " cars/hour<br>",
        "<b>Efficient toll:</b> $", sprintf("%.2f", m$toll_opt), "<br>",
        "<b>Loss with no toll:</b> $", format(round(m$dwl), big.mark = ","), " per hour"
      ))
    )
  })
}

shinyApp(ui, server)

Things to try

  • Set the toll equal to the efficient toll: the blue dotted line moves onto the green one. Traffic falls to the efficient level.
  • Double the capacity with no toll: trip time falls, but traffic rises. Then raise demand a little, as a growing city would. Trip time creeps back up. This is the fundamental law in action.
  • Raise the value of time: the gap between private and social cost grows. Congestion is more costly in rich, busy cities.
  • Lower demand: at low traffic, the efficient toll is close to zero. Tolls matter only on crowded roads.

Congestion pricing in practice

Singapore started charging drivers to enter its center in 1975. London followed in 2003 and Stockholm in 2006. New York started its congestion charge for lower Manhattan in January 2025. In each case, traffic in the charged zone fell.

Tolls are unpopular before they start and more popular after. People see faster trips and better bus service. The fairness question matters too. Low-income drivers pay a larger share of their income, but many low-income workers ride transit, and toll revenue can fund it.

Akbar, Couture, Duranton and Storeygard (2023) add a twist. In Indian cities, most of the gap in travel speed between cities comes from slow roads even when traffic is light, not from congestion. Better roads and street design can matter more than tolls.

Mass transit

Buses and trains are the other answer to congestion. Each rider on a full train takes a car off the road.

Transit has a special cost structure. Building a rail line is very expensive, but carrying one more rider costs almost nothing. So transit works best where many people travel the same way: dense cities with a busy downtown.

Transit also gets better as more people use it. More riders justify more frequent service, which means shorter waits for everyone. Economists call this the Mohring effect. It is one reason most cities price transit below cost.

The catch is that most US metro areas are spread out, with jobs in many places. That makes it hard for transit to win riders from cars. A congestion toll helps transit too, since it makes driving downtown cost more.

Connections

Did you know?

  • William Vickrey won the Nobel Prize in Economics in 1996. He died three days after it was announced.
  • The travel-time curve in the simulation comes from the US Bureau of Public Roads in 1964. Planners still call it the BPR function.
  • Traffic jams can form with no accident at all. When roads are close to capacity, one driver braking hard can send a wave of stopped cars backward for miles.

Key papers