The Payroll Bug That Lived in a Timezone: How a Silent Failure Changed Our Sync Architecture

Akshay ThakurAkshay Thakur
3 min read

TL;DR

We had a subtle but critical payroll bug: deductions were getting silently dropped due to timezone mismatches between us (Rippling) and a partner system (Employee Navigator). What looked like a simple effective_date == pst_now() check turned into a case study in why real-world systems need to be resilient to time, contracts, and tacit assumptions. We solved it with a ScheduledDeductions model and a batch job system. Here's the story.


Background

At Rippling, our 3P Ben Admin team integrates with external benefit platforms like Employee Navigator to sync deduction data with our internal payroll engine. The catch? Rippling Payroll doesn't support future-dated deductions.

So we had an agreement with our partner: they'll send us deduction updates only on the day those deductions are meant to take effect. Our logic was simple:

if deduction.effective_date == pst_now().date():
    sync_to_payroll(deduction)

And for a while — it worked. Until it didn’t.


The Bug: When "Today" Isn’t the Same Day

We started noticing missing deductions. No crashes, no retries, no alerts — just... missing deductions in payroll.

After digging through logs, we found a recurring pattern:

  • The partner sent the data at 1:55 AM CST on 2025-04-14.

  • But pst_now() on our end still read 2025-04-13, 11:55 PM PST.

  • So our equality check failed, and we skipped syncing the deduction.

Yup — we were off by one hour, but it cost us payroll data.

The partner was technically sending data on the right date. But in their timezone, not ours.


The Fix: ScheduledDeductions + Batch Processing

We realized that our implementation was too brittle. Timezone edge cases shouldn’t cause silent data loss. So we moved to an architecture built for this.

🔧 Enter: ScheduledDeductions

Instead of syncing deductions immediately, we now:

  1. Store every incoming deduction update in a ScheduledDeductions model

  2. Add metadata like effective_time, companyId, employeeId, etc.

  3. Set the status to "Scheduled"

🕒 Batch Job FTW

Every 6 hours, a cron job runs:

  • Fetches deductions where effective_time <= pst_now() - timedelta(hours=6) and status = 'Scheduled'

  • Applies them in batch to EmployeeDeductionType

  • Marks success as COMPLETED, and collects failures into an errors bucket

🧠 Why This Works

  • Handles future-dated deductions reliably

  • Decouples from timezone-sensitive assumptions

  • Adds observability: we now know how many deductions succeeded or failed

  • Scales: 1,000+ deductions processed in seconds


Architecture Diagram

sequenceDiagram
    participant V as 3P Vendor
    participant B as Ben Admin Service
    participant S as ScheduledDeductions DB
    participant C as Cron Job (Every 6 hours)
    participant E as EmployeeDeductionType
    participant P as Payroll Engine

    V->>B: Send deduction request
    B->>S: Save as Scheduled with effective_time and status = "Scheduled"

    Note over C: Cron runs every 6 hours

    C->>S: Query deductions where effective_time ≤ now - 6h and status = "Scheduled"
    S-->>C: Return eligible deductions

    loop For each eligible deduction
        C->>E: Update EmployeeDeductionType
        alt Success
            C->>S: Mark status as "COMPLETED"
        else Failure
            C->>S: Add to error_details
        end
    end

    alt If all succeeded
        E->>P: Changes picked up in next payrun
    else If any errors
        C->>B: Trigger alert with errors
    end

Lessons Learned

  1. Equality checks on time are dangerous. Always normalize or buffer.

  2. Tacit contracts break easily. Make assumptions explicit.

  3. Timezones are where bugs go to hide.

  4. Batch systems are more resilient than real-time syncs when dealing with external inputs.


Final Thoughts

This wasn’t the flashiest bug I’ve fixed, but it was the most quietly devastating. And solving it felt like a turning point — not just technically, but in how I thought about system design.

We didn't just fix a bug. We turned a fragile sync system into a robust, scalable pipeline.


Hope this helped someone think a bit deeper about time, contracts, and systems. If you've ever chased a timezone bug at 2 AM, you’re not alone.

0
Subscribe to my newsletter

Read articles from Akshay Thakur directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Akshay Thakur
Akshay Thakur

Developer from India.