Ordering Pre-Tax 401(k) and Section 125 Deductions Before the Tax Basis

When a payroll engine subtracts every “pre-tax” deduction from a single flat taxable-wage variable, it silently under-withholds Social Security and Medicare tax, because a Section 125 cafeteria-plan election and a 401(k) elective deferral do not reduce the same tax bases. This ordering problem is part of the Deduction Mapping Rules topic within the Payroll Calculation Engines & Validation Rules framework, and it isolates the two deduction types into their own deterministic stage so each one only ever touches the tax bases the Internal Revenue Code actually assigns to it.

Problem Framing

Most payroll engines model “pre-tax deductions” as one bucket subtracted once from gross pay to produce one taxable-wage figure. That model is wrong for two specific deduction types that are frequently mapped side by side in the same deduction table:

  • Section 125 cafeteria-plan elections — health, dental, vision, FSA, and (in a cafeteria-plan-eligible arrangement) HSA contributions under IRC § 125 — are excluded from “wages” for federal income tax (FIT) withholding and for FICA under IRC § 3121(a)(5). One election reduces three tax bases at once: FIT, Social Security, and Medicare.
  • 401(k) elective deferrals under IRC § 401(k) reduce taxable income for FIT purposes, but IRC § 3121(v)(1) explicitly treats deferred amounts as FICA wages at the time of deferral. A 401(k) deferral reduces exactly one tax base — FIT — and leaves Social Security and Medicare wages untouched.

An engine that applies both deductions to a single combined taxable-wage variable either over-reduces FICA wages (understating Social Security and Medicare tax, which surfaces as a discrepancy against Enforcing the FICA Social Security Wage-Base Cap and requires a Form 941-X correction) or under-reduces FIT wages (over-withholding income tax and misstating W-2 Box 1). The crux this page isolates: Section 125 reduces all three bases; 401(k) reduces only one. Getting the order and the base assignment right is not a stylistic choice — it is the difference between a correct paycheck and a quarter-end reconciliation failure.

Ordering also matters for a second, less obvious reason. Most 401(k) plan documents define “plan compensation” — the base against which a percentage-of-pay election is calculated — as compensation net of cafeteria-plan reductions, consistent with how IRC § 414(s) permits compensation definitions to exclude § 125 amounts. If the engine computes the 401(k) election as a percentage of gross wages instead of gross wages after the Section 125 reduction, the resulting dollar deferral no longer matches the plan’s compensation definition, which distorts ADP/ACP nondiscrimination testing even when no statutory dollar limit is exceeded. Section 125 must therefore be applied first, both to correctly seed the FICA bases and to correctly seed the base the 401(k) percentage is computed against.

Deterministic order of pre-tax deduction application Gross wages enter a Section 125 cafeteria-plan reduction that lowers the FIT, Social Security, and Medicare tax bases identically. The result then splits into three tracks: the FIT track passes through a 401(k) elective-deferral reduction that lowers only that base, while the Social Security and Medicare tracks pass through unchanged. The three tracks converge into three distinct final taxable-wage figures, showing that FIT taxable wages end lower than Social Security and Medicare taxable wages, which remain equal to each other. Gross wages one Decimal per pay period Section 125 cafeteria (IRC § 125) health · dental · vision · FSA · HSA reduces FIT + Social Security + Medicare 401(k) elective deferral IRC § 401(k) / § 402(g) cap reduces FIT only Social Security track unaffected by 401(k) Medicare track unaffected by 401(k) FIT taxable wages gross − 125 − 401(k) the lowest of the three bases Social Security wages gross − 125 (401(k) excluded) subject to the annual wage cap Medicare wages gross − 125 (401(k) excluded) equal to Social Security wages
Section 125 lowers all three tax bases together; the 401(k) deferral that follows lowers only the FIT base, so Social Security and Medicare wages stay equal and higher than FIT wages.

Prerequisites & Data Requirements

The resolver that orders these two deduction types needs the following fields, already routed and classified by the Deduction Mapping Rules resolver. Every monetary field is Decimal; the engine must reject a float at the boundary rather than coerce it.

Field Type Precondition
gross_wages Decimal Total cash wages for the pay period, already net of any non-taxable reimbursements.
section125_reduction Decimal Flat per-period cafeteria-plan election; 0 if the employee has no active election.
k401_election_rate Decimal Percentage of post-125 compensation, expressed as a Decimal fraction (e.g. Decimal("0.06")).
employee_age int As of December 31 of the plan year, used to resolve the applicable catch-up tier.
ytd_k401_contributions Decimal Elective deferrals already contributed this plan year, across all periods with this employer.
plan_year_limits lookup The § 402(g) base limit and catch-up amounts for the plan year, loaded from a versioned table — never hardcoded, since the IRS republishes them annually.

The plan-year limits table used in the examples below (illustrative figures for the 2026 plan year, structured exactly as the engine should load them):

Limit Amount
§ 402(g) elective-deferral base limit Decimal("24500.00")
Catch-up, age 50–59 or 64+ Decimal("8000.00")
Enhanced catch-up, age 60–63 (SECURE 2.0) Decimal("11250.00")

Two preconditions sit outside the function signature:

  1. Section 125 must already be resolved and active for the period. This routine does not decide whether a cafeteria-plan election applies — that classification comes from the upstream Deduction Mapping Rules resolver. This routine only orders the application of an already-classified pre_tax election against the correct bases.
  2. ytd_k401_contributions is scoped to the current employer. If the employee changed jobs mid-year, the prior employer’s contributions do not appear here; cross-employer aggregation for the § 402(g) limit is the employee’s responsibility at filing, not this engine’s.

Step-by-Step Implementation

Step 1 — Pin the plan-year limits and model the inputs. Load limits from a versioned table rather than hardcoding them, and model records as frozen dataclasses so an activated period’s inputs cannot mutate mid-calculation.

from dataclasses import dataclass
from decimal import Decimal
import logging

logger = logging.getLogger("payroll.deductions.pretax_order")

ELECTIVE_DEFERRAL_LIMIT_2026 = Decimal("24500.00")
CATCHUP_STANDARD_2026 = Decimal("8000.00")       # ages 50-59 and 64+
CATCHUP_ENHANCED_2026 = Decimal("11250.00")      # ages 60-63, SECURE 2.0


@dataclass(frozen=True)
class PayPeriodInputs:
    employee_id: str
    gross_wages: Decimal
    section125_reduction: Decimal
    k401_election_rate: Decimal
    employee_age: int
    ytd_k401_contributions: Decimal


@dataclass(frozen=True)
class TaxBases:
    fit_taxable_wages: Decimal
    ss_taxable_wages: Decimal
    medicare_taxable_wages: Decimal
    k401_contribution: Decimal
    k401_capped: bool

Step 2 — Apply Section 125 first, against all three bases at once. The cafeteria-plan reduction is the only deduction in this pipeline that touches Social Security and Medicare wages, so it must resolve before anything else runs. Expected output for gross_wages=5000.00, section125_reduction=350.00: wages_after_125 = Decimal("4650.00").

def apply_section125(inputs: PayPeriodInputs) -> Decimal:
    """Reduce gross wages by the Section 125 election once, for all three bases."""
    if inputs.section125_reduction < Decimal("0"):
        raise ValueError(
            f"emp={inputs.employee_id} section125_reduction cannot be negative"
        )
    wages_after_125 = inputs.gross_wages - inputs.section125_reduction
    logger.info(
        "section125_applied emp=%s gross=%s reduction=%s wages_after_125=%s",
        inputs.employee_id, inputs.gross_wages,
        inputs.section125_reduction, wages_after_125,
    )
    return wages_after_125

Step 3 — Resolve the applicable § 402(g) ceiling for the employee’s age. SECURE 2.0’s enhanced catch-up applies only to ages 60 through 63; age 64 and older revert to the standard catch-up. Expected output: age 55Decimal("32500.00"); age 61Decimal("35750.00"); age 64Decimal("32500.00").

def resolve_402g_limit(employee_age: int) -> Decimal:
    """Return the total elective-deferral ceiling for the employee's age tier."""
    if employee_age >= 60 and employee_age <= 63:
        return ELECTIVE_DEFERRAL_LIMIT_2026 + CATCHUP_ENHANCED_2026
    if employee_age >= 50:
        return ELECTIVE_DEFERRAL_LIMIT_2026 + CATCHUP_STANDARD_2026
    return ELECTIVE_DEFERRAL_LIMIT_2026

Step 4 — Compute the 401(k) deferral against post-125 compensation and cap it at the remaining § 402(g) room. The election rate applies to wages_after_125, not gross, and the dollar result is capped by whatever YTD room remains — never by the plan compensation alone. Expected output for wages_after_125=4650.00, k401_election_rate=0.08, ytd_k401_contributions=32450.00, employee_age=55 (limit 32500.00): raw election 372.00, remaining room 50.00, so k401_contribution = Decimal("50.00") and k401_capped = True.

def compute_k401_contribution(
    wages_after_125: Decimal, election_rate: Decimal,
    employee_age: int, ytd_k401_contributions: Decimal, employee_id: str,
) -> tuple[Decimal, bool]:
    """Compute the 401(k) deferral against post-125 wages, capped by § 402(g)."""
    limit = resolve_402g_limit(employee_age)
    remaining_room = limit - ytd_k401_contributions
    if remaining_room < Decimal("0"):
        remaining_room = Decimal("0")

    raw_election = (wages_after_125 * election_rate).quantize(Decimal("0.01"))
    capped = raw_election > remaining_room
    contribution = remaining_room if capped else raw_election

    logger.info(
        "k401_computed emp=%s wages_after_125=%s rate=%s raw=%s "
        "limit=%s ytd=%s contribution=%s capped=%s",
        employee_id, wages_after_125, election_rate, raw_election,
        limit, ytd_k401_contributions, contribution, capped,
    )
    return contribution, capped

Step 5 — Assemble the three distinct tax bases in one call. Section 125 reduces fit, ss, and medicare together; the 401(k) contribution reduces fit only. Expected output for the running example: fit_taxable_wages = Decimal("4278.00"), ss_taxable_wages = medicare_taxable_wages = Decimal("4650.00").

def compute_pretax_bases(inputs: PayPeriodInputs) -> TaxBases:
    """Order Section 125 before 401(k) and assign each to its correct tax bases."""
    wages_after_125 = apply_section125(inputs)
    k401_contribution, capped = compute_k401_contribution(
        wages_after_125, inputs.k401_election_rate,
        inputs.employee_age, inputs.ytd_k401_contributions, inputs.employee_id,
    )
    fit_taxable_wages = wages_after_125 - k401_contribution
    ss_taxable_wages = wages_after_125
    medicare_taxable_wages = wages_after_125

    logger.info(
        "pretax_order_final emp=%s fit=%s ss=%s medicare=%s k401=%s capped=%s",
        inputs.employee_id, fit_taxable_wages, ss_taxable_wages,
        medicare_taxable_wages, k401_contribution, capped,
    )
    return TaxBases(
        fit_taxable_wages, ss_taxable_wages, medicare_taxable_wages,
        k401_contribution, capped,
    )


inputs = PayPeriodInputs(
    employee_id="E-70213",
    gross_wages=Decimal("5000.00"),
    section125_reduction=Decimal("350.00"),
    k401_election_rate=Decimal("0.08"),
    employee_age=55,
    ytd_k401_contributions=Decimal("32450.00"),
)
result = compute_pretax_bases(inputs)
# result.fit_taxable_wages == Decimal("4600.00")  (4650.00 - 50.00 capped contribution)
# result.ss_taxable_wages == result.medicare_taxable_wages == Decimal("4650.00")
# result.k401_contribution == Decimal("50.00"); result.k401_capped is True

Verification

Confirm correctness against the specific divergence this page isolates, not just aggregate withholding totals:

  1. Section 125 reduces all three bases equally. With section125_reduction=350.00 and k401_election_rate=0, assert ss_taxable_wages == medicare_taxable_wages == gross_wages - 350.00 and that fit_taxable_wages equals the same value, since no 401(k) reduction applies yet.
  2. 401(k) reduces FIT only. Holding section125_reduction fixed, vary k401_election_rate and assert ss_taxable_wages and medicare_taxable_wages never change while fit_taxable_wages decreases by exactly the computed k401_contribution.
  3. § 402(g) cap at the exact boundary. Set ytd_k401_contributions equal to the resolved limit and assert k401_contribution == Decimal("0.00") and k401_capped is True; set it one cent below the limit and assert the contribution equals exactly that one cent of remaining room.
  4. Catch-up tier boundaries. Assert age 59 resolves to the standard 8000.00 catch-up, age 60 resolves to the enhanced 11250.00 catch-up, age 63 still resolves to 11250.00, and age 64 reverts to 8000.00 — the enhanced tier is a closed four-year window, not “50 and up.”
  5. Percentage computed against post-125 wages, not gross. With gross_wages=5000.00, section125_reduction=350.00, and k401_election_rate=0.08, assert the raw election is Decimal("372.00") (4650.00 * 0.08), never Decimal("400.00") (5000.00 * 0.08).
  6. Decimal determinism. Run the same PayPeriodInputs three times and assert byte-identical TaxBases output; any drift indicates a float leaked into k401_election_rate or a limits lookup.

Failure Modes

  • 401(k) percentage computed against gross wages instead of post-125 compensation. Root cause: the engine reads gross_wages directly for the percentage election instead of the Section 125 output, so the deferral no longer matches the plan document’s compensation definition under IRC § 414(s). Remediation: always compute k401_election_rate * wages_after_125, never k401_election_rate * gross_wages; add a boundary test asserting the two differ whenever section125_reduction > 0.
  • 401(k) contribution subtracted from the Social Security or Medicare base. Root cause: a single combined “taxable wages” variable absorbs both deductions instead of three separately tracked bases, silently understating FICA wages. Remediation: track fit_taxable_wages, ss_taxable_wages, and medicare_taxable_wages as three distinct fields from the start; a discrepancy here requires a Form 941-X and a W-2c to correct understated Social Security and Medicare wages in Boxes 3 and 5, cross-checked against Enforcing the FICA Social Security Wage-Base Cap.
  • Missing cross-employer § 402(g) aggregation after a mid-year job change. Root cause: ytd_k401_contributions resets to zero for the new employer and the engine has no visibility into deferrals already made at the prior employer, so an excess deferral goes undetected until the employee’s personal tax filing. Remediation: flag employees who report prior-employer 401(k) contributions during onboarding for manual excess-deferral tracking, and route the case for a corrective distribution before the April 15 deadline that follows the deferral year under IRC § 402(g)(2), since an uncorrected excess deferral is taxed twice — once when deferred and again when distributed.

Frequently Asked Questions

Why does a Section 125 election reduce FICA wages but a 401(k) deferral does not?

The Internal Revenue Code treats the two differently by design. IRC § 3121(a)(5) excludes qualifying cafeteria-plan amounts from the statutory definition of FICA wages entirely, so a Section 125 election never enters the Social Security or Medicare base. IRC § 3121(v)(1) does the opposite for elective deferrals: it explicitly treats amounts deferred under a 401(k) arrangement as FICA wages at the moment of deferral, even though those same amounts are excluded from income for FIT purposes. One statute excludes a base; the other explicitly includes it.

How does the SECURE 2.0 enhanced catch-up for ages 60–63 change the § 402(g) ceiling?

For employees who are age 60, 61, 62, or 63 as of December 31 of the plan year, the enhanced catch-up amount replaces the standard catch-up rather than stacking on top of it. In the illustrative 2026 figures used above, that means a ceiling of 24500.00 + 11250.00 for that four-year window, while ages 50 through 59 and ages 64 and older use the standard 24500.00 + 8000.00 ceiling. An engine that applies the enhanced amount to every employee 50 and older will overstate the § 402(g) room for anyone 64 or above.

Does the order of applying Section 125 before 401(k) change net pay, or only the intermediate tax bases?

It changes both, but for different reasons. Net pay changes because a percentage-based 401(k) election computed against post-125 compensation produces a smaller dollar deferral than the same rate applied to gross wages, which is the deferral amount most plan documents actually intend. The tax bases change independently of that: regardless of the dollar amount deferred, Social Security and Medicare wages are unaffected by the 401(k) step in either ordering, while FIT wages are only correct when Section 125 has already been subtracted before the 401(k) reduction is applied.

What happens if 401(k) is calculated as a percentage of gross instead of post-125 compensation?

The resulting deferral no longer matches the compensation definition most plan documents use, which is typically wages net of cafeteria-plan reductions under the flexibility IRC § 414(s) gives plan sponsors. This does not necessarily breach the statutory § 402(g) dollar ceiling, but it does distort the deferral relative to the plan’s own formula, which can throw off ADP/ACP nondiscrimination testing and produce a contribution amount that reconciles incorrectly against the benefits carrier’s own election records.