# Fernway: How Fernway stopped losing bookings between the payment and the supplier

https://galley.themes.dev/customers/fernway/

> Fernway moved its booking pipeline from fourteen cron jobs and a Redis queue to Runnel workflows. Failed confirmations dropped from 1.8% to 0.04% in the first quarter.

## The problem

Fernway sells multi-day rail and ferry trips across northern Europe. A single booking touches up to six suppliers: rail operators, ferry lines, small hotels. Most of them confirm through slow, inconsistent APIs, and a few still confirm by email.

The original pipeline was a Redis queue, a worker written in 2019 and fourteen cron jobs on one EC2 instance that "swept" for bookings in odd states. When a supplier timed out after the customer had paid, the booking sat in `payment_captured` until a sweeper noticed it. Sometimes the sweeper retried the whole booking and charged the card again. In summer 2024, 1.8% of bookings needed someone in support to finish them by hand, and refunds for double charges took a full-time person.

"Nobody was sure which cron job did what," says Jonas Ekdahl, Fernway's head of engineering. "We had a wiki page called 'Do not restart the scheduler box on Fridays'."

## What they built

Fernway rewrote the pipeline as a single Runnel workflow, `confirm-booking`, with one step per supplier. Payment capture is its own step, so it runs exactly once however often a later step fails. Each supplier call has its own retry policy, tuned to how that supplier behaves.

```ts title="workflows/confirm-booking.ts"
export const confirmBooking = runnel.workflow({
  name: 'confirm-booking',
  retries: { attempts: 8, backoff: 'exponential', maxDelay: '30m' },
  run: async ({ bookingId }: { bookingId: string }, ctx) => {
    const booking = await ctx.step('load', () => bookings.get(bookingId));
    await ctx.step('capture-payment', () => payments.capture(booking.paymentId));
    for (const leg of booking.legs) {
      await ctx.step(`confirm-${leg.id}`, () => suppliers[leg.supplier].confirm(leg));
    }
    await ctx.step('send-tickets', () => mailer.tickets(booking));
  },
});
```

The email-only suppliers were the awkward part. At first a schedule polled the confirmations mailbox every five minutes. Since Runnel 2.6 they are a `waitForEvent` step instead: the workflow waits up to 48 hours for the inbound-email parser to send `supplier.confirmed`, and pages the on-call engineer through a `run.failed` webhook if it never arrives.

The fourteen cron jobs turned into five Runnel schedules. Nine of them existed only to clean up after the old queue and were deleted. The scheduler box was switched off in January 2025.

The migration ran in parallel for three weeks. New bookings went through Runnel while the old pipeline kept handling bookings already in flight, and the team compared outcomes daily before removing the old code.

## Results

In the first full quarter on Runnel, bookings that needed manual confirmation fell from 1.8% to 0.04%, and there has not been a double charge since the switch. Support now spends the time it used to spend on stuck bookings on trip changes, which customers actually ask about.

The engineering gain is less visible but matters as much. When a supplier has an outage, the on-call engineer opens the dashboard, filters runs by the failing step and sees exactly which bookings are waiting. When the supplier recovers, they retry those runs from that step with one command, and the customers never notice.

Fernway now uses Runnel for supplier price syncs and its nightly settlement report as well, about 2.4 million runs a month on the Scale plan.
