Why are all calls from Retool to our OpenAPI resource taking ~50s to complete?

We are setting up a new OpenAPI Resource to access a service we've developed using FastAPI, deployed to AWS Lambda. Authentication is provided by Auth0 + Google SSO, and we are retrieving valid access, refresh, and ID tokens without issue.

However, every call to our API via this Retool resource using this access token is taking roughly 50s to complete, even our verification endpoint (which accepts no input payload and produces a minimal response w/o accessing any additional resource) which resolves in a few milliseconds outside Retool. This can be seen in the screenshot below:

Analyzing our AWS logs for these calls confirms that 1. the API calls on our service are being resolved in an expected amount of time (<1s), and 2. the endpoints are being called by Retool at the very end of that ~50s timeline. The following screenshot shows the AWS logs for the "Check authorization status" logs shown in the previous screenshot. Note that the entire request takes less than a second, and that it is received at the very end 2:49:35 (PST), which was the same second the authentication check ended:

When the calls do complete on Retool's side, they are indeed successful and produce the desired outcome. But they're all taking far too long to resolve.

While monitoring the browser dev tools, the "Check authorization status" is happening completely server-side, in Retool's service, so it's impossible to determine what is consuming the remainder of the request time:

To test that this isn't inherently slow, I wrote a simple test client using NodeJS that tests the following:

  • An OAuth2 handshake against Auth0, including Google SSO login
  • Verification of the resulting access token against our validation endpoint
  • Download and process of the OpenAPI spec from our server using SwaggerClient

This local script completes (modulo the actual process of signing in w/Google) in under 2s.

require('dotenv').config();
const express = require('express');
const axios = require('axios');
const SwaggerClient = require('swagger-client');
const querystring = require('querystring');
const { stat } = require('fs');

const app = express();
const PORT = 3000;

// OAuth URLs & Credentials from .env
const {
    CLIENT_ID,
    CLIENT_SECRET,
    AUTHORIZATION_URL,
    TOKEN_URL,
    REDIRECT_URI,
    OPENAPI_SPEC_URL,
    VERIFICATION_URL,
} = process.env;

console.log("OAuth URLs & Credentials:", {
    CLIENT_ID,
    CLIENT_SECRET,
    AUTHORIZATION_URL,
    TOKEN_URL,
    REDIRECT_URI,
    OPENAPI_SPEC_URL,
    VERIFICATION_URL,
});

// Step 1: Redirect user to OAuth 2.0 provider for authorization
app.get('/login', (req, res) => {
    // Get the current timestamp as the state in seconds as the state
    const state = Date.now();
    const authUrl = `${AUTHORIZATION_URL}?` + querystring.stringify({
        response_type: 'code',
        client_id: CLIENT_ID,
        redirect_uri: REDIRECT_URI,
        scope: 'openid profile email offline_access',
        state: state,
    });
    res.redirect(authUrl);
});

// Step 2: Handle OAuth 2.0 Callback and Exchange Code for Tokens
app.get('/auth/callback', async (req, res) => {
    const callbackStart = Date.now();
    const { code } = req.query;

    if (!code) {
        return res.status(400).send("Authorization code missing.");
    }

    try {
        // Exchange code for tokens
        console.log("Exchanging code for tokens...", code);
        const tokenResponse = await axios.post(TOKEN_URL, querystring.stringify({
            grant_type: 'authorization_code',
            code,
            redirect_uri: REDIRECT_URI,
            client_id: CLIENT_ID,
            client_secret: CLIENT_SECRET,
        }), {
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        });
        const timeToGetToken = Date.now() - callbackStart;
        console.log("Time to get token:", timeToGetToken);

        // Check if the response was successful
        console.log("Token Response:", tokenResponse.data);
        if (tokenResponse.status !== 200) {
            throw new Error("Failed to exchange code for tokens.");
        }
        const {
            access_token: accessToken,
            refresh_token: refreshToken,
            id_token: idToken,
            state,
        } = tokenResponse.data;

        console.log("Access Token:", accessToken);
        console.log("Refresh Token:", refreshToken);
        console.log("ID Token:", idToken);
        console.log("State:", state);

        // Step 3: Fetch OpenAPI Spec using swagger-client
        const client = await SwaggerClient({
            url: OPENAPI_SPEC_URL
        });

        // Extract available endpoints
        const endpoints = Object.keys(client.spec.paths);
        const timeToParseSpec = Date.now() - callbackStart - timeToGetToken;
        console.log("Time to parse spec:", timeToParseSpec);

        // GET the verification endpoint with the bearer token to verify the token
        const verificationResponse = await axios.get(VERIFICATION_URL, {
            headers: { Authorization: `Bearer ${accessToken}` }
        });
        const verification = verificationResponse.data
        const timeToVerifyToken = Date.now() - callbackStart - timeToGetToken - timeToParseSpec;
        console.log("Time to verify token:", timeToVerifyToken);

        // Log the verification response JSON
        console.log("Verification Response:", verification);

        // Elapsed time
        const elapsedTime = Date.now() - callbackStart;
        console.log("Elapsed Time:", elapsedTime);

        // Display tokens and API endpoints
        res.json({ accessToken, refreshToken, idToken, endpoints, verification });

    } catch (error) {
        console.error("Error:", error.response ? error.response.data : error.message);
        res.status(500).send("Authentication failed.");
    }
});

// Start Server
app.listen(PORT, () => {
    console.log(`OAuth client running at http://localhost:${PORT}`);
    console.log(`Login at: http://localhost:${PORT}/login`);
});

I'd like to understand why this resource is taking so long to complete every call, if there's a configuration I've gotten wrong, and if there's something that can be done to make this closer to the performance of API calls against this service outside of a Retool context.

To answer my own question, based on other forum posts, I'm guessing it's because this is an OpenAPI 3.1 specification, and Retool is failing to properly parse our API spec because it only supports up to OpenAPI 3.0.

I've created a REST API resource for the same endpoint, and it is not having any of these performance issues. This isn't ideal, since we lose out on the ability to know all available endpoints, parameters, types and response payloads, but at least it unblocks our ability to use Retool. That said, it would be nice to know if OpenAPI 3.1 support is on the roadmap, and when we might expect this support to ship.

Followup on my previous message:

I've confirmed that the slowness this Resource was experiencing was due to the OpenAPI 3.1 specification, which Retool appears to not be compatible with. I added an endpoint to our service that returns an OpenAPI 3.0-version of our API, and updated the Retool resource config to use that endpoint, and everything now works as expected, without any inordinate delay.

I understand that implementing full OpenAPI 3.1 support may not be trivial, but given that the spec has been out for over 4 years, it seems Retool could at least document which versions of the OpenAPI spec it is compatible with. It would've saved me a lot of time debugging this issue.

Hey @shaug,
So sorry you had to spend so much time debugging this issue. I have let our docs folks know about this and asked them to add something let lets folks know which versions that Retool supports.

Thanks for your feedback. :grinning: