Chaindog: A Serverless SMS App for Theme Park Queue Time Notifications

Casey Johnson
8 min readMay 18, 2023

--

We’ve all done it. An idea hits you in the middle of the night. The Aha! moment. You sit down with your IDE of choice. Suddenly, you don’t know exactly how, but you find yourself weeks into a passion project that you didn’t have time for in the first place.

For me, that was Chaindog.

https://github.com/caseyjohnsonwv/chaindog

A real interaction with Chaindog.

I. Conception

Anyone will tell you I am a huge roller coaster nerd — enough that I took a pay cut and moved to Orlando, Florida to work for a theme park. And that’s after getting a roller coaster tattooed on my arm. When I’m not in Orlando, I’m visiting 40–50 amusement parks each year, both riding and photographing their roller coasters.

Instagram: @caseyjohnsonphotos

Large-scale virtual queues and skip-the-line passes were pioneered by the FastPass system at Disney World. Today, these exist in some form at almost every major park worldwide: Flash Pass, Quick Queue, Fast Lane, etc. While Disney’s original FastPass system was free, all of today’s counterparts seek to upsell guests on the premise of shorter lines — and therefore a more enjoyable in-park experience.

Growing up in West Virginia, we didn’t have much to go around. My mother saved money on her school teacher salary for years so we could visit Disney World once in June of 2009. I remember being so grateful that the FastPass system existed so we could hide from the central Florida heat instead of waiting in line.

Fast forward to 2021. I am out of college and fiscally solvent, so for the first time ever, I’m able to travel to a multitude of amusement parks…

And every one of them is charging to skip the lines! In some places, it’s $250, and tons of people are paying for it! I think back to being 12 at Disney World using the FastPass system. I think back to being 19 in college, grocery shopping on $7 a week and having the power shut off at my apartment — at the time, that was only 3 years prior. Standing in a 2 hour line for Steel Vengeance at Cedar Point on a scorching August afternoon, I thought to myself:

There has got to be a better way.

Over the winter that followed, the first iteration of this project was born: Firewatch. Chaindog is a continuation / redesign of Firewatch, my original proof of concept for a queue time notification system.

II. Succinct Problem Statement

If you skipped over that entire section, here’s the TL;DR —

  1. How can I, as a theme park guest, wait in shorter lines than other guests without spending money on skip-the-line upsells?
  2. How can I, as an avid park-goer, achieve this same outcome at any park, not just ones I’m familiar with?

This can be solved in large part by general theme park knowledge and the free data source Queue Times, which publishes wait times from theme parks’ own wait time tracking systems. But as a guest, I don’t want to spend all day refreshing a website or checking an app — I want the app to tell me when the line is short.

Enter several weeks of furious late-night programming sessions…

One of Chaindog’s first successful watches.

III. Designing Chaindog

The term chain dog in roller coaster engineering refers to the ratcheting anti-rollback system found on lift-hill-driven rides. The chain dog exists to ensure the train never rolls backwards down the lift hill in the event it disengages the lift chain.

The blue anti-rollback devices are sometimes referred to as “chain dogs.”

In this case, my project Chaindog ensures you never have to wait in a long line, even in the event the park is busy. I accomplished this very simply: by building an SMS wrapper that allows me to query Queue Times data in text messages.

At a high level, Chaindog is two halves: Data Ingestion and SMS Handling.

The beauty of this decoupled architecture is the ability to share production data across environments. In the diagram below, there are only two links between the two halves. Because we are using Terraform, simply setting upstream_env_name in a .tfvars file will plug a new SMS handling module into an existing Data Ingestion module. This both reduces our impact on Queue Times and reduces spend in our AWS account.

Serverless architectures always look more complicated than they really are.

IV. We Have to Go Deeper

Data Ingestion

  1. Our upstream data source Queue Times refreshes every 5 minutes, so the Ingestion Cronjob (an EventBridge rule) triggers every 5 minutes.
  2. The Park ID Lambda retrieves a JSON file containing a unique ID for each supported park. The entire file is dumped to S3, and each park’s unique ID is individually pushed to the Park ID Queue.
  3. The Wait Time Lambda fans out to five concurrent executions, retrieving wait time data for all 100+ supported parks in 15–20 seconds. This data is returned in JSON form, where it is dumped directly to S3 under wait-times/<park_id>.json.
  4. As each JSON file arrives in S3, the bucket pushes an event to the Notification Topic, which triggers processing on the SMS handling side.

SMS Handling — Outbound

  1. The Notification Lambda is invoked by the upstream Notification Topic. This happens once for each theme park when its data refreshes. This Lambda function queries the DynamoDB Watch Table for active user alerts at this particular park. If the user’s wait time condition has been met, a message is pushed to the SMS Topic and the Dynamo record is deleted.
  2. The SMS Lambda is invoked by the SMS Topic and sends an outbound HTTP POST request to Twilio. The POST payload simply contains the source / target phone numbers plus the text message to be sent; Twilio handles the rest.

SMS Handling — Inbound

  1. When a user texts a specific phone number provisioned by Twilio, it can be configured to send a POST request webhook to an API endpoint. We are receiving that data with AWS API Gateway.
  2. The API endpoint passes the data to the Watch Lambda, which simply transcribes the data to a Dynamo record — after ensuring the wait time is currently long enough to warrant watching. (If the user requests a 45 minute wait and the line is currently 15, we can just tell them now!)

Other Design Notes

It’s worth pointing out that there is no database in the data ingestion module — only object storage in S3. This was a conscious design choice for simplicity, as structured and semi-structured data in S3 can be queried directly using a flavor of SQL. With the scale of this application, the cost difference between this and a NoSQL DB is negligible.

# looking up a park's unique ID
query = f"""
SELECT *
FROM s3object[*][*].parks[*] AS s
WHERE s.name = '{park_name_sanitized}'
"""
park_record = query_s3(
query,
'parks.json',
bucket_name,
)[0]

# looking up a ride's wait time
query= f"""
SELECT *
FROM s3object[*].waits.lands[*].rides[*] AS s
WHERE LOWER(s.name) = '{ride_name_sanitized}'
LIMIT 1
"""
ride_record = query_s3(
query,
wait_time_json_s3_key,
bucket_name,
)[0]

V. The Verdict

Every once in a while, you build something for fun and it works way better than you expected. Chaindog has turned out to be the single most useful side project I have ever pursued. I’ve even iterated on it, adding handling for more advanced cases, like a user modifying their watch after creation:

Even a difficult guest can still find Chaindog helpful!

Because of the serverless architecture and extremely low spend, I can leave this application running 24/7 for only a few cents per month. Whenever I’m visiting a park supported by Queue Times, Chaindog is ready and waiting.

There are certainly cases when it falls short, though. For example, recognizing the park name and ride name in the user’s message is left to fuzzy matching, which tends to make mistakes.

There is also the possibility that a ride’s line never gets short enough:

Above all, Chaindog falls victim to the principle of GIGO. Queue Times is a fantastic data source, but I am only ingesting data about individual rides’ wait times, not parks themselves. We assume that the park is closed when all rides are closed, but the wait time data is not perfect — some rides never get marked as closed. There is more data available to ingest at the park level that would significantly improve our accuracy.

VI. Future Work

As mentioned above, there is more data on Queue Times about each park, including their hours. This could be useful for us.

I’ve also explored growing Chaindog into a full-fledged chatbot. This was developed before the recent fervor of ChatGPT and OpenAI, meaning NLP and conversational messages were not at all on my mind. There could also be consideration given to personalization, such as caching and reusing the last ride and/or park the user asked about.

For that mater, I have a variety of bugs and enhancements tracked in GitHub as issues that have never been developed. These would implement a variety of features, such as: requiring a Yes/No authorization to delete a user’s watch, or validating incoming webhook authenticity using Twilio’s signature header.

VII. Finding Me

Chaindog was the project I touted proudly in my interviews for my current position at Universal Orlando Resort. I loved every bit of building this application, and the wait time notification system in our mobile app is obviously one of my favorite features.

Connect with me on LinkedIn! I also photograph roller coasters — the biggest winners make it to Instagram.

Tuesday, Wednesday, and Thursday, you can find me in line for Jurassic World VelociCoaster after 5:00pm :)

--

--