Policies

A look at SNAP eligibility policies by state.

SNAP policies by state

Most states (40 + DC) have implemented broad-based categorical enrollment (BBCE) for SNAP, meaning that people in these states are categorically eligible for SNAP benefits if they qualify for certain benefits or services funded by TANF or state maintenance of effort (MOE) funds.

Under BBCE, households in most states can automatically qualify for SNAP without passing the program’s standard income or asset tests, both reducing the administrative burden for states and expanding access to families who may be above the income or asset limit but who are expressing significant need through employment and training programs, childcare assistance, transportation aid, etc.

However, of the 41 states that have BBCE for SNAP, five have maintained asset limits for some time period. Those states are Idaho, Michigan, Nebraska, Pennsylvania, and Texas. For these states in the table below, the asset limit displayed is that of the earliest policy. Four of those states (all but Texas) have changed their asset test policies over time.

Asset test policies

A deeper dive into the states with BBCE for SNAP who maintained asset tests, and how those tests have changed over time.

df <- data.frame(
  State = c("Idaho", "Michigan", "Nebraska", "Pennsylvania", "Texas"),
  Notes = c(
    "06/2009: Enacted BBCE for SNAP. Income limit set to 130 FPL, eliminated for households with at least one senior or disabled member. (No asset limit). Income and asset limits apply to households with or without dependent children.\n\n06/2011: Implementation of an asset test, $5,000 with the exclusion of (part of) the value of at least one, but not all vehicles (one vehicle per adult). Income limit 130 FPL remains.",
    "10/2000: Enacted BBCE for SNAP. Income limit set to 200 FPL, whether household has seniors, members with disabilities, or dependent children. (No asset limit)\n\n10/2011: Implementation of an asset test, $5,000 with the exclusion of an amount from the fair market value of one vehicle. Income limit 200 FPL remains.\n\n01/2012: Vehicle limit increased to at least one, but not all vehicles.\n Exclude one vehicle plus $15,000 combined (?) FMV of remaining vehicles.\n\n12/2019: Asset limit raised to $15,000 and exclusion of all vehicles\n\n03/2024: Asset limit eliminated",
    "10/2011: Enacted BBCE for SNAP. Income limit set to 130 FPL, asset limit set to $25,000 which is eliminated for households with at least one senior or disabled member. Exclusion of (part of?) the value of all vehicles.\n\n05/2021: Income limit increased to 165 FPL",
    "10/2008: Enacted BBCE for SNAP. Income limit set to 130 FPL, 200 FPL for households with at least one senior or disabled member.\n\n08/2009: Income limit raised to 160 FPL. Still 200 for households with at least one senior or disabled member.\n\n06/2012: Asset limit of $5,500 implemented, excluding part of the value of at least one (but not all) vehicles.\n\n05/2015: Asset limit eliminated.",
    "09/2001: Enacted BBCE for SNAP. Income limit set to 165 FPL, regardless of household composition. Asset limit set to $5,000 with the exclusion of an amount from the fair market value of one vehicle."
  )
)

kable(df, "html", longtable = TRUE, booktabs = TRUE) %>%
  kable_styling(latex_options = c("repeat_header"), font_size = 16) %>%
  column_spec(1, width = "3cm") %>%
  column_spec(2, width = "12cm") %>%
    kable_classic()
State Notes
Idaho

06/2009: Enacted BBCE for SNAP. Income limit set to 130 FPL, eliminated for households with at least one senior or disabled member. (No asset limit). Income and asset limits apply to households with or without dependent children.

06/2011: Implementation of an asset test, $5,000 with the exclusion of (part of) the value of at least one, but not all vehicles (one vehicle per adult). Income limit 130 FPL remains.
Michigan

10/2000: Enacted BBCE for SNAP. Income limit set to 200 FPL, whether household has seniors, members with disabilities, or dependent children. (No asset limit)

10/2011: Implementation of an asset test, $5,000 with the exclusion of an amount from the fair market value of one vehicle. Income limit 200 FPL remains.

01/2012: Vehicle limit increased to at least one, but not all vehicles. Exclude one vehicle plus $15,000 combined (?) FMV of remaining vehicles.

12/2019: Asset limit raised to $15,000 and exclusion of all vehicles

03/2024: Asset limit eliminated
Nebraska

10/2011: Enacted BBCE for SNAP. Income limit set to 130 FPL, asset limit set to $25,000 which is eliminated for households with at least one senior or disabled member. Exclusion of (part of?) the value of all vehicles.

05/2021: Income limit increased to 165 FPL
Pennsylvania

10/2008: Enacted BBCE for SNAP. Income limit set to 130 FPL, 200 FPL for households with at least one senior or disabled member.

08/2009: Income limit raised to 160 FPL. Still 200 for households with at least one senior or disabled member.

06/2012: Asset limit of $5,500 implemented, excluding part of the value of at least one (but not all) vehicles.

05/2015: Asset limit eliminated.
Texas 09/2001: Enacted BBCE for SNAP. Income limit set to 165 FPL, regardless of household composition. Asset limit set to $5,000 with the exclusion of an amount from the fair market value of one vehicle.

SNAP beneficiaries

Let’s now take a look at how the number of SNAP recipients has changed over time. The USDA count of SNAP beneficiaries starts in 1989 and is intermittent (1989, 1993, 1995) until 1997, from which point we have annual data (for June of each year) up to 2012.

SNAP beneficiaries over time

Here is a look at how the raw numbers of recipients have changed over time, grouped by the state’s BBCE + asset test policy.

# data starts in 1989, 1993, 1995, and yearly from 1997-2012

snap_counts_long <- pivot_longer(
    snap_counts,
    cols = starts_with("PRGNUM"),
    names_to = "year",
    values_to = "SNAP_benes",
    names_prefix = "PRGNUM"
)

snap_counts_long <- snap_counts_long[,c('STATE_CD','year','SNAP_benes')] %>%
    mutate(year = as.Date(paste0('06/01/',case_when(year >= 89 ~ 1900 + as.integer(year),
                                            year <= 12 ~ 2000 + as.integer(year))),'%m/%d/%Y'),
        SNAP_benes = as.integer(gsub(',','',SNAP_benes))) %>%
    group_by(STATE_CD) %>%
    arrange(year) %>%
    mutate('SNAP_bene_change' = SNAP_benes / lag(SNAP_benes) - 1) %>%
    ungroup()

snap_counts_BBCE_asset <- snap_counts_long[snap_counts_long$STATE_CD %in% c(asset_states),]
snap_counts_BBCE_noasset <- snap_counts_long[!(snap_counts_long$STATE_CD %in% c(asset_states,noBBCE_states)),]
snap_counts_noBBCE <- snap_counts_long[snap_counts_long$STATE_CD %in% noBBCE_states,]

# BBCE + asset test states

snap_change_BBCE_asset <- snap_counts_BBCE_asset %>%
    group_by(year) %>%
    summarise(total = sum(SNAP_benes, na.rm = TRUE)) %>%
    mutate(SNAP_bene_change = total / lag(total) - 1) %>%
    mutate(STATE_CD = 'BBCE + asset test')

snap_change_BBCE_asset_stateavgs <- snap_counts_BBCE_asset %>%
    arrange(STATE_CD,year) %>%
    mutate(SNAP_bene_change = SNAP_benes / lag(SNAP_benes) - 1) %>%
    group_by(year) %>%
    summarise(SNAP_bene_change = mean(SNAP_bene_change)) %>%
    mutate(STATE_CD = 'BBCE + asset test')

# BBCE + no asset test states

snap_change_BBCE_noasset <- snap_counts_BBCE_noasset %>%
    group_by(year) %>%
    summarise(total = sum(SNAP_benes, na.rm = TRUE)) %>%
    mutate(SNAP_bene_change = total / lag(total) - 1) %>%
    mutate(STATE_CD = 'BBCE + no asset test')

snap_change_BBCE_noasset_stateavgs <- snap_counts_BBCE_noasset %>%
    arrange(STATE_CD,year) %>%
    mutate(SNAP_bene_change = SNAP_benes / lag(SNAP_benes) - 1) %>%
    group_by(year) %>%
    summarise(SNAP_bene_change = mean(SNAP_bene_change)) %>%
    mutate(STATE_CD = 'BBCE + no asset test')

# No BBCE states

snap_change_noBBCE <- snap_counts_noBBCE %>%
    group_by(year) %>%
    summarise(total = sum(SNAP_benes, na.rm = TRUE)) %>%
    mutate(SNAP_bene_change = total / lag(total) - 1) %>%
    mutate(STATE_CD = 'No BBCE')

snap_change_noBBCE_stateavgs <- snap_counts_noBBCE %>%
    arrange(STATE_CD,year) %>%
    mutate(SNAP_bene_change = SNAP_benes / lag(SNAP_benes) - 1) %>%
    group_by(year) %>%
    summarise(SNAP_bene_change = mean(SNAP_bene_change)) %>%
    mutate(STATE_CD = 'No BBCE')


plot_data_aggregate <- rbind(snap_change_BBCE_asset, snap_change_BBCE_noasset, snap_change_noBBCE)
plot_data_averaged <- rbind(snap_change_BBCE_asset_stateavgs, snap_change_BBCE_noasset_stateavgs, snap_change_noBBCE_stateavgs)

Here I’m aggregating the total number of SNAP beneficiaries across states within a policy group and looking at the change in total number of beneficiaries across those states, over time.

# Plot

ggplot(plot_data_aggregate[!is.na(plot_data_aggregate$SNAP_bene_change),], aes(x = year, y = SNAP_bene_change * 100, group = STATE_CD, color = STATE_CD)) +
    geom_line() +
    geom_point() +
    theme_minimal() +
    labs(x = "Year", y = "% change in # SNAP recipients", color = "Policy group", title = "Year-over-year change by policy type (aggregated)")

Here I’m calculating the state-specific change in the number of SNAP beneficiaries and taking an an un-weighted average across the policy group. As you can see, the trends are the same whether looking at aggregate levels (above) or state-averages.

ggplot(plot_data_averaged[!is.na(plot_data_averaged$SNAP_bene_change),], aes(x = year, y = SNAP_bene_change * 100, group = STATE_CD, color = STATE_CD)) +
    geom_line() +
    geom_point() +
    theme_minimal() +
    labs(x = "Year", y = "% change in # SNAP recipients", color = "Policy group", title = "Year-over-year change by policy type (averaged)")

Asset test states

Here is the number of SNAP beneficiaries in asset test states, as well as each state’s year-to-year change in levels.

plot_data <- subset(snap_counts_long, STATE_CD %in% c('AL','AK','ID','PA','MI','NE','TX') &
                               year >= as.Date('1900-01-01'))

ggplot(plot_data[plot_data$year != as.Date('1989-06-01'),], aes(x = year, y = SNAP_benes/1000, group = STATE_CD, color = STATE_CD)) +
    geom_line() +
    geom_point() +
    scale_y_log10() +
    labs(title = 'Year-over-year level change for asset-test states (levels)',
         y = 'SNAP recipients (thousands)')

ggplot(plot_data[plot_data$year != as.Date('1989-06-01'),], aes(x = year, y = SNAP_bene_change * 100, group = STATE_CD, color = STATE_CD)) +
    geom_line() +
    geom_point() +
    theme_minimal() +
    labs(x = "Year", y = "% change in SNAP recipients", color = "State Code", title = "Year-over-year change for asset-test states")

A state-by-state look

Below I’ve plotted the change in year-to-year SNAP beneficiaries in states with an asset test compared to other states with BBCE but without asset tests.

The ORANGE line marks when the state implemented BBCE for SNAP.

A RED line indicates when the state implemented an asset test for SNAP.

A GREEN line indicates when the state raised or eliminated asset testing.

Note that there are several policy changes that happen after the last year in the USDA data (2012).

# Function to compare policy change states to non-changing states

log_plots <- function(states){
    
    plot_data_logs <- snap_counts_long %>%
        group_by(STATE_CD) %>%
        arrange(year) %>%
        mutate(SNAP_bene_change = SNAP_benes / lag(SNAP_benes) - 1) %>%
        ungroup() %>%
        subset(., STATE_CD %in% states)
    
    plot_data_logs <- rbind(plot_data_logs[,c('STATE_CD','year','SNAP_bene_change')], 
                            snap_change_BBCE_noasset_stateavgs[,c('STATE_CD','year','SNAP_bene_change')]) %>%
        subset(year >= start_year)
    
    plot_data_logs$STATE_CD <- factor(plot_data_logs$STATE_CD, levels = state_order)
    
    plot <- ggplot(plot_data_logs, aes(x = year, y = SNAP_bene_change, group = STATE_CD, color = STATE_CD)) +
        geom_line() +
        geom_point() +
        scale_color_manual(values = state_palette) +
        theme_minimal() +
        labs(x = "Year", y = "% change in # SNAP recipients", color = "State Code", title = paste0("Year-over-Year Change, ",states))
    
    return(plot)
    
}

Idaho

plot <- log_plots('ID')

plot + 
    geom_vline(xintercept = as.Date('2009-06-01'), color = 'orange3', alpha = 0.7) +
    geom_vline(xintercept = as.Date('2011-06-01'), color = 'red3', alpha = 0.7)

Michigan

plot <- log_plots('MI')

plot + 
    geom_vline(xintercept = as.Date('2000-10-01'), color = 'orange3', alpha = 0.7) +
    geom_vline(xintercept = as.Date('2011-06-01'), color = 'red3', alpha = 0.7) +
    geom_vline(xintercept = as.Date('2012-01-01'), color = 'green3', alpha = 0.7)

Nebraska

plot <- log_plots('NE')

plot + 
    geom_vline(xintercept = as.Date('2011-10-01'), color = 'orange3', alpha = 0.5)

Pennsylvania

plot <- log_plots('PA')

plot + 
    geom_vline(xintercept = as.Date('2008-10-01'), color = 'orange3', alpha = 0.7) +
    geom_vline(xintercept = as.Date('2009-08-01'), color = 'green3', alpha = 0.7) +
    geom_vline(xintercept = as.Date('2012-06-01'), color = 'red3', alpha = 0.7)

Texas

plot <- log_plots('TX')

plot + 
    geom_vline(xintercept = as.Date('2001-09-01'), color = 'orange3', alpha = 0.5)

Some diff-in-diffs

Playing around with the USDA data.

Stacked diff-in-diff experiments

BBCE vs no BBCE states

Pooled diff-in-diff between states that implemented BBCE without asset tests and states that never implemented BBCE for SNAP. State and experiment-by-time fixed effects. No controls, so probably correlated with many other state and time-varying factors. Nevertheless, we see a bump in SNAP beneficiaries with BBCE, as expect.

Pooled diff-in-diff (1) \[\log(\text{SNAP benes #}) = \beta_0 + \beta_1BBCE_s \times \textbf{1}_{t \geq 0}+\alpha_s + \gamma_{kt} + \epsilon_{kit}\] Pooled diff-in-diff (2) \[\text{Change in SNAP benes #} = \omega_0 + \omega_1BBCE_s \times \textbf{1}_{t \geq 0}+\alpha_s + \gamma_{kt} + \epsilon_{kit}\] Implementation of BBCE is associated with a 27.5% increase in the number of beneficiaries and a 8.3 percentage point increase in the year-over-year change in beneficiaries. Again, no controls other than state and time fixed effects.

bbce <- snap_policies[,c('state_pc','yearmonth','bbce','bbce_asset')] %>%
    filter(.,bbce == 1) %>%
    group_by(state_pc) %>%
    summarise(bbce_start = min(yearmonth))

bbce$bbce_start <- as.Date(paste0(bbce$bbce_start,'01'),'%Y%m%d')

bbce_experiment <- data.frame(
    'STATE_CD' = character(),
    'year' = as.Date(character()),
    't' = numeric(),
    'post' = numeric(),
    'bbce' = numeric(),
    'experiment' = character()
)

for(state in bbce_states) {
    
    bbce_counts <- snap_counts_long[snap_counts_long$STATE_CD == state,] %>%
        mutate('bbce' = 1)
    
    neverTreated_counts <- snap_counts_noBBCE %>%
        mutate('bbce' = 0)
    
    data <- rbind(bbce_counts,neverTreated_counts) %>%
        mutate('experiment' = factor(state))
    
    data$t <- round(as.numeric(difftime(data$year, bbce$bbce_start[bbce$state_pc == state], units = 'weeks') / 52))
    data$post <- ifelse(data$t >= 0, 1, 0)
    
    bbce_experiment <- rbind(bbce_experiment,data)
    
}

bbce_experiment$'SNAP_benes' <- bbce_experiment$SNAP_benes

bbce_felm1 <- felm(log(SNAP_benes) ~ bbce:post | STATE_CD + t:experiment|0|STATE_CD,data = bbce_experiment)
bbce_felm2 <- felm(SNAP_bene_change ~ bbce:post | STATE_CD + t:experiment|0|STATE_CD,data = bbce_experiment)

stargazer(bbce_felm1, bbce_felm2,
          type = 'text')
## 
## =======================================================
##                             Dependent variable:        
##                     -----------------------------------
##                      log(SNAP_benes)  SNAP_bene_change 
##                            (1)               (2)       
## -------------------------------------------------------
## bbce:post               0.275***          0.083***     
##                          (0.047)           (0.016)     
##                                                        
## -------------------------------------------------------
## Observations              7,030             6,660      
## R2                        0.963             0.028      
## Adjusted R2               0.963             0.016      
## Residual Std. Error 0.230 (df = 6946) 0.173 (df = 6576)
## =======================================================
## Note:                       *p<0.1; **p<0.05; ***p<0.01

Asset test vs no asset test states

Pooled diff-in-diff between states that implemented BBCE with asset tests and states that implemented BBCE without asset tests. State and experiment-by-time fixed effects. No controls.

Pooled diff-in-diff (1) \[\log(\text{SNAP benes #}) = \beta_0 + \beta_1AssetTest_s \times \textbf{1}_{t \geq 0}+\alpha_s + \gamma_{kt} + \epsilon_{kit}\]

Pooled diff-in-diff (2) \[\text{Change in SNAP benes #} = \omega_0 + \omega_1AssetTest_s \times \textbf{1}_{t \geq 0}+\alpha_s + \gamma_{kt} + \epsilon_{kit}\] Compared to states that have or will implement BBCE without asset tests, the asset test states still seem to see an increase in beneficiaries after the implementation of an asset test. Again, quick and dirty with no controls.

asset_experiment <- data.frame(
    'STATE_CD' = character(),
    'year' = as.Date(character()),
    't' = numeric(),
    'post' = numeric(),
    'asset' = numeric(),
    'experiment' = character()
)

for(state in asset_states) {
    
    bbceAsset_counts <- snap_counts_long[snap_counts_long$STATE_CD == state,] %>%
        mutate('asset' = 1)
    
    bbceNoAsset_counts <- snap_counts_BBCE_noasset %>%
        mutate('asset' = 0)
    
    data <- rbind(bbceAsset_counts,bbceNoAsset_counts) %>%
        mutate('experiment' = factor(state))
    
    data$t <- round(as.numeric(difftime(data$year, asset_limit_policies$date[asset_limit_policies$STATE_CD == state], units = 'weeks') / 52))
    data$post <- ifelse(data$t >= 0, 1, 0)
    
    asset_experiment <- rbind(asset_experiment,data)
    
}

asset_felm1 <- felm(log(SNAP_benes) ~ asset:post | STATE_CD + t:experiment|0|STATE_CD,data = asset_experiment)
asset_felm2 <- felm(SNAP_bene_change ~ asset:post | STATE_CD + t:experiment|0|STATE_CD,data = asset_experiment)

stargazer(asset_felm1, 
          asset_felm2,
          type = 'text',
          keep.stat = c('adj.rsq'))
## 
## ============================================
##                   Dependent variable:       
##             --------------------------------
##             log(SNAP_benes) SNAP_bene_change
##                   (1)             (2)       
## --------------------------------------------
## asset:post      0.298**          0.028      
##                 (0.121)         (0.032)     
##                                             
## --------------------------------------------
## Adjusted R2      0.937           0.020      
## ============================================
## Note:            *p<0.1; **p<0.05; ***p<0.01