Category: JavaScript

  • The 18-Second Request: Tracing a Latency Bug to a Hidden Retry Loop

    The 18-Second Request: Tracing a Latency Bug to a Hidden Retry Loop

    For three days, our API gateway service was hitting 504 Gateway Timeouts on requests that should have been instantaneous. The issue was blocking QA and frontend work, and the symptoms were maddeningly vague.

    This is a breakdown of how we debugged it, the two false leads that cost us days, and the two lines of code that actually fixed it.

    The Symptoms

    • Requests to microservice-iam (user data) were hitting the 5-second proxy limit and dying.
    • Requests to microservice-app (application logic) were very slow, but often succeeding.

    The False Leads

    1. “It’s a Resource Issue”

    When a microservices environment starts crawling, the immediate suspicion is resource exhaustion. Checking htop on the develop server showed high memory usage and a lot of context switching.

    I first checked if we were accidentally throttling ourselves. I reviewed the Docker configurations to see if we had set aggressive cpus or memory limits on the containers. We hadn’t.

    Convincingly pointing to hardware limitations, we decided to scale up. I migrated the environment to a significantly larger instance, added more vCPUs and doubled the RAM. I expected the latency to drop immediately.

    The result: Zero change. The timeouts persisted. This confirmed it wasn’t a load problem.

    2. The “Slow but Successful” Red Herring

    This was the most damaging distraction. Because microservice-app requests were completing (albeit slowly) while microservice-iam requests were timing out, I assumed the infrastructure (Redis/Postgres) was fine. If Redis or Postgres was broken, everything should fail, right?

    This led me to scrub the microservice-iam codebase for hours, looking for inefficient loops or bad logic specific to that service. I was looking for a bug in the service code, not the shared infrastructure.

    The Investigation

    We needed to isolate the bottleneck.

    1. Checking the Network

    I bypassed the application layer and ran a raw Node.js TCP script to handshake directly with the IAM service’s health port.

    • Result: < 5ms response. The network was fine.

    2. Checking the Database

    I grabbed the exact query the application was running and ran EXPLAIN ANALYZE on Postgres.

    • Result: 0.341 ms execution time. The database was fast.

    The Bottleneck: Exponential Latency

    If the network and DB were instant, the time had to be disappearing in the application logic.

    The confusing part was microservice-app. It uses the exact same caching library and logic as microservice-iam (versioned keys, scoped lookups), so it should have been failing too.

    The difference turned out to be the service’s role. microservice-app only handles application-specific logic. microservice-iam handles user data and authentication for the entire platform. Every request that hits the gateway, regardless of destination, triggers an auth check that uses microservice-iam.

    It was getting hammered with exponentially higher request volume than microservice-app. While microservice-app was suffering from the same latency bug, it was just enough “under the radar” to scrape by with slow successes. microservice-iam, sitting in the critical path of everything, was being pushed over the edge.

    I wrote a script to simulate the Redis call sequence directly from the container:

    • Call 1: 200ms
    • Call 2: 400ms
    • Call 3: 800ms
    • Call 4: 1.6s
    • Call 5: 3.2s

    By the time metadata checks were finished, over 18 seconds had passed.

    The Root Cause

    The issue was a conflict between our environment configuration and the Redis driver’s default behavior.

    1. Configuration Drift: The environment configuration for develop had a REDIS_PASSWORD set. However, the Redis server on develop was running in a mode that did not require authentication.
    2. Driver Behavior: We were using Keyv, which defaults to the node-redis driver. When this driver detects an auth mismatch (sending a password when none is needed), it doesn’t just fail or ignore it—it enters a retry loop with an exponential back-off.

    Because microservice-iam made a number of sequential calls, that back-off penalty was applied n times, compounding until the request timed out.

    The Fix

    We didn’t need to fix the server config (though we should); we needed the application to be resilient to this kind of drift.

    We switched the underlying driver to ioredis. Unlike node-redis, ioredis handles unnecessary passwords gracefully: it logs a warning but executes the command immediately without a delay.

    // Before: Implicitly using node-redis
    return createKeyv(redisUri);
    
    // After: Explicitly injecting ioredis
    const redis = createRedisInstance(config);
    return createKeyv(redis);

    After deploying this change, request durations dropped from 18 seconds to 35 milliseconds.

    Takeaway

    Inconsistent failure modes are often just volume issues in disguise. The fact that one service worked while another failed didn’t mean the infrastructure was healthy, it just meant one service was hitting the “poison” button more often than the other.

  • Block Editor Best Practices: WordPress Meetup Saarland

    Block Editor Best Practices: WordPress Meetup Saarland

    WordPress Meetups are always one of the best ways to meet like-minded people, teach people about WordPress, have amazing discussions, and bring more people to the wonderful community. I participated in the 3rd WordPress Meetup in Saarland on 5th September, this time as a speaker. I talked about probably the most controversial feature of WordPress, the Block Editor (also known as Gutenberg). The topic was mainly about Block Editor Best Practices – for users, designers, and developers.

    Recently, we revamped rtCamp‘s website. It was a mammoth task – custom blocks, patterns, templates, and what not. During the process, we discovered some pain points with the block editor and also figured out some best practices. This talk focused on the outcomes of the project.

    During the talk, I realized how much context-switching I needed to do. One of the members in the audience was an artist and had just installed WordPress. They wanted to know the powers of Gutenberg. On the other hand, one of the members of the audience, Fredric Döll has founded Digitenser Consulting, wanted to learn more about how to efficiently create for and with the block editor for their clients.

    Gutenberg is a very powerful tool but it is often misunderstood. It is also important to understand that for some sites, Gutenberg may not make sense. But for the sites where editorial experience is key, it is imperative that the website is planned really well. A robust plan helps with feasible designs which lead to a better overall developer experience.

    The next WordPress Meetup in Saarland will happen on 23.01.2025. If you’re around Saarbrücken at that time, feel free to drop your emails in the comment.

    Note: In the presentation, we discussed negative margins. Gutenberg does have support for negative margins; however, our discussion was more oriented towards user experience. Currrently, negative margins in Gutenberg, have a little UX situation.

    Block Editor Best Practices – Deck

    You can access the presentation slides (Google Slides) this link.

  • sshc: a simple command-line SSH manager

    sshc: a simple command-line SSH manager

    I usually have to SSH into a lot of servers; personal servers and work-related. Remembering their hostnames or IPs has always been a task. I have tried a few apps like Termius, but they often come with their own set of drawbacks. Many of these solutions are paid, which can be a significant investment if you’re just looking for a simple way to manage your connections. Furthermore, they often require extensive setup and configuration, which can be time-consuming when you just want to quickly connect to your servers.

    What I really needed was a lightweight, free solution that I could set up quickly and start using right away. I wanted something that would help me organize my SSH connections without the overhead of a full-featured (and often overpriced) application.

    That’s why I decided to create my own solution: a simple npm package that addresses these exact pain points. My goal was to develop a tool that’s easy to install, requires minimal setup, and gets you connected to your servers with minimal fuss.

    In this post, I’ll introduce you to this package and show you how it can simplify your SSH workflow without breaking the bank or requiring a considerable effort to set up.

    Installing simple-sshc

    Installing simple-sshc requires node version 14.0.0 or above to work. If you have not already, you can install node and npm here.

    Once you have node and npm setup, run this command to install simple-sshc globally:

    $ npm install -g simple-sshc

    You can verify the installation using:

    $ sshc version                                                  
    sshc version 1.0.1

    Connecting to a server

    You can SSH into your saved hosts by simply invoking the sshc command:

    Features

    Adding connections

    Easily add new SSH connections to your list with a simple interactive prompt:

    $ sshc add
    Enter the label: myserver 
    Username: user
    Hostname (IP address): 192.168.1.100

    The CLI guides you through the process, ensuring you don’t miss any crucial details. Once added, your connection is saved and ready for quick access.

    List all connections

    View all your saved connections at a glance:

    $ sshc list
    sshc list command and its output on shell.

    Modify existing connections

    Need to update a connection? You can use sshc modify to do that.

    $ sshc modify
    ? Select the connection to modify: myserver
    ? New username: newuser
    ? New hostname (IP address): 192.168.1.101

    Remove connections

    Cleaning up is just as easy:

    $ sshc remove 
    ? Select the connection you wish to remove: oldserver 
    ? Are you sure you want to remove this connection? Yes

    GitHub

    You can download the source code from GitHub: https://github.com/danish17/sshc/

  • Protected Routes in Next.js

    Protected Routes in Next.js

    If you are building a SaaS website that has awesome features or a simple website with minimal user functionality, you know Authentication and Authorization are crucial (difference between authentication and authorization). Protected Routes in Next.js help us ensure that unauthenticated users are not able to see routes/pages intended for logged in (authenticated) users. There are a few approaches to to implement Protected Routes in Next.js, i.e., enforce authentication for a page/route.

    But, first of all – why do we love Next.js? Next.js is arguably the most popular and go-to React framework. It packs some cool stuff including file-based routing, incremental static regeneration, and internationalization (i18n). With Next.js 13, we have got even more power – layouts and Turbopack!

    You might be wondering – why bother protecting routes? We are building a SaaS product with a Next.js frontend and Nest.js backend. We have implemented authentication in the backend but we also need to ensure that forced browsing* is prevented and User Experience is enriched. Actual authentication logic should reside inside our back-end logic. All the API calls must be appropriately authenticated. In our app, whenever there is an unauthenticated request it returns 401 Unauthorized. An ACL is also in place so whenever user requests a resource they do not have access to, the backend returns 403 Forbidden.

    Now, let’s create a route protection flow in Next.js.:
    If a user requests a protected route (something that requires authentication), we redirect them to the login page.
    We should not prevent access if a route is public (supposed to be viewed regardless of the users’ authentication state) like a login page.

    At the end, the goals are simple: safety and security.

    Jodi Rell

    Using RouteGuard

    The concept of a RouteGuard is simple. It is a wrapper component that checks whether the user has access to the requested page on every route change. To track the access, we use one states: authorized. If authorized is true, then the user may see the page or else user is redirected to the login page. To update the state, we have a function authCheck() which prevents access (sets authorized to false) if the user does not have access and the page is not public (e.g. landing page, login page, sign-up page).

    Logic of RouteGuard to implement Protected Routes in Next.js.
    Working of RouteGuard
    import { Flex, Spinner } from '@chakra-ui/react';
    import { useRouter } from 'next/router';
    import publicPaths from '../data/publicPaths';
    import { useAppDispatch, useAppSelector } from '../hooks/storeHooks';
    import { setRedirectLink } from '../redux/AuthSlice';
    import {
      JSXElementConstructor,
      ReactElement,
      useEffect,
      useState,
    } from 'react';
    
    const RouteGuard = (props: {
      children: ReactElement<unknown, string | JSXElementConstructor<unknown>>;
    }) => {
      const { children } = props;
    
      const router = useRouter();
      const [authorized, setAuthorized] = useState(false);
      const user = useAppSelector((state) => state.auth);
    
      const dispatch = useAppDispatch();
    
      useEffect(() => {
        const authCheck = () => {
          if (
            !user.isLoggedIn &&
            !publicPaths.includes(router.asPath.split('?')[0])
          ) {
            setAuthorized(false);
            dispatch(setRedirectLink({ goto: router.asPath }));
            void router.push({
              pathname: '/login',
            });
          } else {
            setAuthorized(true);
          }
        };
    
        authCheck();
    
        const preventAccess = () => setAuthorized(false);
    
        router.events.on('routeChangeStart', preventAccess);
        router.events.on('routeChangeComplete', authCheck);
    
        return () => {
          router.events.off('routeChangeStart', preventAccess);
          router.events.off('routeChangeComplete', authCheck);
        };
      }, [dispatch, router, router.events, user]);
    
      return authorized ? (
        children
      ) : (
        <Flex h="100vh" w="100vw" justifyContent="center" alignItems="center">
          <Spinner size="xl" />
        </Flex>
      );
    };
    
    export default RouteGuard;

    Note: we are using Redux to store the user’s data; authentication is out of the scope of this blog post.

    Implementing the Middleware

    In a scenario where the users’ session expires while they are on a protected page, they will not be able to fetch newer resources (or perform any actions for that matter). That’s, once again, really bad UX. We cannot expect a user to refresh, so we need a way to let them know that their session is no longer valid.

    To implement the same, we will use another awesome Next.js feature – Middlewares! In few words, a middleware sits between your server and the frontend. Middleware allows you to run code before a request is completed, then based on the incoming request, you can modify the response by rewriting, redirecting, modifying the request or response headers, or responding directly.

    After session expiration, whenever the user makes a request, it will result in 401 Unauthorized. We have implemented a middleware which listens to the response for each request that is being made from the frontend; if the request results in 401 Unauthorized, we dispatch the same action, i.e. log out the user and redirect to the login page.

    Working of the unauthenticatedInterceptor middleware to implement Protected Routes in Next.js.
    Working of the middleware
    import {
      MiddlewareAPI,
      isRejectedWithValue,
      Middleware,
    } from '@reduxjs/toolkit';
    import { logout } from '../redux/AuthSlice';
    import { store } from '../redux/store';
    
    interface ActionType {
      type: string;
      payload: { status: number };
      meta: {};
      error: {};
    }
    
    const unauthenticatedInterceptor: Middleware =
      (_api: MiddlewareAPI) =>
      (next: (action: ActionType) => unknown) =>
      (action: ActionType) => {
        if (isRejectedWithValue(action)) {
          if (action.payload.status === 401 || action.payload.status === 403) {
            console.error('MIDDLEWARE: Unauthorized/Unauthenticated [Invalid token]');
            store.dispatch(logout());
          }
        }
    
        return next(action);
      };
    
    export default unauthenticatedInterceptor;

    Suggested Readings