AWS Builder Center

AWS AppSync: Building Real-Time Apps Without the Headache

A practical guide to building real-time applications with AWS AppSync and GraphQL, featuring a complete todo app with live subscriptions, authentication, and multi-user collaboration—no WebSocket servers required.

AI Engineer | AWS Community Builder
Building real-time applications used to be a pain. You'd spend weeks setting up WebSocket servers, managing connections, handling reconnections, and then—oh joy—figuring out how to scale the whole thing when your app actually gets users.
I've been there. We all have.
That's where AWS AppSync comes in, and honestly? It's kind of a game-changer. Let me walk you through what it is, why you might want to use it, and whether it's the right fit for your project.

Table of Contents


What Actually Is AWS AppSync?

Okay, so here's the deal. AWS AppSync is basically a managed GraphQL service that handles real-time data for you.
You know how everyone's obsessed with GraphQL these days? Instead of making 5 different API calls to get user data, their posts, comments, and likes, you just ask for everything you need in one go. That's GraphQL.
Now add real-time updates to that. When something changes, everyone who's subscribed gets notified instantly. No polling. No "refresh every 5 seconds." Just... instant updates.
And the best part? AWS manages all the infrastructure. You don't run servers. You don't manage WebSocket connections. You just write your schema and resolvers, and AppSync handles the rest.
Here's what you get:
  • Serverless - Zero servers to manage (seriously, none)
  • Real-time subscriptions - WebSockets handled for you
  • Multiple data sources - DynamoDB, Lambda, RDS, HTTP APIs, whatever
  • Built-in auth - Cognito, IAM, API keys, custom authorizers
  • Pay per request - No idle server costs

Think of It Like This

Let me give you an analogy that actually makes sense.
Imagine you're at a restaurant. In the old REST API world, you'd need to:
  1. Call one waiter to get your appetizer from the kitchen
  2. Call another waiter to get your drink from the bar
  3. Call a third waiter to get dessert from the pastry station
  4. Keep asking "Is my food ready yet?" every few minutes
Exhausting, right?
With GraphQL and AppSync, you have one smart waiter who:
  • Takes your complete order at once
  • Coordinates with all the stations
  • Brings everything together in one trip
  • Rings a bell when your food is ready (no need to keep asking)
That waiter is AppSync. The kitchen, bar, and pastry station? Those are your data sources (DynamoDB, Lambda, RDS, etc.).
You just place your order (write your query), and AppSync figures out the rest.

The Architecture: How It All Fits Together

Let me show you how this actually works under the hood.

High-Level Architecture

Real-Time Subscription Flow

Here's what happens when someone subscribes to real-time updates:

Data Resolution Flow

This is how AppSync resolves a complex query across multiple data sources:

What Can You Actually Do With It?

Alright, let's talk features. But not the boring marketing stuff—the things you'll actually use.

1. GraphQL Queries (Get Exactly What You Need)

No more over-fetching. No more making 10 API calls. Just ask for what you want:
1
2
3
4
5
6
7
8
9
10
query GetUserProfile {
user(id: "123") {
name
email
posts {
title
likes
}
}
}

2. Real-Time Subscriptions (The Magic Part)

This is where it gets cool. Users get updates the moment something changes:
1
2
3
4
5
6
7
8
subscription OnNewMessage {
onCreateMessage {
id
content
author
timestamp
}
}

3. Connect to Anything

AppSync talks to pretty much any data source:
  • DynamoDB - Your NoSQL database
  • Lambda - Custom business logic
  • RDS/Aurora - Traditional SQL databases
  • OpenSearch - Full-text search
  • HTTP APIs - External services
  • EventBridge - Event-driven stuff

4. Offline Support

Your mobile app works offline, and when it reconnects, AppSync syncs everything. Conflict resolution included.

5. Caching Built-In

Server-side caching means faster responses and less database load. Just turn it on.

6. Security That Actually Works

Pick your auth method:
  • API Keys (for public APIs)
  • IAM (for AWS services)
  • Cognito (for user authentication)
  • Custom Lambda authorizers (for anything else)
  • Plus AWS WAF for extra protection

7. Merge Multiple APIs

Got microservices with their own GraphQL APIs? Merge them into one endpoint. Your frontend will thank you.

Real Talk: When Should You Use This?

Let me be straight with you about when AppSync makes sense.

The Problems It Solves

Without AppSync, you're dealing with:
  • Setting up and managing WebSocket servers
  • Writing tons of boilerplate to connect multiple data sources
  • Building your own auth system (again)
  • Polling for updates like it's 2010
  • Scaling infrastructure when traffic spikes
  • Debugging connection issues at 2 AM
With AppSync:
You just... don't. It's all handled. You write your schema, connect your data sources, and you're done.

When It Actually Makes Sense

Use AppSync if you're building:
  • Chat apps - Messages need to appear instantly
  • Collaborative tools - Multiple users editing the same thing
  • Social feeds - Likes, comments, posts updating live
  • IoT dashboards - Sensor data streaming in real-time
  • Gaming leaderboards - Scores updating as players compete
  • E-commerce - Live inventory, order tracking
  • Financial apps - Stock prices, transaction alerts
  • Sports apps - Live scores and play-by-play
Basically, if users expect to see updates without refreshing, AppSync is your friend.

Let's Build Something Real

Enough theory. Let's build a real-time todo app. Nothing fancy, just enough to show you how it works.
📦 Full Source Code: Check out the complete working example on GitHub  with deployment instructions.

What We're Building

A collaborative todo app where:
  • Multiple users can create, update, and delete todos
  • Changes appear instantly across all connected clients
  • Users authenticate with AWS Cognito
  • Everything syncs in real-time via GraphQL subscriptions

The GraphQL Schema

First, define what your data looks like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
type Todo {
id: ID!
title: String!
description: String
completed: Boolean!
priority: Priority!
owner: String!
createdAt: AWSDateTime!
updatedAt: AWSDateTime!
version: Int!
}

enum Priority {
LOW
MEDIUM
HIGH
}

type Query {
getTodo(id: ID!): Todo
listMyTodos: [Todo]
}

type Mutation {
createTodo(input: CreateTodoInput!): Todo
updateTodo(input: UpdateTodoInput!): Todo
toggleTodo(id: ID!): Todo
deleteTodo(id: ID!): Todo
}

type Subscription {
onCreateTodo(owner: String): Todo
@aws_subscribe(mutations: ["createTodo"])
onUpdateTodo(owner: String): Todo
@aws_subscribe(mutations: ["updateTodo", "toggleTodo"])
onDeleteTodo(owner: String): Todo
@aws_subscribe(mutations: ["deleteTodo"])
}

input CreateTodoInput {
title: String!
description: String
priority: Priority!
}

input UpdateTodoInput {
id: ID!
title: String
description: String
priority: Priority
completed: Boolean
version: Int!
}

The Resolver (Where the Magic Happens)
This is the code that actually saves your todo to DynamoDB:

import { util } from '@aws-appsync/utils';

export function request(ctx) {
const id = util.autoId();
const now = util.time.nowISO8601();

return {
operation: 'PutItem',
key: util.dynamodb.toMapValues({ id }),
attributeValues: util.dynamodb.toMapValues({
title: ctx.args.input.title,
description: ctx.args.input.description || '',
priority: ctx.args.input.priority,
completed: false,
owner: ctx.identity.username,
createdAt: now,
updatedAt: now,
version: 1
})
};
}

export function response(ctx) {
return ctx.result;
}

The Frontend (React + Next.js)

import { useState, useEffect } from 'react';
import { generateClient } from 'aws-amplify/api';
import { listMyTodos, createTodo, toggleTodo, deleteTodo,
onCreateTodo, onUpdateTodo, onDeleteTodo } from '@/lib/graphql';

const client = generateClient();

export default function TodoList() {
const [todos, setTodos] = useState([]);
const [title, setTitle] = useState('');
const [priority, setPriority] = useState('MEDIUM');

useEffect(() => {
// Fetch initial todos
fetchTodos();

// Set up real-time subscriptions
const createSub = client.graphql({ query: onCreateTodo }).subscribe({
next: ({ data }) => {
if (data?.onCreateTodo) {
setTodos(prev => [...prev, data.onCreateTodo]);
}
}
});

const updateSub = client.graphql({ query: onUpdateTodo }).subscribe({
next: ({ data }) => {
if (data?.onUpdateTodo) {
setTodos(prev => prev.map(todo =>
todo.id === data.onUpdateTodo.id ? { ...todo, ...data.onUpdateTodo } : todo
));
}
}
});

const deleteSub = client.graphql({ query: onDeleteTodo }).subscribe({
next: ({ data }) => {
if (data?.onDeleteTodo) {
setTodos(prev => prev.filter(todo => todo.id !== data.onDeleteTodo.id));
}
}
});

return () => {
createSub.unsubscribe();
updateSub.unsubscribe();
deleteSub.unsubscribe();
};
}, []);

const fetchTodos = async () => {
const result = await client.graphql({ query: listMyTodos });
setTodos(result.data.listMyTodos || []);
};

const handleCreate = async (e) => {
e.preventDefault();
if (!title.trim()) return;

await client.graphql({
query: createTodo,
variables: { input: { title, priority } }
});

setTitle('');
};

const handleToggle = async (id) => {
// Optimistic update
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));

try {
await client.graphql({ query: toggleTodo, variables: { id } });
} catch (error) {
// Revert on error
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
}
};

const handleDelete = async (id) => {
await client.graphql({ query: deleteTodo, variables: { id } });
};

return (
<div className="max-w-2xl mx-auto p-6">
<h1 className="text-3xl font-bold mb-8">Real-Time Todos</h1>

<form onSubmit={handleCreate} className="mb-8 flex gap-2">
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="What needs to be done?"
className="flex-1 px-4 py-2 border rounded-lg"
/>
<select
value={priority}
onChange={(e) => setPriority(e.target.value)}
className="px-4 py-2 border rounded-lg"
>
<option value="LOW">Low</option>
<option value="MEDIUM">Medium</option>
<option value="HIGH">High</option>
</select>
<button
type="submit"
className="px-6 py-2 bg-blue-500 text-white rounded-lg"
>
Add
</button>
</form>

<div className="space-y-2">
{todos.map((todo) => (
<div key={todo.id} className="flex items-center gap-3 p-4 bg-white border rounded-lg">
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggle(todo.id)}
className="w-5 h-5 cursor-pointer"
/>
<div className="flex-1">
<p className={todo.completed ? 'line-through text-gray-400' : ''}>
{todo.title}
</p>
</div>
<span className="px-2 py-1 text-xs rounded">
{todo.priority}
</span>
<button
onClick={() => handleDelete(todo.id)}
className="text-red-500 hover:text-red-700"
>
Delete
</button>
</div>
))}
</div>
</div>
);
}

What Just Happened?

  1. User creates a todo → Mutation sent to AppSync
  2. AppSync saves it to DynamoDB → Resolver executes
  3. Subscription fires → All connected clients notified
  4. Everyone's UI updates instantly → No refresh needed
That's it. No WebSocket server. No connection management. No scaling headaches.

Architecture of Our Todo App

Deployment

Want to try it yourself? The GitHub repository includes: 
  • Complete CloudFormation template for one-command deployment
  • Frontend setup with Next.js 14 and Tailwind CSS
  • Authentication configured with AWS Cognito
  • Step-by-step deployment guide
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Clone the repo
git clone https://github.com/Ajaykumarkv17/AWS_AppSync_ToDo_App.git
cd aws-appsync-todo-app

# Deploy backend
cd backend
npm run deploy

# Configure frontend with your API details
cd ../frontend
# Update amplify-config.ts with your AppSync endpoint

# Run locally
npm install
npm run dev

The Real-Time Magic in Action

Here's what makes this special:
Open the app in two browser tabs:
  1. Create a todo in Tab 1 → It appears instantly in Tab 2
  2. Toggle completion in Tab 2 → Tab 1 updates immediately
  3. Delete from Tab 1 → Vanishes from Tab 2 in real-time
No polling. No manual refresh. Just pure real-time collaboration.

What About the Alternatives?

Look, AppSync isn't the only game in town. Let's talk about your other options.

1. API Gateway + WebSockets

AWS's other API service. More manual, but more flexible.
AppSync vs API Gateway:
  • AppSync = GraphQL + real-time built-in
  • API Gateway = REST + you manage WebSockets yourself
Use API Gateway when: You want REST, don't need GraphQL, or have existing REST infrastructure.

2. Self-Hosted GraphQL (Apollo, Hasura)

Run your own GraphQL server. Full control, full responsibility.
AppSync vs Self-Hosted:
  • AppSync = Managed, auto-scaling, pay-per-request
  • Self-Hosted = You manage servers, you handle scaling, 24/7 costs
Use Self-Hosted when: You need complete control or have very specific requirements.

3. Firebase

Google's real-time database. Great for mobile apps.
AppSync vs Firebase:
  • AppSync = AWS ecosystem, GraphQL, multiple data sources
  • Firebase = Google ecosystem, simpler, great mobile SDKs
Use Firebase when: You're building mobile-first or want simpler setup.

4. Supabase

Open-source Firebase alternative. PostgreSQL-based.
AppSync vs Supabase:
  • AppSync = AWS, multiple data sources, enterprise features
  • Supabase = PostgreSQL, open-source, generous free tier
Use Supabase when: You love PostgreSQL or want open-source.

5. Socket.io + Custom Server

Build it yourself with Node.js.
AppSync vs Socket.io:
  • AppSync = Managed, serverless, auto-scaling
  • Socket.io = Full control, custom everything, you manage scaling
Use Socket.io when: You need complete customization or have existing Node.js infrastructure.

Quick Decision Tree

So... Should You Use AppSync?

If you're building something that needs real-time updates, you're already in AWS, and you don't want to spend weeks setting up infrastructure—yeah, use AppSync. It's genuinely good at what it does.
The GraphQL part takes a bit of learning if you're coming from REST, but once it clicks, you'll wonder why you ever made 10 API calls when one would do.
The real-time subscriptions just work. No managing WebSocket connections, no dealing with reconnection logic, no scaling nightmares. It's all handled.
And the serverless model means you're not paying for idle servers. You pay for what you use. For most apps, that's way cheaper than running servers 24/7.
Use AppSync when:
  • You need real-time features
  • You want to ship fast
  • You don't want to manage infrastructure
Skip it when:
  • Simple REST is enough
  • You need complete control
  • Real-time isn't important

Learn More

Official Documentation

Tutorials & Workshops

Community Resources

Tools & Utilities

Video Content

My Demo Project

  • GitHub Repository: aws-appsync-todo-app 
  • Tech Stack: Next.js 14, AWS AppSync, DynamoDB, Cognito
  • Features: Real-time sync, authentication, optimistic updates
Thanks for reading this far! I hope this guide helped you understand AWS AppSync and gave you the confidence to try it in your own projects.

What's Next?

Now that you understand AppSync, here are some ideas to explore:
  1. Add more features to the todo app (tags, due dates, file attachments)
  2. Build a chat application with typing indicators and read receipts
  3. Create a collaborative whiteboard with real-time drawing
  4. Implement a live dashboard with streaming metrics
  5. Try AppSync with other data sources (Lambda, RDS, HTTP APIs)
The best way to learn is by building. Start small, break things, fix them, and iterate.
Happy coding! 🚀
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article