Hosting: Heroku

We assume that you have the basic knowledge about creating bots using grammY. If you are not ready yet, don’t hesitate to head over to our friendly Guide! 🚀

This tutorial will guide you how to deploy a Telegram bot to Herokuopen in new window by using either webhooks or long polling. We also assume that you have a Heroku account already.


First, install some dependencies:

# Create a project directory.
mkdir grammy-bot
cd grammy-bot
npm init --y

# Install main dependencies.
npm install grammy express

# Install development dependencies.
npm install -D typescript @types/express @types/node

# Create TypeScript config.
npx tsc --init

We will store our TypeScript files inside a folder src, and our compiled files in a folder dist. Create the folders in the project’s root directory. Then, inside folder src, create a new file named bot.ts. Our folder structure should now look like this:

├── node_modules/
├── dist/
├── src/
│   └── bot.ts
├── package.json
├── package-lock.json
└── tsconfig.json

After that, open tsconfig.json and change it to use this configuration:

  "compilerOptions": {
    "target": "ESNEXT",
    "module": "esnext", // changed from commonjs to esnext
    "lib": ["ES2021"],
    "outDir": "./dist/",
    "strict": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  "include": ["src"]


Because the module option above has been set from commonjs to esnext, we have to add "type": "module" to our package.json. Our package.json should now be similar to this:

  "name": "grammy-bot",
  "version": "0.0.1",
  "description": "",
  "main": "dist/app.js",
  "type": "module",  // add property of "type": "module"
  "scripts": {
    "dev-build": "tsc"
  "author": "",
  "license": "ISC",
  "dependencies": {
    "grammy": "^1.2.0",
    "express": "^4.17.1"
  "devDependencies": {
    "typescript": "^4.3.5",
    "@types/express": "^4.17.13",
    "@types/node": "^16.3.1"
  "keywords": []


As mentioned earlier, we have two options for receiving data from Telegram: webhooks and long polling. You can learn more about the both advantages and then decide which ones is suitable in these awesome tips!


If you decide to use long polling instead, you can skip this section and jump down to the section about long polling. 🚀

In short, unlike long polling, webhook do not run continuously for checking incoming messages from Telegram. This will reduce server load and save us a lot of dyno hoursopen in new window, especially when you are using the Eco plan. 😁

Okay, let us continue! Remember we have created bot.ts earlier? We will not dump all the code there, and leave coding the bot up to you. Instead, we are going to make app.ts our main entry point. That means every time Telegram (or anyone else) visits our site, express decides which part of your server will be responsible for handling the request. This is useful when you are deploying both website and bot in the same domain. Also, by splitting codes to different files, it make our code look tidy. ✨

Express and Its Middleware

Now create app.ts inside folder src and write this code inside:

import express from "express";
import { webhookCallback } from "grammy";
import { bot } from "./bot";

const domain = String(process.env.DOMAIN);
const secretPath = String(process.env.BOT_TOKEN);
const app = express();

app.use(`/${secretPath}`, webhookCallback(bot, "express"));

app.listen(Number(process.env.PORT), async () => {
  // Make sure it is `https` not `http`!
  await bot.api.setWebhook(`https://${domain}/${secretPath}`);

Let’s take a look at our code above:

⚡ Optimization (optional)

bot.api.setWebhook at line 14 will always run when Heroku starts your server again. For low traffic bots, this will be for every request. However, we do not need this code to run every time a request is coming. Therefore, we can delete this part completely, and execute the GET only once manually. Open this link on your web browser after deploying our bot:<bot_token>/setWebhook?url=<webhook_url>

Note that some browsers require you to manually encodeopen in new window the webhook_url before passing it. For instance, if we have bot token abcd:1234 and URL, then our link should look like this:

⚡ Optimization (optional)

Use Webhook Reply for more efficiency.

Creating bot.ts

Next step, head over to bot.ts:

import { Bot } from "grammy";

const token = process.env.BOT_TOKEN;
if (!token) throw new Error("BOT_TOKEN is unset");

export const bot = new Bot(token);

bot.command("start", (ctx) => ctx.reply("Hello there!"));
bot.on("message", (ctx) => ctx.reply("Got another message!"));

Good! We have now finished coding our main files. But before we go to the deployment steps, we can optimize our bot a little bit. As usual, this is optional.

⚡ Optimization (optional)

Every time your server starts up, grammY will request information about the botopen in new window from Telegram in order to provide it on the context object under We can set the bot informationopen in new window to prevent excessive getMe calls.

  1. Open this link<bot_token>/getMe in your favorite web browser. Firefoxopen in new window is recommended since it displays json format nicely.
  2. Change our code at line 4 above and fill the value according to the results from getMe:
const token = process.env.BOT_TOKEN;
if (!token) throw new Error("BOT_TOKEN is unset");

export const bot = new Bot(token, {
  botInfo: {
    id: 111111111,
    is_bot: true,
    first_name: "xxxxxxxxx",
    username: "xxxxxxbot",
    can_join_groups: true,
    can_read_all_group_messages: false,
    supports_inline_queries: false,

Cool! It’s time to prepare our deployment environment! Straight to Deployment Section everyone! 💪

Long Polling

Your Script Will Run Continuously When Using Long Polling

Unless you know how to handle this behavior, make sure you have enough dyno hoursopen in new window.

Consider using webhooks? Jump up to the webhooks section. 🚀

Using long polling on your server is not always a bad idea. Sometimes, it is suitable for data gathering bots that don’t need to respond quickly and handle lots of data. If you want to do this once an hour, you can do that easily. That’s something you cannot control with webhooks. If your bot gets flooded with messages, you will see a lot of webhooks requests, however, you can more easily limit the rate of updates to process with long polling.

Creating bot.ts

Let’s open the bot.ts file that we have created earlier. Have it contain these lines of code:

import { Bot } from "grammy";

const token = process.env.BOT_TOKEN;
if (!token) throw new Error("BOT_TOKEN is unset");

const bot = new Bot(token);

  (ctx) => ctx.reply("I'm running on Heroku using long polling!"),


That’s it! We are ready to deploy it. Pretty simple, right? 😃 If you think it is too easy, check out our Deployment Checklist! 🚀


Nope… our Rocket Bot is not ready to launch yet. Complete these stages first!

Compile Files

Run this code in your terminal to compile the TypeScript files to JavaScript:

npx tsc

If it runs successfully and does not print any errors, our compiled files should be in the dist folder with .js extensions.

Set up Procfile

For the time being, Heroku has several types of dynosopen in new window. Two of them are:

  • Web dynos:
    Web dynos are dynos of the “web” process that receive HTTP traffic from routers. This kind of dyno has a timeout of 30 seconds for executing code. Also, it will sleep if there is no request to handle within a 30 minutes period. This type of dyno is quite suitable for webhooks.

  • Worker dynos:
    Worker dynos are typically used for background jobs. It does NOT have a timeout, and will NOT sleep if it does not handle any web requests. It fits long polling.

Create file named Procfile without a file extension in the root directory of our project. For example, Procfile.txt and procfile are not valid. Then write this single line code format:

<dynos type>: <command for executing our main entry file>

For our case it should be:

web: node dist/app.js
worker: node dist/bot.js

Set up Git

We are going to deploy our bot using Git and Heroku Cliopen in new window. Here is the link for the installation:

Assuming that you already have them in your machine, and you have a terminal open in the root of our project’s directory. Now initialize a local git repository by running this code in your terminal:

git init

Next, we need to prevent unnecessary files from reaching our production server, in this case Heroku. Create a file named .gitignore in root of our project’s directory. Then add this list:


Our final folder structure should now look like this:

├── .git/
├── node_modules/
├── dist/
│   ├── bot.js
│   └── app.js
├── src/
│   ├── bot.ts
│   └── app.ts
├── package.json
├── package-lock.json
├── tsconfig.json
├── Procfile
└── .gitignore
├── .git/
├── node_modules/
├── dist/
│   └── bot.js
├── src/
│   └── bot.ts
├── package.json
├── package-lock.json
├── tsconfig.json
├── Procfile
└── .gitignore

Commit files to our git repository:

git add .
git commit -m "My first commit"

Set Up a Heroku Remote

If you have already created Heroku appopen in new window, pass your Existing app’s name in <myApp> below, then run the code. Otherwise, run New app.

heroku create
git remote -v
heroku git:remote -a <myApp>

Deploying Code

Finally, press the red button and liftoff! 🚀

git push heroku main

If it doesn’t work, it’s probably our git branch is not main but master. Press this blue button instead:

git push heroku master