Design for Failure: Processing Payments with a Background Worker

Processing payments correctly is hard. This is one of the biggest lessons I've learned while writing my various SaaS projects. Stripe does everything they can to make it easy, with quick start guides and great documentation. One thing they really don't cover in the docs is what to do if your connection with their API fails for some reason. Processing payments inside a web request is asking for trouble, and the solution is to run them using a background job.

The Problem

Let's take Stripe's example code:

Stripe.api_key = ENV['STRIPE_API_KEY']

# Get the credit card details submitted by the form
token = params[:stripeToken]

# Create the charge on Stripe's servers - this will charge the user's card
  charge = Stripe::Charge.create(
    :amount => 1000, # amount in cents, again
    :currency => "usd",
    :card => token,
    :description => ""
rescue Stripe::CardError => e
  # The card has been declined

Pretty straight-forward. Using the stripeToken that stripe.js inserted into your form, create a charge object. If this fails due to a CardError, you can safely assume that the customer's card got declined. Behind the scenes, Stripe::Charge makes an https call to Stripe's API. Typically, this completes almost immediately.

But what if it doesn't? The internet between your server and Stripe's could be slow or down. DNS resolution could be failing. There's a million reasons why this code could take awhile. Browsers typically have around a one minute timeout and application servers like Unicorn usually will kill the request after 30 seconds. That's a long time to keep the user waiting just to end up at an error page.

The Solution

The solution is to put the call to Stripe::Charge.create in a background job. This example is going to use a very simple background worker system named Sucker Punch. It runs in the same process as your web request but uses Celluloid to do things in a background thread.

First, let's create a job class:

class StripeCharger
  include SuckerPunch::Worker

  def perform(event)
    ActiveRecord::Base.connection_pool.with_connection do
      token =  event[:token]
      txn = Transaction.find(event[:transaction_id])

        charge = Stripe::Charge.create(
          amount: txn.amount,
          currency: "usd",
          card: token,
        txn.state = 'complete'
        txn.stripe_id =!
      rescue Stripe::Error => e
        txn.state = 'failed'
        txn.error = e.json_body!

Again, pretty straightforward. Sucker Punch will create an instance of your job class and call #perform on it with a hash of values that you pass in to the queue, which we'll get to in a second. We look up a Transaction record, initiate the charge, and capture any errors that happen along the way.

Transaction in this case is a simple ActiveRecord object with just a few attributes, just enough to capture what Stripe gives us:

class Transaction < ActiveRecord::Base
  attr_accessible :stripe_id, :state, :amount, :error, :email

Sucker Punch needs to know about our job class, so let's tell it in an initializer:

SuckerPunch.config do
  queue name: :payments_queue, worker: StripeCharger, workers: 10

Now for the controller that ties it all together:

class TransactionsController < ApplicationController

  def create
    txn =
      amount: 1000,
      email: params[:email],
      state: 'pending'
        token: params[:stripeToken]
      render json: txn.to_json
      render json: {error: txn.error_messages}, status: 422

  def show
    txn = Transaction.find(params[:id])
    raise'not found')
      unless txn

    render json: txn.to_json

The create method creates a new Transaction record, setting it's state to pending. It then queues the transaction to be processed by StripeCharger. The show method simply looks up the transaction and spits back some JSON. On your customer-facing page you'd do something like this:

function doPoll(id){
    $.get('/transactions/' + id, function(data) {
        if (data.state === "complete") {
          window.location = '/thankyou';
        } elsif (data.state === "failed") {
        } else {
          setTimeout(function(){ doPoll(id); }, 500);

Your page will poll /transactions/<id> until the transaction ends in either success or failure. You'd probably want to show a spinner or something to the user while this is happening.

With this setup, you've insulated yourself from problems in your connection to Stripe, your connection to your customer, and everything in between.

This is an excerpt from my guide Mastering Modern Payments: Using Stripe with Rails.

Mastering Modern Payments

Build a Better Payment System

Get a free five part email course all about Stripe and Rails, including the first three chapters of Mastering Modern Payments.

No spam. Unsubscribe at any time.
Pete Keen Portrait Pete Keen has been professional software developer for a decade, building payment systems and other software for companies large and small. He blogs here and at