DEV Community

Cover image for Experiment: Ruby on Vercel (an odyssey)
Carlos Ghan
Carlos Ghan

Posted on

Experiment: Ruby on Vercel (an odyssey)

(cover image generated with Google Gemini)

What / Why

As the title says, this is just an experiment with running "serverless" Ruby on Vercel, not a comparison with other app-hosting options (I don't even provide any metrics on costs/performance, etc.).

For the sake of brevity, what this post mentions barely scratches the surface of the platform, and I encourage you to explore further and judge by yourself.

Besides that, I thought it was an interesting challenge due to the really scarce documentation.
(I don't even remember how I stumbled upon Vercel's Ruby runtime existence... πŸ€”)

Bird's-eye overview of Vercel

Putting it simply (and risking it to read like an old tired salesman, or even an AI), Vercel is a cloud platform for building, deploying, and scaling modern web applications, plus all the "agentic" rigmarole.
One of its flagship concepts is "fluid compute", which I find quite "attractive" (though not super original/innovative).

Under the fluid compute model, the deployable units are called "Functions" (similar to AWS Lambdas), and their main benefit is supposed to be reducing cold-starts. The documentation describes it as: "Run server-side code without managing servers".

The platform is mostly focused on Next.js (a React framework created and maintained by them) and favors the JavaScript/TypeScript ecosystem (it welcomes Vue and other frontend frameworks), though it has support for Python and other languages like Go, Rust, and (surprisingly!) Ruby.

In fact, I'll be reproducing (and adapting) this very same example but using Ruby instead of Python + FastAPI, on the free tier.

It's worth mentioning, that if a language you like it's not officially supported, apparently you can go for a community-contributed runtime, or build it yourself by using the Runtime API .

Other benefits mentioned here (which I'm not going to cover in this post):

  • Zero configuration out of the box
  • Optimized concurrency
  • Dynamic scaling
  • Automatic cold start optimizations
  • Cross-region and availability zone failover
  • Error isolation

Fun (read "unfortunate") fact: shortly after I decided to sketch up this writing (months ago), the company was in the news spotlight due to a security breach)

Ruby runtime

It's in beta stage, so it's not recommended for production or anything serious. (Plus, there's no guarantee it will still be available in the future; a very low adoption may not justify the effort of keeping the lights on)

At the time of this writing (August 2026), only Ruby 3.3.x is supported (a bummer, considering that 4.0.x it's being out for a while now).
If you specify a higher version at Gemfile, the deployment fails with an error: "Found Gemfile with invalid Ruby version"

As for the dependencies, the documentation states there's support for:

  • auto-installing gems specified in the Gemfile
  • or, alternatively, they can be vendored with the command bundler install --deployment (e.g. gems with native extensions).

(The latter, I didn't try.)

File hierarchy

File hierarchy feels a bit too-rigid to me, because all of your Ruby code must live under an api folder (which in turn may include subfolders) at your project's root. This implies that your URLs will start with /api (unless you mangle them somehow; see the appendix section at the end of this post for middleware).

api
 β”œβ”€β”€ index.rb # Handles "/api"
 β”œβ”€β”€ foo.rb   # Handles "/api/foo"
 β”œβ”€β”€ users
 β”‚     β”œβ”€β”€ index.rb # Handles "/api/users"
 β”‚     β”œβ”€β”€ bar.rb   # Handles "/api/users/bar"
Enter fullscreen mode Exit fullscreen mode

(!!) IMPORTANT (!!)
To save you from some headaches:

  • .gitignore has no effect on Vercel - only .vercelignore does. A vendor/ gitignored will still ship on CLI deploys.
  • .vercelignore does not affect vercel build (command used to "compile" the project), only vercel deploy uploads.

But wait... there's more.
A couple of "safety checkpoints" to avoid nasty gotchas: due to the way the runtime works, you need to define a vercel.json file with the following content:

{ "outputDirectory": "public" }
Enter fullscreen mode Exit fullscreen mode

otherwise, any Ruby code files outside the top-level api folder, will be treated as plain-text, returning the source-code they contain as the response (not fun at all).

Also, public/ must stay non-empty; an empty output directory silently falls back to serving the whole repo.
Any dummy public/index.html file is ok (maybe you can redirect the browser to your API, or it could even be empty).

Request handlers

Basically, you have two different ways of implementing the request-handling code:

  • A file defining a constant named Handler that stores a proc with |request, response| as block arguments:
    • request is an instance of WEBrick::HTTPRequest
    • response is an instance of WEBrick::HTTPResponse
  • A file with a class named Handler that inherits from WEBrick::HTTPServlet::AbstractServlet

(You can mix and match: have some files using the first option, and others using the second one; as long as they define a Handler constant, they will do the trick)

Creating the project

To me, the simplest way to create a project it's importing an existing GitHub repo:

Exploring other more "programmatic" ways is left as an exercise to the reader :)

Gimme the codez!

(Do not perform code-review on these examples... they're just quick and dirty to have a proof-of concept πŸ™‚)

.ruby-version

3.3.12
Enter fullscreen mode Exit fullscreen mode

vercel.json

{
  "outputDirectory": "public"
}

Enter fullscreen mode Exit fullscreen mode

.vercelignore

vendor/
bin/
.ruby-lsp/
.env*
Enter fullscreen mode Exit fullscreen mode

Gemfile

source "https://rubygems.org"

gem "webrick"
Enter fullscreen mode Exit fullscreen mode

api/weather/index.rb

# frozen_string_literal: true

require "open-uri"
require "json"
require "webrick"

# In a real-life scenario, these must live in environment variables.
GEO_API_DOMAIN = "geocoding-api.open-meteo.com"
GEO_API_SEARCH_PATH = "/v1/search"

WEATHER_API_HOST = "api.open-meteo.com"
WEATHER_API_PATH = "/v1/forecast"

HTTP_OK = WEBrick::HTTPStatus::OK.code
HTTP_BAD_REQUEST = WEBrick::HTTPStatus::BadRequest.code
HTTP_NOT_FOUND = WEBrick::HTTPStatus::NotFound.code
HTTP_BAD_GATEWAY = WEBrick::HTTPStatus::BadGateway.code

Handler = Proc.new do |request, response|
  city = request.query["city"].to_s
  units = request.query["units"].to_s
  units = "metric" if units.empty?

  response["Content-Type"] = "application/json"

  if city.empty?
    response.status = HTTP_BAD_REQUEST
    response.body = JSON.dump({ error: "Missing required query parameter: city" })
    next
  end

  begin
    result = get_weather(city:, units:)
  rescue ArgumentError => e
    response.status = HTTP_BAD_REQUEST
    response.body = JSON.dump({ error: e.message })
    next
  rescue StandardError => e
    response.status = HTTP_BAD_GATEWAY
    response.body = JSON.dump({ error: e.message })
    next
  end

  if result.nil?
    response.status = HTTP_NOT_FOUND
    response.body = JSON.dump({ error: "No results for city: #{city}" })
  else
    response.status = HTTP_OK
    response.body = JSON.dump(result)
  end
end

# Get current weather for a city.
#
# @param city [String] City name (e.g., "London", "New York")
# @param units [String] Temperature units - "metric" (Celsius) or "imperial" (Fahrenheit)
#
# @return [Hash, nil] Weather data including temperature, humidity, wind speed, and location
#   info, or nil when the city cannot be geocoded
def get_weather(city:, units: :metric)
  units_sym = units.to_sym
  raise ArgumentError, "Invalid units: #{units.inspect}" unless [:metric, :imperial].include?(units_sym)

  geo_params = {
    name: city,
    count: 1,
    language: :en,
    format: :json
  }

  geo_url = URI::HTTPS.build(
    host: GEO_API_DOMAIN,
    path: GEO_API_SEARCH_PATH,
    query: URI.encode_www_form(geo_params)
  )
  geo_response = URI.open(geo_url)
  response_http_code = geo_response.status[0].to_i
  raise StandardError, "Geocoding API request failed: #{geo_response.status}" if response_http_code != HTTP_OK

  # The geocoding API omits "results" entirely when nothing matches.
  location = JSON.parse(geo_response.read).fetch("results", []).first
  return nil if location.nil?

  weather_params = {
    latitude: location["latitude"],
    longitude: location["longitude"],
    current: "temperature_2m,relative_humidity_2m,apparent_temperature,wind_speed_10m",
    timezone: "auto"
  }
  if units_sym == :imperial
    weather_params.merge!({
      temperature_unit: "fahrenheit",
      wind_speed_unit: "mph"
    })
  end

  weather_url = URI::HTTPS.build(
    host: WEATHER_API_HOST,
    path: WEATHER_API_PATH,
    query: URI.encode_www_form(weather_params)
  )
  weather_response = URI.open(weather_url)
  response_http_code = weather_response.status[0].to_i
  raise StandardError, "Weather API request failed: #{weather_response.status}" if response_http_code != HTTP_OK

  weather_data = JSON.parse(weather_response.read)
  {
    city: location["name"],
    country: location["country"],
    latitude: location["latitude"],
    longitude: location["longitude"],
    units: units_sym,
    current: weather_data["current"]
  }
end
Enter fullscreen mode Exit fullscreen mode

Running locally

First, make sure you install Vercel's CLI:

# Change the following command to your preferred way
npm i -g vercel
Enter fullscreen mode Exit fullscreen mode

then, run the server:

# At your project's root:
vercel dev
Enter fullscreen mode Exit fullscreen mode

and, finally, make your first local request (SPOILER ALERT: it won't work):

curl "http://localhost:3000/api/weather?city=london"

# The output:
#   Runtime "ruby3.3" is not implemented
Enter fullscreen mode Exit fullscreen mode

Oh, snap! what a showstopper... even when the vercel dev command succeeds, any request to the API endpoint fails with that disgusting error.

I didn't spend any time trying to debug that, and immediately resorted to Claude Code for finding an answer. And here's the thing:

  • The local vercel "harness" doesn't emulate Vercel's routing/rewrites, .env.local loading, or platform headers - only the function itself.

A poor-man's workaround (via a Claude Code generated script) allows you to do a bit of testing, but it still doesn't fully emulate Vercel rewrites, headers, etc.)
Put the following code in a bin/dev script, and use that in place of the vercel dev command:

#!/usr/bin/env ruby
# frozen_string_literal: true

# Local dev server for the Ruby functions in api/.
#
# `vercel dev` cannot run these: @vercel/ruby's dev server only handles Rack
# (.ru) entrypoints and returns null for .rb, so the CLI falls back to
# @vercel/fun, which registers no Ruby runtimes at all and fails with
#   Runtime "ruby3.3" is not implemented
#
# This mounts the Handler proc the same way Vercel's runtime does in
# production (vc_init.rb: `server.mount_proc '/', Handler`), so request
# handling matches what a deployment sees. What it does NOT emulate is the
# Vercel layer around the function: routing/rewrites, .env.local loading,
# and platform headers.
#
#   bin/dev
#   curl "http://localhost:3000/api/weather?city=London&units=imperial"

require "webrick"

require_relative "../api/weather/index"

host = ENV.fetch("HOST", "127.0.0.1")
port = Integer(ENV.fetch("PORT", "3000"))

server = WEBrick::HTTPServer.new(BindAddress: host, Port: port)
server.mount_proc "/api/weather", Handler

trap("INT") { server.shutdown }
trap("TERM") { server.shutdown }

server.start
Enter fullscreen mode Exit fullscreen mode

And now, execute the script:

./bin/dev
# [2026-08-01 21:05:02] INFO WEBrick 1.9.2 [2026-08-01 21:05:02] INFO ruby 3.3.1 (2024-04-23) [arm64-darwin25]
# [2026-08-01 21:05:02] INFO WEBrick::HTTPServer#start: pid=73221 port=3000
Enter fullscreen mode Exit fullscreen mode

in another terminal:

curl "http://localhost:3000/api/weather?city=London&units=imperial"
# {"city":"London","country":"United Kingdom","latitude":51.50853,"longitude":-0.12574,"units":"imperial","current":{"time":"2026-08-02T01:00","interval":900,"temperature_2m":71.7,"relative_humidity_2m":34,"apparent_temperature":67.7,"wind_speed_10m":4.7}}
Enter fullscreen mode Exit fullscreen mode

That looks good to me.

Deployment

You can install the official vercel CLI tool in a similar fashion to other platforms (you can see logs, deploy, etc.), but just pushing to the origin repo will trigger a deploy (like "good ol' Heroku"). And, at least in my tinkering, it was blazing fast.

Quoting verbatim the example I mentioned:

  • Push the changes to your remote repository or run theΒ vercelΒ cli command
  • Vercel will create a new preview deployment for your to test
  • Merge to main branch to deploy to Production

Then, at your Vercel account's Deployments section you should see something like the following:

And you can test directly in your browser or using curl/wget/etc.:

Appendix

Middleware

Unless I overlooked something, if you want to implement a middleware, you cannot write it in Ruby... you must use JavaScript or TypeScript (a file named middleware.js or middleware.ts respectively)

This is a toy example:

// It requires the "vercel" package to be installed
import { next } from "@vercel/functions"

export default function middleware(request: Request) {
    const url = new URL(request.url);

    if (!url.pathname.startsWith("/api")) {
        return new Response(null, {status: 418}) // I'm a teapot
    }

    return next()
}
Enter fullscreen mode Exit fullscreen mode

Build - sneak peek under the hood

When you execute the vercel build command, a .vercel/output folder is generated with the build's output, and among the files, you can find a kind of WEBrick wrapper for each of your "functions":

β”œβ”€β”€ functions
β”‚Β Β  └── api
β”‚Β Β      └── weather
β”‚Β Β          └── index.func
β”‚Β Β              β”œβ”€β”€ vc__handler__ruby.rb
β”‚Β Β              └── vc__utils__ruby.rb
Enter fullscreen mode Exit fullscreen mode

An excerpt from from vc__handler__ruby.rb

require 'tmpdir'
require 'webrick'
require 'net/http'
require 'base64'
require 'json'
require_relative 'vc__utils__ruby'

$entrypoint = 'api/weather/index'

ENV['RAILS_ENV'] ||= 'production'
ENV['RACK_ENV'] ||= 'production'
ENV['RAILS_LOG_TO_STDOUT'] ||= '1'


$service_route_prefix = resolve_service_route_prefix

def rack_handler(httpMethod, path, body, headers, script_name = '')
  require 'rack'

  app, _ = Rack::Builder.parse_file($entrypoint)
  server = Rack::MockRequest.new app
Enter fullscreen mode Exit fullscreen mode

Closing thoughts

It is worth and fun trying out the Ruby runtime in order to learn something new, or prototype a hosted API in a "non-traditional" way, but I would not recommend it (at least for now) for anything else.

Side note (and this is just my personal view): I disliked Vercel panels and dashboards' UX, they felt a bit clumsy and hard to navigate; needs some love. That being said, I'm eager to see how it evolves in the future.

Top comments (0)